AI-Assisted Dev NotesRohit’s interview study guide
Chapter 08

AI in the Team

Leading a team that uses AI assistants: guidelines people can follow, reviewing AI-generated changes, security, licensing and data, measuring honestly whether it helps, and making shared AI setups work across a US–India team.

5 topics
Parts marked Advanced are extra depth. Skip them on a first read or a quick revision.

#Setting team guidelines for AI-assisted work

Added

Every developer on a team now has an AI assistant one keystroke away, and without guidance each of them makes up their own rules: one pastes a customer's log bundle into a public chatbot, another won't touch Copilot because they're not sure it's allowed, a third lets an agent open twenty-file pull requests nobody can review. Team guidelines turn those private guesses into one shared answer to four questions: which tools, with which data, for which work, and who is responsible for the result.

DORA's research gives this a name. The DORA AI Capabilities Model (Google Cloud blog, 23 September 2025, from the survey of nearly 5,000 people behind the 2025 report) lists seven capabilities that amplify AI's benefits, and the first is a "clear and communicated AI stance": "Your organization's position on AI-assisted tools must be clear and well-communicated." The same post notes that the capability doesn't measure what the policy says, only that people know it. The other six are healthy data ecosystems, AI-accessible internal data, strong version control practices, working in small batches, a user-centric focus and quality internal platforms, and most of them show up later in this chapter.

The mental model is a traffic-light map keyed to data, not to tools. Green: approved tools on company seats, for code and internal docs, with normal review. Amber: allowed with extra care, such as changes to device commands or authentication, which need a second reviewer and a note on what was verified. Red: never, whatever the tool, such as customer logs with personal data, credentials, or anything a contract says can't leave the company. Tools change every month; data classes and responsibilities don't, so a guideline written around them survives the next product launch.

What a good set of guidelines covers:

QuestionExample answer for a device-platform teamWhy it's there
Which tools?Copilot Business seats; the team's device-lab MCP server; nothing else without reviewdata terms differ between plans and vendors
Which data?public and internal: yes; confidential: approved tools only; restricted (customer logs, credentials): neverthe most common real incident is pasting the wrong thing
Which work?anything, but device commands, auth, migrations and release config get a second reviewerrisk depends on the code area, not on who typed it
Who owns the result?the person who opens the pull request, including agent-written code"the AI wrote it" is never an answer in review
How is it disclosed?an "AI-assisted" label and a "what I verified" line in the PR descriptiontells the reviewer where to look harder
Where do shared prompts and instructions live?in the repository, owned by the tech leadsso everyone gets the same context and changes are reviewed
How do we learn?a monthly session to share what worked and what bit usthe tools change faster than any document

Guidelines are easier to follow, and to check, when the rules are data. Here is a team's policy written as a small table plus a function that answers "may I use this tool for this?", run against some typical requests. The policy and the people are made up for illustration.

ai-policy.mjs
// A team's AI guidelines as data, and a checker that answers "may I use this tool for this?".
// The policy is ILLUSTRATIVE: the shape is the point, not these exact rules.
const policy = {
  tools: { // approved tools, each with the highest data class it may see
    "copilot-business": { maxData: "confidential", surfaces: ["completions", "chat", "agent-mode", "code-review", "cloud-agent"] },
    "device-lab-mcp": { maxData: "confidential", surfaces: ["agent-mode"] },
    "public-chatbot": { maxData: "public", surfaces: ["chat"] },
  },
  dataOrder: ["public", "internal", "confidential", "restricted"], // restricted: customer logs, credentials, keys
  reviewRequired: ["device-commands", "auth", "migrations", "release-config"], // code areas that always get a second human reviewer
};

function decide({ tool, surface, data, area, personalAccount = false }) {
  const t = policy.tools[tool];
  if (!t) return ["deny", `${tool} is not an approved tool; ask in #ai-tools to get it reviewed`];
  if (personalAccount) return ["deny", "use the company seat, not a personal plan (different data terms)"];
  if (!t.surfaces.includes(surface)) return ["deny", `${tool} is approved for ${t.surfaces.join(", ")} only`];
  const rank = (d) => policy.dataOrder.indexOf(d);
  if (data === "restricted") return ["deny", "restricted data never goes to an AI tool; use the redaction script first"];
  if (rank(data) > rank(t.maxData)) return ["deny", `${tool} may see ${t.maxData} data at most, this is ${data}`];
  if (policy.reviewRequired.includes(area)) return ["allow + review", `${area} changes need a second human reviewer and the AI-assisted label`];
  return ["allow", "normal review"];
}

const requests = [
  { who: "Priya (Bengaluru)", tool: "copilot-business", surface: "agent-mode", data: "internal", area: "web-ui" },
  { who: "Sam (Austin)", tool: "copilot-business", surface: "cloud-agent", data: "confidential", area: "device-commands" },
  { who: "Arjun (Bengaluru)", tool: "public-chatbot", surface: "chat", data: "internal", area: "web-ui" },
  { who: "Dana (Austin)", tool: "copilot-business", surface: "chat", data: "restricted", area: "support" },
  { who: "Ravi (Bengaluru)", tool: "copilot-business", surface: "chat", data: "internal", area: "web-ui", personalAccount: true },
  { who: "Lee (Austin)", tool: "random-mcp-server", surface: "agent-mode", data: "internal", area: "tooling" },
  { who: "Meera (Bengaluru)", tool: "device-lab-mcp", surface: "chat", data: "confidential", area: "support" },
];
for (const r of requests) {
  const [verdict, why] = decide(r);
  console.log(`${r.who.padEnd(18)} ${`${r.tool}/${r.surface}`.padEnd(30)} ${r.data.padEnd(12)} → ${verdict.padEnd(14)} ${why}`);
}
Result
Priya (Bengaluru)  copilot-business/agent-mode    internal     → allow          normal review
Sam (Austin)       copilot-business/cloud-agent   confidential → allow + review device-commands changes need a second human reviewer and the AI-assisted label
Arjun (Bengaluru)  public-chatbot/chat            internal     → deny           public-chatbot may see public data at most, this is internal
Dana (Austin)      copilot-business/chat          restricted   → deny           restricted data never goes to an AI tool; use the redaction script first
Ravi (Bengaluru)   copilot-business/chat          internal     → deny           use the company seat, not a personal plan (different data terms)
Lee (Austin)       random-mcp-server/agent-mode   internal     → deny           random-mcp-server is not an approved tool; ask in #ai-tools to get it reviewed
Meera (Bengaluru)  device-lab-mcp/chat            confidential → deny           device-lab-mcp is approved for agent-mode only
What’s happening
  1. policy.tools lists the approved tools, each with the highest data class it may see and the surfaces it's approved for. dataOrder ranks the classes from public to restricted. Everything else in the function follows from those two facts.
  2. Priya uses Copilot's agent mode on internal web UI code: approved tool, approved surface, data within its limit, low-risk area, so allow with normal review. Most requests should land here; guidelines that say "no" to ordinary work get ignored.
  3. Sam has Copilot's cloud agent work on device-command code with confidential data. It's allowed, but device-commands is in reviewRequired, so the answer is allow + review: a second human reviewer and the AI-assisted label. The risk comes from the code area, so the same rule would apply if Sam had typed every line.
  4. Arjun's public chatbot may see public data only; internal code is a level above, so deny, with the reason. Dana wants to paste restricted data (a customer log); that's denied for every tool, and the answer points to the redaction script from Security, licensing and data concerns.
  5. Ravi uses an approved product on a personal account: denied, because personal plans come with different data terms (chapter 02's What happens to your code and prompts quotes GitHub's). Lee's unknown MCP server is denied with a way forward ("ask in #ai-tools to get it reviewed"). Meera uses an approved MCP server on a surface it isn't approved for.
  6. What this demonstrates: a policy that can be evaluated is a policy people can ask. Every "no" carries a reason and a next step, which is what keeps people inside the rules instead of working around them.
AdvancedA one-page guideline, written down

The table above becomes a short document in the repository, where people find it and where changes are reviewed. This one is illustrative; a real one also links the company's security policy and names an owner.

docs/ai-guidelines.md
# Using AI assistants on the device admin team

Owner: tech leads (Austin + Bengaluru). Reviewed every quarter; last review 2026-10-27.

## Tools
- Use Copilot through your company seat only. Personal plans have different data terms.
- MCP servers: only the ones in `.vscode/mcp.json` in this repository. Want another? Open an issue
  with what it can read and do; a tech lead reviews it within two working days.

## Data
- Never paste customer logs, call recordings, credentials or keys into any AI tool.
  Run `npm run redact -- <file>` first and check the output.
- Device lab data is fine with approved tools.

## Work
- You own every line you merge, including what an agent wrote. Be able to explain it.
- Device commands, auth, migrations and release config: second reviewer, AI-assisted label.
- Keep PRs under 400 changed lines. Agents make big diffs easy; reviewers' attention doesn't scale.

## Sharing
- Prompts and instructions that work go in `.github/prompts/` and `.github/copilot-instructions.md`,
  through a pull request.
- Bring one thing that worked and one that didn't to the monthly AI session.
What’s happening
  1. Owner and review date at the top: a guideline nobody owns goes stale within one product cycle, and these products change monthly.
  2. Tools names the rule and the reason ("different data terms"), and gives a path for new tools with a response time. A guideline with no path to "yes" pushes people to use tools quietly.
  3. Data is the red light, stated as a concrete action (npm run redact), not a vague "be careful".
  4. Work repeats the ownership rule from the Tech Lead site's review topic and turns the risky areas into a label and a second reviewer. The 400-line limit matches the team's normal PR guideline: AI doesn't get a bigger budget.
  5. Sharing makes improvement part of the process: good prompts become files in the repository, reviewed like code, which is the subject of the last topic in this chapter.
AdvancedDORA's seven capabilities, applied

The seven capabilities from DORA's AI Capabilities Model (Google Cloud blog, 23 September 2025) read like a checklist for a lead. What each could mean on a device-platform team:

CapabilityOn this team
Clear and communicated AI stancethe one-page guideline, owned and reviewed quarterly
Healthy data ecosystemsdevice telemetry and support data that's accurate and documented, so tools built on it are trustworthy
AI-accessible internal datathe device-lab and docs MCP servers, instruction files that point at real patterns
Strong version control practicessmall commits, easy rollback, instruction and MCP config under review
Working in small batchesthe 400-line limit applies to agents too
User-centric focusfeatures judged by what room users and admins experience, not by how much code was produced
Quality internal platformsCI, test fakes for devices, a lab that anyone at either site can use

DORA presents these as capabilities that amplify AI's benefits, found from survey data; they're correlations, not a guarantee. They're useful as a structure for a plan, and as a vocabulary interviewers may recognise.

AdvancedRolling guidelines out
  • Start from what people already do. Ask the team, anonymously, which tools they use and for what. You'll find the real risks (and the real wins) faster than by writing rules first.
  • Pilot, then widen. One team for a few weeks, with a channel for questions, then everyone. The pilot is also your measurement baseline (Measuring whether it helps).
  • Make "yes" cheap. Company seats provisioned for everyone, approved MCP servers preconfigured in the repository, a redaction script that's one command. Rules are followed when the compliant path is the easy one.
  • Enforce what you can in tools. Organisation policies in Copilot (chapter 02's Copilot for teams), CODEOWNERS on instruction files, CI checks on MCP config. Keep the document for what tools can't check: judgement, ownership, disclosure.
  • Revisit on a schedule. Quarterly, and whenever a tool changes its data terms or a new capability (agents opening PRs, MCP servers) arrives.
AdvancedGuidelines for agents and MCP servers

Completions and chat suggest; agents act. Agent mode runs commands in your terminal, the cloud agent pushes branches and opens pull requests, and MCP servers give either of them new reach. Guidelines written for "autocomplete" don't cover that, so add a short section for agents:

TopicExample ruleWhy
Where agents may runagent mode in your own checkout; the cloud agent on repositories the org has enabledan agent acts with the permissions of where it runs
Commandsapprove each terminal command unless it's on the team's read-only list (npm test, git status)"always allow" on a shell is "always allow" on everything the shell can do
Secrets in the workspaceno .env with production values on machines where agents run; lab tokens onlyan agent can read every file the folder holds, ignored or not (Security, licensing and data concerns)
MCP serversonly those in the repository's .vscode/mcp.json; new ones by reviewa server's descriptions and results go straight into the model's context
Writes to devicesonly through the team's device MCP server, which confirms with a personthe guardrails live there (chapter 07)
Agent PRssame size limits, same review rules, a named human ownerGitHub doesn't count the requester's approval on cloud agent PRs

A new MCP server request gets the same five questions every time: what can it read, what can it do, whose credentials does it use, where does it run (local process or someone else's service), and who maintains it (pin a version, review upgrades). Chapter 06's Trusting third-party servers goes deeper.

AdvancedBehavioural version: introducing a practice
Allowed, careful or never?

Classify each under the illustrative policy above: (a) asking Copilot Chat on a company seat to explain an internal React component; (b) letting Copilot's cloud agent change the retry logic for device commands; (c) pasting a customer's device log, as received, into Copilot Chat to find the error; (d) using a free personal assistant account for internal code because your seat hasn't been provisioned yet.

Show answer

(a) allow; (b) allow + review; (c) deny until it's redacted; (d) deny, and get the seat fixed.

What’s happening
  1. (a) Approved tool, approved surface, internal data, ordinary code area: green, normal review.
  2. (b) Approved tool and surface, but device-commands is a review-required area: allowed with a second reviewer and the AI-assisted label. The rule is about the code's blast radius, not about the agent.
  3. (c) A raw customer log is restricted data: names, phone numbers, meeting titles, network details. Denied for every tool. After redaction it may become confidential, which an approved tool may see.
  4. (d) The tool may be fine, but a personal account has different data terms and none of the organisation's policies. The right fix is the provisioning delay, which is the lead's problem to solve quickly.
  5. The pattern: each answer follows from data class, code area and account, not from the product name.

#Reviewing AI-generated changes

Added

The Tech Lead site's Reviewing AI-generated code teaches how to read an AI-written pull request: the "confident new contractor" mental model, a worked example where a generated retry helper reboots a device twice and logs its admin PIN, and a checklist (does the author understand every line, do the APIs exist, do the tests test the requirement, security hot spots, retries and errors, fit with the codebase, size). This topic doesn't repeat that. It covers what a lead adds around reviewing: making AI involvement visible, routing changes to the right reviewers by risk, and the rules GitHub applies to PRs its cloud agent opens.

The mental model is air traffic control, not a second pilot. A reviewer can't re-fly every flight. What a lead can do is make sure each change is labelled, routed to someone qualified for its risk, and held until the checks for that kind of change have passed. Risk comes from what the change touches, not from who typed it, so the routing rules are the same for human-written and AI-written code; disclosure just tells the reviewer where to look harder.

A pull request template makes disclosure routine instead of awkward. This is illustrative:

.github/pull_request_template.md
## What and why

## How I verified it
<!-- Commands you ran, devices or fakes you tested against, what you checked by hand. -->

## AI assistance
- [ ] None
- [ ] Assistant suggestions I edited (completions, chat)
- [ ] Generated by an agent (agent mode or the cloud agent): which parts?

What I checked in the generated parts:
<!-- e.g. "rewrote the retry logic; added 400 and timeout tests; checked every import exists" -->

## Risk
- [ ] Touches device commands, auth, migrations or release config (needs a second reviewer)
What’s happening
  1. "How I verified it" comes before the AI section on purpose: verification is required for every change, AI-assisted or not.
  2. The three AI options describe how much was generated, not whether it's "allowed". A reviewer reads an agent-generated change differently from one where the author accepted a few completions.
  3. "What I checked in the generated parts" is the useful line. "Rewrote the retry logic; added 400 and timeout tests" tells the reviewer what was already questioned and what wasn't.
  4. The risk box mirrors the guideline's review-required areas, so the author routes the PR before the reviewer has to.
  5. It's a review aid, not a confession: the Tech Lead topic makes the same point about saying in the description what was generated and what you verified.
AdvancedRouting can be automated

Routing can be automated. Here's a sketch of the checks a bot or CI job could run on each PR: who must review it, and what the reviewer should look at first. The rules and PRs are made up.

review-routing.mjs
// Routing pull requests by risk, not by who (or what) typed the code. ILLUSTRATIVE rules and PRs.
const RISKY = [
  [/^server\/src\/devices\/commands/, "device commands (reboot, factory reset)", "@api-leads"],
  [/^server\/src\/auth\//, "authentication", "@security"],
  [/^server\/migrations\//, "database migration", "@api-leads"],
  [/^(\.github\/(copilot-instructions\.md|instructions\/|prompts\/)|AGENTS\.md|\.vscode\/mcp\.json)/, "AI instructions or MCP config", "@tech-leads"],
];
const MAX_LINES = 400;

function route(pr) {
  const notes = [];
  const owners = new Set();
  let reviewers = 1;
  for (const f of pr.files) for (const [re, what, team] of RISKY) if (re.test(f.path)) { owners.add(team); notes.push(`touches ${what}`); }
  if (owners.size) reviewers = 2;
  const lines = pr.files.reduce((n, f) => n + f.added + f.deleted, 0);
  if (lines > MAX_LINES) notes.push(`${lines} lines changed (limit ${MAX_LINES}): ask for a split`);
  const src = pr.files.filter((f) => f.path.includes("/src/"));
  const tests = pr.files.filter((f) => f.path.includes("/test/"));
  if (src.length && !tests.length) notes.push("source changed, no test changed");
  if (tests.some((f) => f.deleted > f.added)) notes.push("tests shrank: check nothing was deleted to make them pass");
  if (pr.author === "copilot-cloud-agent") notes.push(`agent PR: ${pr.requestedBy}'s approval doesn't count; another reviewer must approve`);
  if (pr.aiAssisted && !pr.verificationNote) notes.push('AI-assisted but no "what I verified" note in the description');
  return { reviewers, owners: [...owners], notes };
}

const prs = [
  { id: 101, author: "priya", aiAssisted: true, verificationNote: true,
    files: [{ path: "web/src/rooms/RoomCard.tsx", added: 40, deleted: 12 }, { path: "web/test/RoomCard.test.tsx", added: 30, deleted: 0 }] },
  { id: 102, author: "copilot-cloud-agent", requestedBy: "sam", aiAssisted: true, verificationNote: false,
    files: [{ path: "server/src/devices/commands.ts", added: 60, deleted: 5 }, { path: "server/test/commands.test.ts", added: 4, deleted: 22 }] },
  { id: 103, author: "arjun", aiAssisted: true, verificationNote: true,
    files: [{ path: "server/src/rooms/list.ts", added: 380, deleted: 90 }] },
  { id: 104, author: "dana", aiAssisted: false,
    files: [{ path: ".github/copilot-instructions.md", added: 2, deleted: 9 }] },
];
for (const pr of prs) {
  const r = route(pr);
  console.log(`#${pr.id} by ${pr.author}${pr.aiAssisted ? " (AI-assisted)" : ""}: ${r.reviewers} reviewer(s)${r.owners.length ? `, owners ${r.owners.join(" ")}` : ""}`);
  for (const n of r.notes) console.log(`  - ${n}`);
}
Result
#101 by priya (AI-assisted): 1 reviewer(s)
#102 by copilot-cloud-agent (AI-assisted): 2 reviewer(s), owners @api-leads
  - touches device commands (reboot, factory reset)
  - tests shrank: check nothing was deleted to make them pass
  - agent PR: sam's approval doesn't count; another reviewer must approve
  - AI-assisted but no "what I verified" note in the description
#103 by arjun (AI-assisted): 1 reviewer(s)
  - 470 lines changed (limit 400): ask for a split
  - source changed, no test changed
#104 by dana: 2 reviewer(s), owners @tech-leads
  - touches AI instructions or MCP config
What’s happening
  1. RISKY maps path patterns to an area and an owning team: device commands and migrations to @api-leads, auth to @security, and AI instruction and MCP config files to @tech-leads. Any match raises the PR to two reviewers.
  2. #101 (Priya, AI-assisted, web UI with tests and a verification note) gets one reviewer and no notes. AI-assisted isn't a risk flag by itself; most such PRs should look like this.
  3. #102 was opened by Copilot's cloud agent at Sam's request. It touches device commands (two reviewers, @api-leads), its tests shrank (22 lines deleted, 4 added, which is how "make the tests pass" sometimes gets done), it has no verification note, and the routing reminds the reviewer that Sam's approval won't count.
  4. That reminder reflects GitHub's documented rule for cloud agent PRs ("Review output from Copilot" and "Risks and mitigations for GitHub Copilot cloud agent", read 27 October 2026): if the repository requires approvals, the approval of the person who asked Copilot for the PR "won't count toward the required number"; someone else must approve. By default GitHub Actions workflows also don't run on Copilot's pushes until a reviewer clicks Approve and run workflows, and the docs say to check workflow changes first because workflows can access secrets.
  5. #103 is 470 changed lines with no tests: ask for a split and tests, exactly as for a human PR. #104 is a human change to .github/copilot-instructions.md that deletes more than it adds, which goes to the tech leads, because it changes what every assistant and Copilot's own code review are told.
  6. What this demonstrates: the routing never asks "was this AI?" to decide risk. It uses AI involvement only for the notes that help the reviewer: missing verification, shrinking tests, the agent-approval rule.
AdvancedWhy instruction files get the strictest review

.github/copilot-instructions.md, .github/instructions/, AGENTS.md, prompt files and .vscode/mcp.json look like documentation, but they change what every assistant on the team does. Chapter 02 quotes GitHub's docs: Copilot code review reads custom instructions from the pull request's head branch, so a PR can change the instructions its own AI review follows (Custom instructions and prompt files). An MCP config change can add a server with access to your code and secrets. Treat these like CI configuration:

  • CODEOWNERS entries with the tech leads for every one of them (the last topic's check enforces it).
  • Read deletions as carefully as additions: removing "never retry device commands without an idempotency key" is a behaviour change.
  • Keep them short and testable. A rule a reviewer can't check ("write good code") is noise that dilutes the rules that matter.
AdvancedAI review as a first pass

Copilot code review and similar tools are useful as a first pass: they catch typos, missing null checks and inconsistent naming before a human spends time on them. GitHub's docs say Copilot "is not guaranteed to spot all problems" and to always validate its feedback (quoted on the Tech Lead site and in chapter 02's Copilot code review). Copilot approvals that count toward required reviews exist in public preview and are off by default. A team rule that works: AI review comments are suggestions to the author; a human reviewer who owns the area approves.

AdvancedSpotting tests that were weakened

The most expensive AI-review miss is a test changed to agree with the code. Agents told "make the tests pass" sometimes loosen an assertion, skip a test or delete the line that failed; chapter 03's Ways an assistant "passes" tests without doing the work lists the patterns. A small check over the test diff points the reviewer at them. The diff below is made up:

test-weakening.mjs
// Spotting tests that were weakened to make a change pass. Reads a unified diff of test files.
// The diff is MADE UP, in the shape `git diff main...branch -- test/` prints.
const diff = `diff --git a/test/commands.test.ts b/test/commands.test.ts
--- a/test/commands.test.ts
+++ b/test/commands.test.ts
@@ -10,14 +10,12 @@
 test("reboot is sent once", async () => {
-  expect(device.reboots).toBe(1);
+  expect(device.reboots).toBeGreaterThan(0);
 });
-test("400 is not retried", async () => {
+test.skip("400 is not retried", async () => {
   await expect(send(badRequest)).rejects.toThrow("answered 400");
-  expect(fetchFn).toHaveBeenCalledTimes(1);
 });
 test("timeout error names the device", async () => {
-  await expect(send(slow)).rejects.toThrow("huddle-2 did not answer");
+  await expect(send(slow)).rejects.toThrow();
 });
+test("sends a command", async () => {
+  expect(await send(ok)).toBeDefined();
+});`;

const removed = diff.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---"));
const added = diff.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++"));
const count = (lines, re) => lines.filter((l) => re.test(l)).length;
const findings = [];
const assertions = /expect\(|assert\./;
console.log(`assertion lines: ${count(removed, assertions)} removed, ${count(added, assertions)} added`);
for (const l of added) {
  if (/\.(skip|todo)\(|\bxit\(|\bxdescribe\(/.test(l)) findings.push(`test disabled: ${l.slice(1).trim()}`);
  if (/toBeGreaterThan\(0\)|toBeDefined\(\)|toBeTruthy\(\)|\.toThrow\(\)/.test(l)) findings.push(`loose assertion: ${l.slice(1).trim()}`);
}
for (const l of removed) {
  const exact = l.match(/\.(toBe|toEqual|toHaveBeenCalledTimes)\((.+)\);/);
  if (exact) findings.push(`exact check removed: ${l.slice(1).trim()}`);
}
console.log(findings.length ? findings.map((f) => `! ${f}`).join("\n") : "no weakening found");
Result
assertion lines: 3 removed, 3 added
! loose assertion: expect(device.reboots).toBeGreaterThan(0);
! test disabled: test.skip("400 is not retried", async () => {
! loose assertion: await expect(send(slow)).rejects.toThrow();
! loose assertion: expect(await send(ok)).toBeDefined();
! exact check removed: expect(device.reboots).toBe(1);
! exact check removed: expect(fetchFn).toHaveBeenCalledTimes(1);
What’s happening
  1. The check reads only the test files' diff, the part of a PR reviewers skim fastest, and splits it into removed (-) and added (+) lines.
  2. Counting assertions isn't enough: 3 assertion lines were removed and 3 added, so a "no fewer assertions" rule would pass this PR. Every one of the replacements is weaker.
  3. Loose assertions added: toBe(1) became toBeGreaterThan(0), so the "reboot is sent once" test now passes when the device reboots twice, which is the exact bug the Tech Lead site's example PR had. toThrow("huddle-2 did not answer") became toThrow(), which passes for any error at all. A new test asserts only toBeDefined().
  4. A test disabled: test.skip("400 is not retried"…) and its call-count check removed. The behaviour it protected is now unprotected, and the suite still says "pass".
  5. Exact checks removed: both toBe(1) and toHaveBeenCalledTimes(1) are listed, so the reviewer can ask "why did these stop being true?". The answer is either a legitimate behaviour change (then the PR description should say so) or a bug the test caught.
  6. It's a heuristic: it flags patterns, not intent, and a human decides. As a CI annotation on PRs that touch tests, it puts the right three lines in front of the reviewer.
AdvancedMore code, same reviewers

Assistants raise the amount of code a team produces faster than they raise its capacity to review it. DORA's 2025 report says AI-assisted coding "can increase the volume and velocity of changes" (p. 64), that faster code still has to pass the same testing and review queues (p. 81), and hypothesises that AI's link to instability is partly "because it is harder to review larger batches of code" (p. 57); InfoQ's summary (17 March 2026) calls it increased "code review complexity". Signs you're there: review queues growing, approvals with no comments on large PRs, more reverts. What a lead can change:

  • Hold the size limit. Small batches are one of DORA's seven AI capabilities, for exactly this reason.
  • Count review as work. Put reviews in planning and stand-ups, so reviewing an agent's PR isn't something people squeeze in.
  • Automate the mechanical part: lint, types, tests, the weakening and dependency checks, Copilot code review as a first pass, so human attention goes to design and risk.
  • Spread review across sites with owners in both time zones, so the queue moves around the clock instead of waiting for one person.
Who approves?

Branch protection on main requires one approval. Sam assigns an issue to Copilot's cloud agent, which opens a PR. Sam reviews it carefully and approves. Can it merge? What has to happen, and what would you check first?

Show answer

No. Sam's approval doesn't count toward the required approval for a PR Sam asked Copilot to create; another reviewer must approve. Check the workflow and test changes first, then approve and run workflows.

What’s happening
  1. GitHub's docs: if the repository requires approvals, the requester's approval of a Copilot pull request "won't count toward the required number" ("Review output from Copilot"), and the cloud agent's risks page says it prevents that user from approving. Sam's careful review is still valuable; it just isn't the gate.
  2. Someone else with write access must approve. That makes the agent's PR follow the same "two people saw it" rule as a human PR, where the author can't approve their own work.
  3. Workflows haven't run on Copilot's pushes yet, so there's no CI result. The reviewer clicks Approve and run workflows, but first reads any change under .github/workflows/, because a workflow change runs with the repository's secrets.
  4. Then the normal checklist: tests that shrank, unfamiliar packages, risky areas. And if Copilot had opened the PR under its own app identity rather than on Sam's behalf (for example when prompted from a shared Slack or Teams channel), rulesets by default require one approval more than configured ("Available rules for rulesets", public preview, read 27 October 2026).
  5. The lesson for an interview: agent PRs aren't "pre-reviewed by AI"; GitHub's own rules treat the requester like an author.

#Security, licensing and data concerns

Added

When a team adopts AI assistants, the concerns fall into three separate questions, and keeping them separate stops the discussion turning into vague worry. What goes out? Code, prompts, logs and files travel to a model provider, and some of them shouldn't. What comes in? Generated code can be insecure, can resemble licensed public code, and can import packages that don't exist or shouldn't be trusted. What can the tools do? Agents and MCP servers act with your permissions: they run commands, read repositories and call APIs.

The mental model is customs at a border. Outbound: declare what leaves, and some things never leave (redact, exclude, keep restricted data out). Inbound: inspect what arrives (review, scanners, dependency and licence checks). And the people and vehicles crossing (agents, MCP servers) carry passes that say what they're allowed to do, issued per trip, with the least access that does the job.

QuestionConcernControls
What goes outsecrets, customer data, partner code sent to a providercompany seats with business data terms, content exclusion, redaction before pasting, keep secrets out of repos (chapter 02)
What comes in: securityinjection, path traversal, weak crypto, logging secretsthe review checklist, static analysis, tests that try to break it (chapter 03)
What comes in: licensingsuggestions that match public code under a licence you can't acceptcode referencing or blocking matches, licence checks on new dependencies
What comes in: supply chainhallucinated or typo-squatted packagesdependency review, registry checks, lockfiles
What the tools can doan agent or MCP server with too much access, or steered by injected textleast privilege, confirmations, approved servers only (chapter 06)

On a device platform the most likely real incident is the first row: someone pastes a support log into a chat to ask "what's wrong?". Logs carry meeting titles, phone numbers, email addresses, IP addresses and, too often, credentials. A redaction step that's one command makes the safe path the easy one. Here is a small one, with consistent placeholders so the result is still useful to a model:

redact-context.mjs
// Scrubbing a device log before it goes into any AI tool. Consistent placeholders keep it useful:
// the same IP becomes the same <ip-1> everywhere, so the model can still follow it. The log is MADE UP.
const log = `09:00:01 boot ok serial=8L23094KX7 mac=00:e0:db:4a:1f:9c
09:01:12 sip registered huddle2@rooms.example.com via 10.20.4.17
09:02:40 web login admin password=S3cret! from 10.20.9.88
09:03:05 call 4411 joined: title='Acme acquisition - board only' from +1 512 555 0142
09:04:58 usb port 2 overcurrent
09:05:03 cloud token=eyJhbGciOiJIUzI1NiJ9.abc.def refresh failed; retry via 10.20.4.17
09:06:10 support note: contact priya.n@example.com`;

const rules = [ // order matters: secrets first, so nothing else half-matches them
  ["secret", /\b(password|token|pin|community)=\S+/gi, (m, k) => `${k}=[removed]`],
  ["title", /title='[^']*'/g, () => "title=<meeting-title>"],
  ["email", /\b[\w.+-]+@[\w-]+(\.[\w-]+)+\b/g],
  ["phone", /\+\d[\d ]{8,14}\d/g],
  ["ip", /\b(\d{1,3}\.){3}\d{1,3}\b/g],
  ["mac", /\b([0-9a-f]{2}:){5}[0-9a-f]{2}\b/gi],
  ["serial", /\bserial=\w+/g, () => "serial=<serial>"],
];
const seen = new Map(); // original value → placeholder
const counts = {};
let out = log;
for (const [kind, re, fixed] of rules) {
  out = out.replace(re, (m, ...groups) => {
    counts[kind] = (counts[kind] ?? 0) + 1;
    if (fixed) return fixed(m, ...groups);
    if (!seen.has(m)) seen.set(m, `<${kind}-${[...seen.values()].filter((v) => v.startsWith(`<${kind}-`)).length + 1}>`);
    return seen.get(m);
  });
}
console.log(out);
console.log("\nreplaced:", Object.entries(counts).map(([k, n]) => `${k} ${n}`).join(", "));
console.log("left for a human to check:", out.split("\n").filter((l) => /note|comment|name=/.test(l)).length, "free-text line(s)");
Result
09:00:01 boot ok serial=<serial> mac=<mac-1>
09:01:12 sip registered <email-1> via <ip-1>
09:02:40 web login admin password=[removed] from <ip-2>
09:03:05 call 4411 joined: title=<meeting-title> from <phone-1>
09:04:58 usb port 2 overcurrent
09:05:03 cloud token=[removed] refresh failed; retry via <ip-1>
09:06:10 support note: contact <email-2>

replaced: secret 2, title 1, email 2, phone 1, ip 3, mac 1, serial 1
left for a human to check: 1 free-text line(s)
What’s happening
  1. Secrets first: password=S3cret! and token=eyJ… become password=[removed] and token=[removed]. The secret rule runs before the others, so a token containing an @ or a dot can't half-match the email rule and leave pieces behind.
  2. Known free-text fields: the meeting title "Acme acquisition - board only" becomes <meeting-title>. A title can be the most sensitive thing in the log, and no pattern for emails or numbers would have caught it; you only get it by knowing the log's format.
  3. Identifiers with consistent placeholders: 10.20.4.17 appears twice and becomes <ip-1> both times, while 10.20.9.88 becomes <ip-2>. The model can still reason "the SIP registration and the cloud retry used the same address" without seeing it. The same goes for <email-1>, <phone-1> and <mac-1>.
  4. The serial number becomes <serial>: device serials can identify a customer to anyone with access to the vendor's systems.
  5. The summary counts what was replaced, and the last line flags the support note line as free text for a human to check. Here it held an email, which was caught, but free text can hold a name or a customer's words that no regular expression knows about. Redaction reduces risk; it doesn't certify a file as safe, so the guideline still says to check the output.
AdvancedLicensing: matching public code

The worry: an assistant reproduces a chunk of public code under a licence (GPL, for example) your product can't accept. GitHub's docs on code referencing (read 27 October 2026) say Copilot checks suggestions against code in public repositories on GitHub.com. For inline suggestions, it compares a suggestion plus about 150 characters of surrounding code against an index of public repositories (private repositories and code outside GitHub aren't searched). Depending on the policy for your account or organisation, matching suggestions are blocked, or shown with information about the match, including the repository and any detected licence. Copilot Chat responses that contain matching code include a link to the match details.

What a lead does with that:

  • Decide the policy deliberately with whoever owns legal risk: block matches, or allow them with references so developers can check the licence. It's an organisation setting in Copilot's policies, listed in chapter 02's policy table.
  • Know its limits. It covers matches against public GitHub code, of a certain length, for Copilot's own suggestions. It says nothing about code pasted from a chat tool outside the organisation's policies.
  • Licences also arrive through dependencies, which is where most licence problems actually come from. That's the next model.
  • Contractual protections (indemnities) vary by vendor, plan and agreement and can depend on settings; check your agreement with legal rather than assuming.
AdvancedDependencies an assistant added

Assistants add packages freely: a name that sounds right, a helper for one function. Three things can go wrong: the licence isn't acceptable, the package doesn't exist (a hallucinated name, which an attacker can register later, the "slopsquatting" risk in chapter 03's Spotting hallucinated APIs and subtle bugs), or it's a new, barely used look-alike of a popular name. A check on every PR that changes package.json catches all three. The registry data here is a made-up snapshot (none of these four names was on npm when checked on 27 October 2026), so the output never changes:

deps-check.mjs
// New dependencies in an AI-assisted pull request, checked against a licence allowlist and the registry.
// The registry answers are a FAKE snapshot: every package name and figure here is made up, so the output never changes.
const today = Date.parse("2026-10-27");
const registry = { // name → what `npm view <name> license time.created` would tell you
  "room-time-utils": { license: "MIT", created: "2019-03-11", weeklyDownloads: 250_000 },
  "device-sse-helpers": { license: "GPL-3.0-only", created: "2023-05-02", weeklyDownloads: 4_100 },
  "expres-validator-utils": { license: "MIT", created: "2026-10-24", weeklyDownloads: 12 },
};
const ALLOWED = new Set(["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC"]);

const before = { "express": "^5.3.0", "zod": "^4.6.0" };
const after = { "express": "^5.3.0", "zod": "^4.6.0", "room-time-utils": "^3.1.0", "device-sse-helpers": "^2.0.1",
  "expres-validator-utils": "^1.0.0", "express-rate-limiter-pro": "^3.2.0" };

for (const name of Object.keys(after).filter((n) => !(n in before))) {
  const meta = registry[name];
  let verdict;
  if (!meta) verdict = "BLOCK  not on the registry: a hallucinated name, and a squatter could register it tomorrow";
  else if (!ALLOWED.has(meta.license)) verdict = `ASK    licence ${meta.license} is not on the allowlist: needs a decision from legal`;
  else if ((today - Date.parse(meta.created)) / 86_400_000 < 30 || meta.weeklyDownloads < 1000)
    verdict = `CHECK  created ${meta.created}, ${meta.weeklyDownloads} downloads/week: new and little used (typo of a popular name?)`;
  else verdict = `ok     ${meta.license}`;
  console.log(`${name.padEnd(26)} ${verdict}`);
}
Result
room-time-utils            ok     MIT
device-sse-helpers         ASK    licence GPL-3.0-only is not on the allowlist: needs a decision from legal
expres-validator-utils     CHECK  created 2026-10-24, 12 downloads/week: new and little used (typo of a popular name?)
express-rate-limiter-pro   BLOCK  not on the registry: a hallucinated name, and a squatter could register it tomorrow
What’s happening
  1. The check diffs dependencies before and after the PR: express and zod were already there, so only the four new names are examined.
  2. room-time-utils: MIT is on the allowlist, it's years old and widely used: ok.
  3. device-sse-helpers: GPL-3.0-only isn't on the allowlist. The answer is ASK, not BLOCK: whether a copyleft dependency is acceptable depends on how the product is distributed, which is a legal decision, not a lint rule.
  4. expres-validator-utils: created three days ago, 12 downloads a week, and one letter away from a popular name. That's the profile of a typo-squat: CHECK by a human before it's installed anywhere.
  5. express-rate-limiter-pro: not on the registry at all. The assistant invented it. BLOCK, because if someone registers that name tomorrow, the next npm install runs their code. In a real check the registry lookup is npm view <name>; a 404 is the signal.
AdvancedWhat an agent in your folder can read

.gitignore keeps files out of commits, so reviewers never see them. It doesn't keep them away from an agent: an agent with a file tool or a shell in your project folder can read whatever the folder holds. Chapter 02 quotes GitHub's docs that content exclusion doesn't apply to agent mode either. This builds a throwaway project with fake secrets and lists each file the way git and an agent see it:

agent-sees.mjs
// What an agent running in your project folder can read, versus what git (and so a reviewer) sees.
// Builds a throwaway project in a temp folder; the "secrets" are fake.
import { execFileSync } from "node:child_process";
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, statSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, relative } from "node:path";

const dir = mkdtempSync(join(tmpdir(), "agent-sees-"));
const write = (p, text) => { mkdirSync(join(dir, p, ".."), { recursive: true }); writeFileSync(join(dir, p), text); };
write(".gitignore", ".env\n*.pem\ncaptures/\n");
write("src/index.js", "export const port = 3000;\n");
write(".env", "DEVICE_API_TOKEN=lab-9f2c41d7\nCLOUD_CLIENT_SECRET=fake-secret\n");
write("certs/device-ca.pem", "-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----\n");
write("captures/huddle-2-call-4411.log", "09:03:05 call 4411 joined: title='Acme acquisition'\n");
execFileSync("git", ["init", "-q"], { cwd: dir });

const tracked = new Set(execFileSync("git", ["ls-files", "--others", "--cached", "--exclude-standard"], { cwd: dir, encoding: "utf8" }).trim().split("\n"));
const walk = (d) => readdirSync(d).flatMap((n) => { const p = join(d, n); if (n === ".git") return []; return statSync(p).isDirectory() ? walk(p) : [relative(dir, p)]; });
const looksSecret = /(TOKEN|SECRET|PASSWORD)=|PRIVATE KEY|title='/;
for (const f of walk(dir).sort()) {
  const text = readFileSync(join(dir, f), "utf8"); // what any file tool or shell in this folder can do
  console.log(`${f.padEnd(32)} git: ${tracked.has(f) ? "visible" : "ignored"}   agent: readable${looksSecret.test(text) ? "   ← sensitive" : ""}`);
}
rmSync(dir, { recursive: true, force: true });
Result
.env                             git: ignored   agent: readable   ← sensitive
.gitignore                       git: visible   agent: readable
captures/huddle-2-call-4411.log  git: ignored   agent: readable   ← sensitive
certs/device-ca.pem              git: ignored   agent: readable   ← sensitive
src/index.js                     git: visible   agent: readable
What’s happening
  1. The project has ordinary source (src/index.js), a .gitignore, and three ignored items: .env with a device API token and a client secret, a private key under certs/, and a call capture log under captures/ with a meeting title. All the secrets are fake.
  2. git ls-files --others --cached --exclude-standard lists what git would consider: .gitignore and src/index.js. The three sensitive files are ignored, so they never appear in a diff, a pull request or a code review.
  3. Every file is readable to anything running in the folder, because readFileSync (or cat, or an agent's read-file tool) doesn't care about .gitignore. Three of them match the "sensitive" pattern.
  4. So the files reviewers never see are exactly the ones an agent can read and might quote into a prompt, a log or a tool argument. The fixes are about the workspace, not the model: no production secrets in working folders where agents run (lab tokens only, from a secret manager), captures kept outside the repository, and per-host settings that stop agents reading certain paths where the host has them.
  5. This is the agent-era version of an old rule: don't keep secrets in places where tools you don't fully control can read them.
AdvancedAgents and MCP servers: permissions are the risk

An agent runs with the permissions of whatever launched it: your shell, your Git credentials, your cloud login. An MCP server runs with its own credentials, and its tool descriptions and results go straight into the model's context. For a team:

  • Approved servers only, listed in the repository's .vscode/mcp.json and, for Copilot Business and Enterprise, enforced with the organisation's MCP policy and allowlist (chapter 02's MCP for organisations).
  • Pin versions of local servers and review upgrades like any dependency; a server's tool descriptions can change in a new release (Trusting third-party servers).
  • Least privilege per server: a read-only token for the docs server; a device-lab token that can't reach customer devices for the lab server (Guardrails for tools that change real hardware).
  • Watch the combination of private data, untrusted content and a way to send data out in one session. That's the condition for the prompt-injection leaks described in chapter 06.
AdvancedData that's special on a device platform

General guidance talks about "customer data"; on a video-conferencing platform it has specific shapes worth naming in the guideline, so people recognise them:

  • Meeting metadata: titles, organisers, attendee names and calendar subjects show up in device logs and call history. A title like "Acme acquisition, board only" is confidential business information.
  • Call content: recordings, transcripts, captures of audio or video used to debug echo or framing. These never go into a general AI tool.
  • Network details: IP addresses, SIP URIs, VLANs and certificates from customer sites describe their internal network.
  • Device credentials and keys: admin passwords, provisioning tokens, private keys in debug bundles.
  • Partner material: SDKs and documents from partners under agreements that may forbid sending them to third-party services. Chapter 02's content exclusion example has such a path.

For each, the guideline names the class (restricted for most of these) and the safe path: redact, use lab data instead, or ask.

Which control?

Match each incident to the control that would have stopped it: (a) a developer pastes a customer's call log into a chatbot; (b) a PR adds a dependency that doesn't exist on npm; (c) a suggestion reproduces 40 lines of a GPL library; (d) an MCP server for tickets starts returning tool descriptions that tell the model to send repository secrets to a URL.

Show answer

(a) redaction and the data rule; (b) the dependency check; (c) the public-code matching policy (block or reference) plus review; (d) approved, pinned servers with reviewed upgrades, and least-privilege tokens.

What’s happening
  1. (a) is outbound data. Redaction before pasting, plus the guideline that restricted data never goes into any tool. Content exclusion wouldn't help: the log wasn't a file in the repository.
  2. (b) is inbound supply chain. A registry lookup on new dependency names fails it before install; review alone often misses a plausible name.
  3. (c) is inbound licensing. Blocking or referencing matching suggestions is the tool-side control; the reviewer and a licence check are the human side.
  4. (d) is tool poisoning. Pinning the server's version means a malicious update doesn't arrive silently; reviewing upgrades catches the new descriptions; a least-privilege token means even a steered model can't read secrets the server never had.
  5. Each incident needed a different control, which is why "be careful with AI" isn't a policy.

#Measuring whether it helps

Added

"Is AI making us faster?" is the question every leadership team asks, and it's easy to answer badly. Vendor numbers come from tasks unlike yours, acceptance rates measure activity rather than outcomes, and developers' own sense of speed has been measured to be wrong. A lead's job is to measure the delivery system, with a comparison, and to say plainly what the numbers can't show.

The mental model is three layers, read from the top down. Adoption (are people using it?) is easy to count and says nothing about value. Activity (suggestions accepted, lines generated, PRs opened by agents) shows the tool is busy, not that the work is better. Outcomes (lead time, change failure rate, escaped defects, time spent on toil, how developers feel about their work) are what matter and are hardest to attribute. Every outcome needs a counter-metric: if PRs merge faster, check reverts and incidents too.

Outcomes lead time, change failure rate, escaped defects, review time, survey what matters; hardest to attribute to AI Activity suggestions accepted, lines generated, agent PRs opened shows the tool is busy, not that the work is better Adoption active users, seats used, which features and surfaces easy to count; says nothing about value Counter-metrics faster merges? check reverts, incidents, review load, PR size A comparison baseline period and a team that didn't change, or staggered rollout Report outcomes with their counter-metrics and a comparison; use the lower layers to explain them.
Adoption and activity explain; outcomes decide. An outcome without a counter-metric and a comparison isn't evidence about AI.

What the research shows, and what it doesn't

Chapter 01's Where they help and where they hurt tabulates the main studies. For a lead the useful column is what each one can't tell you about your team. Each was checked on the publisher's page in October 2026.

StudyWhat it foundWhat it doesn't show
Peng, Kalliamvakou, Cihon, Demirer, arXiv 2302.06590 (13 Feb 2023)95 developers recruited on Upwork, 45 with Copilot and 50 without; among those who finished (35 per group), the Copilot group wrote an HTTP server in JavaScript 55.8% faster (71 vs 161 minutes)one small, standardised greenfield task; the authors say it didn't examine code quality and may not generalise to large collaborative projects
Cui, Demirer, Jaffe, Musolff, Peng, Salz, "The Effects of Generative AI on High-Skilled Work" (SSRN 2024; Management Science, online February 2026)randomised Copilot access at Microsoft, Accenture and a Fortune 100 company, 4,867 developers pooled: 26.08% more completed tasks (standard error 10.3%); newer staff gained more in the paper's estimates (27–39% for short-tenure vs 8–13% for long-tenure developers; 21–40% junior vs 7–16% senior), which the authors call noisy and not statistically significant (MIT Sloan's summary, 4 Nov 2024, labels the tenure figures junior and senior)counts completed tasks (pull requests); quality is proxied only by build success rate; each experiment alone is noisy; adoption was incomplete (Accenture's plateaued around 60%, and 30–40% of engineers never tried the tool)
GitHub (Jared Bauer), "Does GitHub Copilot improve code quality?" (18 Nov 2024, updated 6 Feb 2025)202 experienced Python developers, randomised: Copilot group 53.2% more likely to pass all 10 unit tests (the November 2024 version said 56%, before an invalid submission was removed); 13.6% more lines of code per "code error" (18.2 vs 16.0); readability and other qualities rated 2–4% higherrun by the vendor; one CRUD-style task; "code errors" means readability problems, not bugs; the reviewers were the participants who passed; relative percentages make small differences look large (criticised on Jadarma's blog, 20 Nov 2024, and in The Register, 3 Dec 2024)
METR, early-2025 study (10 Jul 2025)16 experienced maintainers, 246 real issues in their own large repositories: tasks took 19% longer with AI allowed; they expected 24% faster and afterwards believed 20% fasterMETR says it doesn't show AI fails to speed up most developers; small sample; one setting; early-2025 tools
METR, "We are Changing our Developer Productivity Experiment Design" (24 Feb 2026)57 developers, 800+ tasks: estimates of 18% less time (returning developers, 95% CI −38% to +9%) and 4% less time (new, CI −15% to +9%); METR writes "a speedup of -18%"METR calls its own data unreliable: developers refused to work without AI, withheld tasks, and ran several agents at once
DORA 2024 Accelerate State of DevOps (Google Cloud, 22 Oct 2024)a 25% increase in AI adoption was associated with +7.5% documentation quality, +3.4% code quality, +3.1% code review speed, and an estimated −1.5% delivery throughput and −7.2% delivery stabilitysurvey correlations, not causes; DORA also redefined the metrics that year (failed deployment recovery time moved into throughput, and a new rework rate joined stability, pp. 11–12), so it doesn't compare cleanly with earlier reports
DORA 2025 State of AI-assisted Software Development (23 Sep 2025, nearly 5,000 respondents)90% use AI at work; over 80% believe it raised their productivity; 30% have little or no trust in AI code; AI adoption now relates positively to throughput and still negatively to stability; AI is an "amplifier"self-reported and correlational; says what goes with good outcomes, not what your rollout will do

Read together: controlled single tasks show large speed-ups; real work in large codebases is mixed; developers' self-reports are unreliable; and at the organisation level, speed and stability can move in opposite directions. None of these numbers transfers to your team. That's why you measure your own.

Measuring your own rollout

A before/after on one team is the most common measurement and the weakest: everything else that changed in those weeks is mixed in. Chapter 02's Measured impact model shows how a median can improve while the tail and reverts get worse. The next step up is a comparison group: a similar team that didn't change its tools over the same weeks. The pilot's effect is how much more the pilot team changed than the comparison team, a difference-in-differences. The data below is generated from a seeded random number generator, purely to illustrate.

pilot-did.mjs
// Did the AI pilot help? Compare the pilot team's change with a similar team's change over the same weeks
// (difference-in-differences), with a bootstrap interval. ILLUSTRATIVE data from a seeded PRNG (mulberry32).
function mulberry32(a) { return () => { a |= 0; a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const rand = mulberry32(8);
const prs = (n, medianHours) => Array.from({ length: n }, () => Math.round(medianHours * Math.exp((rand() - 0.5) * 1.6) * 10) / 10);

// Hours from PR opened to merged, 8 weeks before and 8 weeks after the pilot started.
// Both teams got faster afterwards: a release freeze ended for everyone in week 9.
const data = {
  pilot: { before: prs(40, 20), after: prs(40, 13), reverts: [2, 5] },
  control: { before: prs(40, 21), after: prs(40, 16), reverts: [2, 2] },
};
const median = (xs) => { const s = [...xs].sort((a, b) => a - b); const m = s.length / 2; return s.length % 2 ? s[m | 0] : (s[m - 1] + s[m]) / 2; };
const change = (t) => median(t.after) / median(t.before) - 1; // relative change in median hours
const pct = (x) => `${x >= 0 ? "+" : ""}${(x * 100).toFixed(0)}%`;

for (const [team, d] of Object.entries(data))
  console.log(`${team.padEnd(8)} median ${median(d.before).toFixed(1)}h → ${median(d.after).toFixed(1)}h (${pct(change(d))}), reverts ${d.reverts[0]} → ${d.reverts[1]} of 40`);
const did = change(data.pilot) - change(data.control);
console.log(`\npilot's own before/after: ${pct(change(data.pilot))}   (what a launch slide would say)`);
console.log(`difference-in-differences: ${pct(did)}   (the part the control team didn't also get)`);

const resample = (xs) => xs.map(() => xs[Math.floor(rand() * xs.length)]);
const sims = Array.from({ length: 2000 }, () => {
  const p = { before: resample(data.pilot.before), after: resample(data.pilot.after) };
  const c = { before: resample(data.control.before), after: resample(data.control.after) };
  return change(p) - change(c);
}).sort((a, b) => a - b);
console.log(`bootstrap 95% interval: ${pct(sims[49])} to ${pct(sims[1949])} (2000 resamples)`);
console.log(sims[1949] < 0 ? "→ the pilot team got faster than the control team" : "→ can't tell a pilot effect from noise with 40 PRs per period");
Result
pilot    median 20.1h → 11.7h (-42%), reverts 2 → 5 of 40
control  median 20.4h → 14.8h (-28%), reverts 2 → 2 of 40

pilot's own before/after: -42%   (what a launch slide would say)
difference-in-differences: -15%   (the part the control team didn't also get)
bootstrap 95% interval: -42% to +25% (2000 resamples)
→ can't tell a pilot effect from noise with 40 PRs per period
What’s happening
  1. Each team has 40 merged PRs before and 40 after, with hours from open to merge, drawn around medians of 20 and 13 hours (pilot) and 21 and 16 (control) from a seeded mulberry32(8), so the output is the same every run. A release freeze ended for both teams in week 9, which is the kind of change that sits inside every real before/after.
  2. The launch-slide number: the pilot team's median fell from 20.1 to 11.7 hours (11.65 before rounding), −42%. On its own, that looks like a great result for AI.
  3. The comparison: the control team, with no new tools, also fell 28% (20.35 to 14.75 hours). Most of the pilot's improvement happened to everyone. The difference-in-differences is −42% − (−28%), which the code computes from unrounded values as −15%: the part the control team didn't also get.
  4. The uncertainty: resampling each group's PRs 2,000 times (the bootstrap, with the same seeded generator) gives a 95% interval of −42% to +25%. That range includes zero and even includes the pilot doing worse. With 40 PRs per period, you can't tell a pilot effect from noise.
  5. The counter-metric: the pilot team's reverts went from 2 to 5 of 40, the control team's stayed at 2. Even if the speed-up were real, you'd need to know whether it came from lighter reviews.
  6. What this demonstrates: an honest report says "the pilot team's merge time fell 42%, but a comparison team fell 28% over the same weeks; the extra 15% is within noise at this sample size, and reverts rose, so we're extending the pilot and watching reverts". That sentence survives a sceptical director's questions; "−42%" doesn't.
AdvancedWhat to measure, and with what
  • Use a framework so you don't measure one thing. SPACE (Forsgren, Storey, Maddila, Zimmermann, Houck, Butler, ACM Queue, 2021) covers satisfaction and well-being, performance, activity, communication and collaboration, and efficiency and flow, and recommends including at least one perceptual measure, such as a survey. DORA's delivery metrics cover throughput and stability.
  • A short, repeated developer survey: time spent on toil, confidence in the code being merged, how often AI output had to be redone, what they'd lose if the tool went away. Perception was wrong about speed in METR's study, but it's the only measure of friction and satisfaction.
  • Delivery data from the systems you already have: PR open-to-merge time, review time, PR size, reverts, change failure rate, incidents traced to changes.
  • Tool usage data for adoption and activity. GitHub's Copilot metrics cover active users, acceptance and agent adoption, and chapter 02 notes which surfaces they don't include.
  • A comparison: a baseline period, a comparison team or a staggered rollout (team A this month, team B next month, so each is the other's control for a while). Randomising developers, as the Cui et al. field experiments did, is the gold standard and rarely practical inside one company.
  • Enough data and time: weeks, not days, and dozens of PRs per group at least. DORA's 2026 ROI of AI-assisted Software Development report (dora.dev, page updated 22 April 2026) frames the early period of a rollout as a possible "productivity dip" to plan for, so early numbers can mislead in both directions.
AdvancedReading the survey by site

On a two-site team, an average can hide the most useful finding. Here's a three-question monthly survey summarised per site and overall. The responses are made up.

survey.mjs
// Summarising a short monthly developer survey per site, without over-reading it. Responses are MADE UP.
// Each answer is 1–5 (1 = strongly disagree, 5 = strongly agree).
const questions = {
  toil: "AI tools reduced the time I spend on repetitive work",
  confidence: "I'm confident in the AI-assisted code I merged this month",
  redo: "I often had to redo what the assistant produced",
};
const responses = [
  ...[[4, 4, 2], [5, 3, 3], [4, 4, 2], [3, 3, 3], [5, 4, 2], [4, 2, 4], [4, 3, 3]].map((a) => ({ site: "Austin", a })),
  ...[[5, 2, 4], [4, 2, 4], [5, 3, 4], [4, 2, 5]].map((a) => ({ site: "Bengaluru", a })),
];
const median = (xs) => { const s = [...xs].sort((a, b) => a - b); const m = s.length >> 1; return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; };
const agree = (xs) => `${Math.round((xs.filter((x) => x >= 4).length / xs.length) * 100)}% agree`;

for (const site of ["Austin", "Bengaluru", "all"]) {
  const rs = responses.filter((r) => site === "all" || r.site === site);
  const line = Object.keys(questions).map((q, i) => { const xs = rs.map((r) => r.a[i]); return `${q} ${median(xs)} (${agree(xs)})`; });
  console.log(`${site.padEnd(10)} n=${String(rs.length).padStart(2)}  ${line.join("  ")}${rs.length < 5 ? "  ← too few to report alone" : ""}`);
}
Result
Austin     n= 7  toil 4 (86% agree)  confidence 3 (43% agree)  redo 3 (14% agree)
Bengaluru  n= 4  toil 4.5 (100% agree)  confidence 2 (0% agree)  redo 4 (100% agree)  ← too few to report alone
all        n=11  toil 4 (91% agree)  confidence 3 (27% agree)  redo 3 (45% agree)
What’s happening
  1. Each response has a site and three answers from 1 to 5. The summary gives the median (survey answers are ordered categories, so a median is safer than a mean) and the share who agree (4 or 5).
  2. Overall, things look fine: toil reduced (median 4, 91% agree), confidence middling (3), redo middling (3). That's the line a summary slide would show.
  3. By site, Austin looks like the overall line, but Bengaluru's four people all agree they often had to redo the assistant's output, and none is confident in what they merged. The overall numbers hid that completely, because Bengaluru is 4 of 11 responses.
  4. The output flags Bengaluru as too few to report alone: with n = 4, one person changes the percentages by 25 points, and individuals are easy to identify. So you don't publish the split; you go and talk to the team.
  5. Likely causes to check, not assume: seats or MCP access provisioned later at one site, instructions that encode Austin's habits, or work types (more legacy code, more device-specific work) where assistants do worse. Each has a different fix, which is why the conversation comes before the conclusion.
AdvancedHow long must a pilot run?

The pilot model's interval was wide because 40 PRs per period is little data. Before a pilot starts, you can estimate how much data it needs by simulating it: assume a real effect, generate many pilots with realistic noise, and count how often the analysis would detect it. This uses the same spread of merge times as the pilot model, both teams speeding up 25% when a freeze ends, and a real extra 15% for the pilot team. All of it is illustrative.

pilot-size.mjs
// How many PRs does a pilot need before a real 15% speed-up shows up reliably? Simulated, seeded (mulberry32).
// Assumption (ILLUSTRATIVE): merge times are log-normal with the spread of the pilot-did model's data.
function mulberry32(a) { return () => { a |= 0; a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const rand = mulberry32(7);
const SPREAD = 1.6; // same spread as pilot-did.mjs: hours = median × e^((u − 0.5) × 1.6), u uniform
const sample = (n, median) => Array.from({ length: n }, () => Math.log(median) + (rand() - 0.5) * SPREAD); // log-hours
const mean = (xs) => xs.reduce((a, b) => a + b, 0) / xs.length;
const varOf = (xs) => { const m = mean(xs); return xs.reduce((a, x) => a + (x - m) ** 2, 0) / (xs.length - 1); };

function onePilot(n, trueEffect) { // both teams speed up 25% (the freeze ends); the pilot gets trueEffect on top
  const groups = [sample(n, 20), sample(n, 20 * 0.75 * (1 - trueEffect)), sample(n, 20), sample(n, 20 * 0.75)];
  const [pb, pa, cb, ca] = groups.map(mean);
  const did = (pa - pb) - (ca - cb); // difference-in-differences on log-hours
  const se = Math.sqrt(groups.reduce((a, g) => a + varOf(g) / g.length, 0));
  return did + 1.96 * se < 0; // the 95% interval is entirely below zero: "the pilot got faster"
}
console.log("PRs per team per period | pilots that detect a real 15% speed-up | ...a real 0% (false alarms)");
for (const n of [20, 40, 80, 160, 320]) {
  const runs = 2000;
  let hit = 0, alarm = 0;
  for (let i = 0; i < runs; i++) { if (onePilot(n, 0.15)) hit++; if (onePilot(n, 0)) alarm++; }
  console.log(`${String(n).padStart(23)} | ${`${((hit / runs) * 100).toFixed(0)}%`.padStart(39)} | ${`${((alarm / runs) * 100).toFixed(1)}%`.padStart(27)}`);
}
Result
PRs per team per period | pilots that detect a real 15% speed-up | ...a real 0% (false alarms)
                     20 |                                     13% |                        2.8%
                     40 |                                     22% |                        2.5%
                     80 |                                     34% |                        2.9%
                    160 |                                     62% |                        2.6%
                    320 |                                     87% |                        2.7%
What’s happening
  1. Merge times are drawn as log-hours: log(median) plus uniform noise of width 1.6, the same spread pilot-did.mjs used. Working on logs turns "15% faster" into a fixed difference, so the difference-in-differences is a difference of means.
  2. Each simulated pilot has four groups: pilot before and after, control before and after. Both teams' "after" medians are 25% lower (the freeze ending); the pilot's is a further 15% lower in the first column and not lower at all in the second.
  3. A pilot "detects" the speed-up when the 95% interval for the difference-in-differences (estimate ± 1.96 standard errors) lies entirely below zero.
  4. With 40 PRs per team per period, a real 15% speed-up is detected only 22% of the time. With 160 it's 62%, with 320 it's 87%. A typical team merges well under 40 PRs a week, so a two-week pilot on one team usually can't show a 15% effect even when it's real.
  5. The last column checks the method: with no real effect, it "detects" one about 2.5–3% of the time, which is what a one-sided 95% interval should give. So the problem with small pilots isn't false alarms; it's that "we saw no significant difference" is the expected result even when the tool helps.
  6. How to use it: pick the smallest effect worth knowing about, simulate with your own team's spread, and plan the pilot length or the number of teams from that. Or decide in advance that the pilot is about adoption, friction and risks, and say so, rather than over-reading a speed number.
AdvancedA monthly report that survives questions

A short, repeated report beats a launch announcement. This template is illustrative; the numbers would be your own:

docs/ai-report-template.md
# AI-assisted development: <month>

## Adoption (context, not success)
Active users: <n> of <seats> (Austin <n>, Bengaluru <n>). Agent PRs opened: <n>.

## Outcomes, with a comparison
| Metric | Pilot teams | Comparison teams | Note |
|---|---|---|---|
| PR open → merge, median / p90 | | | |
| Review time, median | | | |
| Reverts per 100 PRs | | | counter-metric |
| Change failure rate | | | counter-metric |

## What people said (survey, n = <n>)
Toil: <…>. Confidence in merged code: <…>. Top frustration: <…>.

## What we can't conclude yet
<e.g. "40 PRs per period can't show a 15% effect; see the pilot-size estimate".>

## Changes we're making
<one or two, each with an owner>
What’s happening
  1. Adoption first, labelled "context, not success", split by site, so a gap between Austin and Bengaluru shows up as a provisioning or training issue rather than a mystery.
  2. Outcomes always next to a comparison, with medians and p90s, and two counter-metrics in the same table, so nobody can quote the speed row without the stability rows.
  3. The survey carries what delivery data can't: toil, confidence and frustrations. Quote the count of respondents.
  4. "What we can't conclude yet" is the section that earns trust. Saying it in writing every month is what stops "−42%" from escaping into a slide.
  5. Changes with owners turn the report into action: tighten the size limit, add a prompt file, fix seat provisioning at one site.
Read the dashboard

After a two-month Copilot rollout, a dashboard shows: acceptance rate 31%; "AI-written lines" 38% of all added lines; median PR open-to-merge time down from 18 to 12 hours; p90 up from 60 to 75 hours; reverts up from 3 to 7 per month. The director asks, "So it's working?" What do you answer?

Show answer

"It's being used heavily, and typical PRs merge faster, but the slow tail and reverts both got worse, and there's no comparison yet. I can't say it's working; I can say what to look at next."

What’s happening
  1. Acceptance rate and "AI-written lines" are activity. They say the tool is busy. "AI-written lines" is especially weak: accepted text gets edited and deleted, and the share depends on how the tool counts.
  2. The median dropping from 18 to 12 hours is an outcome, and good news for typical PRs, but it's a before/after with no comparison group; anything else that changed in those two months is mixed in.
  3. The p90 rising from 60 to 75 hours says the slowest PRs got slower, perhaps the big agent-generated ones waiting for review. Reverts more than doubling is the counter-metric firing: faster merges may mean lighter review.
  4. Next steps: compare with a team that hasn't rolled out yet, look at which PRs got reverted (AI-assisted or not, which areas), check PR sizes, and run the survey.
  5. The interview point: refusing to say "yes, 33% faster" isn't being negative. It's giving the director a number they can defend.

#Helping a distributed team use AI tools well

Added

On a team split between the US and India, AI assistants can make the time-zone gap smaller or bigger. Smaller, when everyone's assistant knows the same project rules, runs the same well-tested prompts and reaches the same tools, so a change started in Austin and finished in Bengaluru follows one set of conventions. Bigger, when each site, or each person, builds a private setup: different instructions, different MCP servers, different habits, and reviews across the overlap hour turn into arguments about style the assistants introduced. The Tech Lead site covers the people side of distributed teams (Working across US and India time zones); this topic covers the shared AI setup.

The mental model is the team's AI setup is code. Instructions, prompt files and MCP server configuration live in the repository; they're versioned, reviewed, owned and checked in CI; every developer in both locations gets the same version by pulling main. A developer in Bengaluru doesn't need to wait for Austin to wake up to learn "how we add an endpoint": the prompt file is the answer, and it was reviewed by both sites.

Repository: shared AI setup copilot-instructions.md AGENTS.md prompts/*.prompt.md .vscode/mcp.json CODEOWNERS + CI check Austin developers editor + assistant Bengaluru developers editor + assistant Copilot cloud agent instructions + AGENTS.md MCP: repo Copilot settings git pull Device-lab MCP server hosted by the team each person's own login Changes to the setup pull request, reviewed by a lead at each site Same instructions, same prompts, same tools in both time zones; changed only by review.
A shared AI setup for a two-site team. Editors' assistants and the cloud agent read the same instruction files from the repository (the cloud agent's MCP servers are configured in the repository's settings instead); the team's MCP server is shared, but each person signs in as themselves.

What goes into the shared setup:

PieceWhat it gives the teamWatch out for
.github/copilot-instructions.md, AGENTS.mdthe same project rules for every assistant: build and test commands, conventions, "never" rulescontradictions between files; rules nobody can check (chapter 02)
Path-specific instructionsAPI rules when touching the API, web UI rules when touching the UInot every surface reads them
Prompt files (.github/prompts/)the team's best prompts for recurring jobs: add an endpoint, write a handoff, triage a device logprompts without a description or a tools list
.vscode/mcp.jsonthe same approved MCP servers for everyone, secrets via inputsliteral tokens in the file; unpinned packages; unapproved hosts; the cloud agent doesn't read it (its MCP servers are set under the repository's Settings > Copilot > MCP servers, per GitHub's "Configure MCP servers for your repository", read 27 October 2026)
A team-hosted MCP serverdevice lab, tickets or docs available to every assistant, with per-person authshared service accounts that hide who did what
CODEOWNERS and a CI checkchanges are reviewed by leads at both sites; mistakes fail the buildownership only at one site, so changes wait a day

Here is the CI check, run against a deliberately messy example repository. The rules are this team's own, written for illustration; they aren't GitHub or VS Code requirements.

ai-config-check.mjs
// A CI check for the team's shared AI setup: instruction files, prompt files, MCP config, owners.
// The repository below is MADE UP; the rules are the team's own (illustrative), not GitHub's or VS Code's.
const repo = {
  ".github/copilot-instructions.md": "# Device admin\n- Test: `npm test`\n- Never retry device commands without an idempotency key.\n",
  "AGENTS.md": "# Agents\n- Test: `pnpm test`\n- Ask before running database migrations.\n",
  ".github/prompts/add-endpoint.prompt.md": "---\ndescription: Add a REST endpoint\ntools: ['search/codebase', 'edit/editFiles']\n---\nAdd ${input:endpoint}...",
  ".github/prompts/handoff.prompt.md": "---\nname: handoff\n---\nSummarise today's work for the other site...",
  ".vscode/mcp.json": JSON.stringify({
    inputs: [{ type: "promptString", id: "lab-token", description: "Device lab token", password: true }],
    servers: {
      "device-lab": { type: "http", url: "https://mcp.devicelab.example.com/mcp", headers: { Authorization: "Bearer ${input:lab-token}" } },
      "browser": { type: "stdio", command: "npx", args: ["-y", "@playwright/mcp@latest"] },
      "tickets": { type: "http", url: "https://tickets-mcp.example.net/mcp", headers: { Authorization: "Bearer 9f2c41d7e0" } },
    },
  }),
  ".github/CODEOWNERS": "* @device-admin/devs\n/.github/copilot-instructions.md @device-admin/tech-leads\n/AGENTS.md @device-admin/tech-leads\n",
};
const APPROVED_HOSTS = ["mcp.devicelab.example.com", "mcp.tickets.example.com"];
const problems = [];
const say = (file, msg) => problems.push(`${file}: ${msg}`);

// 1. One test command, said the same way everywhere.
const testCmd = (f) => repo[f]?.match(/^- Test: `([^`]+)`/m)?.[1];
const cmds = [".github/copilot-instructions.md", "AGENTS.md"].map((f) => [f, testCmd(f)]).filter(([, c]) => c);
if (new Set(cmds.map(([, c]) => c)).size > 1) say("instructions", `different test commands: ${cmds.map(([f, c]) => `${f} says ${c}`).join(", ")}`);
// 2. Prompt files say what they're for and which tools they may use.
for (const f of Object.keys(repo).filter((f) => f.endsWith(".prompt.md"))) {
  const front = repo[f].match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
  if (!/^description:/m.test(front)) say(f, "no description (people browsing the prompts can't tell what it does)");
  if (!/^tools:/m.test(front)) say(f, "no tools list (it runs with whatever tools the chat session has enabled)");
}
// 3. MCP servers: no literal secrets, pinned packages, approved hosts.
const mcp = JSON.parse(repo[".vscode/mcp.json"]);
for (const [name, s] of Object.entries(mcp.servers)) {
  for (const [h, v] of Object.entries(s.headers ?? {})) if (!/\$\{input:[\w-]+\}/.test(v)) say(".vscode/mcp.json", `${name}: ${h} header holds a literal secret; use an input`);
  const pkg = s.command === "npx" && s.args.find((a) => !a.startsWith("-"));
  if (pkg && !/@\d+\.\d+\.\d+$/.test(pkg)) say(".vscode/mcp.json", `${name}: ${pkg} isn't pinned to an exact version`);
  if (s.type === "http" && !APPROVED_HOSTS.includes(new URL(s.url).host)) say(".vscode/mcp.json", `${name}: ${new URL(s.url).host} is not an approved MCP host`);
}
// 4. Changes to any of these files need a tech lead.
const owned = repo[".github/CODEOWNERS"].split("\n").filter((l) => l.includes("@device-admin/tech-leads")).map((l) => l.split(" ")[0].replace(/^\//, ""));
for (const f of Object.keys(repo).filter((f) => /(instructions|AGENTS|prompt|mcp\.json)/.test(f)))
  if (!owned.some((o) => f === o || (o.endsWith("/") && f.startsWith(o)))) say(f, "not owned by @device-admin/tech-leads in CODEOWNERS");

console.log(problems.map((p) => `✗ ${p}`).join("\n"));
console.log(`${problems.length} problem(s)`); // a CI job would exit 1 here so the pull request can't merge
Result
✗ instructions: different test commands: .github/copilot-instructions.md says npm test, AGENTS.md says pnpm test
✗ .github/prompts/handoff.prompt.md: no description (people browsing the prompts can't tell what it does)
✗ .github/prompts/handoff.prompt.md: no tools list (it runs with whatever tools the chat session has enabled)
✗ .vscode/mcp.json: browser: @playwright/mcp@latest isn't pinned to an exact version
✗ .vscode/mcp.json: tickets: Authorization header holds a literal secret; use an input
✗ .vscode/mcp.json: tickets: tickets-mcp.example.net is not an approved MCP host
✗ .github/prompts/add-endpoint.prompt.md: not owned by @device-admin/tech-leads in CODEOWNERS
✗ .github/prompts/handoff.prompt.md: not owned by @device-admin/tech-leads in CODEOWNERS
✗ .vscode/mcp.json: not owned by @device-admin/tech-leads in CODEOWNERS
9 problem(s)
What’s happening
  1. One test command: .github/copilot-instructions.md says npm test, AGENTS.md says pnpm test. Copilot reads one, other agents read the other, so the two sites' assistants verify changes differently. The check compares the - Test: lines and fails on any difference. Contradictory instructions are the most common shared-setup bug, and the hardest to notice by reading.
  2. Prompt files need a description and a tools list: handoff.prompt.md has neither. Without a description, a developer browsing prompts at 9 pm in Bengaluru can't tell what it does; without a tools list, it runs with whatever tools the chat session has enabled, which for a summarising prompt is more than it needs.
  3. MCP config: the browser server runs @playwright/mcp@latest through npx, so each person might get a different version, and a new release can change tool descriptions without review; pin an exact version. The tickets server has a literal bearer token in the file (anyone who can read the repository has it) and points at a host that isn't approved. The device-lab entry is the good pattern: an approved host and ${input:lab-token}, which VS Code prompts for and stores securely.
  4. Ownership: CODEOWNERS gives the tech leads the two instruction files, but not the prompt files or .vscode/mcp.json. All three change what every assistant does, so all three need a lead's review, ideally with owners at both sites so a PR never waits a full day.
  5. 9 problems, and the comment notes that in CI the job would exit 1 so the PR can't merge. Run it on every PR that touches these paths and the shared setup stays shared.
AdvancedPrompt files for a two-site team

Some of the most useful prompt files on a distributed team aren't about code generation at all. An end-of-day handoff is written by one site for the other, and an assistant that reads the day's commits, open PRs and failing checks can draft it consistently. This one follows the VS Code prompt file format chapter 02 describes; it wasn't run in VS Code for this page, and the tool names are the ones chapter 02's example uses:

.github/prompts/handoff.prompt.md
---
description: "Draft today's handoff note for the other site: what changed, what's blocked, what needs a decision"
name: handoff
agent: ask
tools: ['search/codebase', 'read/readFile']
---
Draft a handoff note for the team in the other time zone, from my work today on ${input:branch:current branch}.

Use only what you can see in the repository and the files I attach. Don't guess at status.

Sections, in this order, each at most five bullets:
1. Done: merged or ready for review, with PR links.
2. In progress: what state it's in and the next concrete step.
3. Blocked or needs a decision: the question, the options, and who can answer.
4. Watch out: anything that could break, such as a flaky test, a device in the lab left in a strange state, or a migration not yet run.

Plain, literal English. No idioms, no "ASAP": give dates and times in both IST and US Central.
What’s happening
  1. agent: ask and a two-tool list make it read-only: a handoff should never edit code.
  2. "Use only what you can see… Don't guess at status" counters the main failure of AI summaries: confident statements about things the model can't know, such as whether a PR was approved.
  3. The four fixed sections mirror the async-handoff structure on the Tech Lead site: done, in progress, blocked, risks. Fixed sections make notes from different people comparable.
  4. "Plain, literal English… both IST and US Central" is about the reader, not the model: idioms and single-time-zone times are the two things that most often get misread across sites.
  5. The person still reads and edits the draft before posting. The prompt saves the gathering; it doesn't sign the note.
AdvancedPractices that spread good use
  • A team MCP server instead of personal scripts. If three people in Bengaluru each wrote their own script to query lab devices, make one shared, reviewed MCP server (chapter 07's server is the shape) with per-person authentication, so the audit log says who did what.
  • Rotate a "prompt clinic" across the overlap window. Thirty minutes a month, alternating which site presents: one prompt or instruction that worked, one that failed and why. Record it for whoever couldn't attend.
  • Share sessions, not just results. When an assistant solved something hard, a short write-up of the conversation, what context was given and what was corrected, teaches more than the final diff.
  • Same tools, same access, both sites. If seats, approved servers or lab access arrive months later at one site, the "AI-assisted" gap becomes a two-tier team. Provision together.
  • Pair across sites on agent work. An agent PR started in Austin's afternoon can be reviewed in Bengaluru's morning; agree who owns it overnight so it doesn't sit unowned.
  • Keep the language plain. Instructions and prompts written in short, literal sentences work better for the model and for colleagues reading in a second language.
AdvancedOnboarding someone into the shared setup

A new developer, wherever they sit, should have the team's AI setup working in their first week, the same way they have the build working. A checklist that fits the Tech Lead site's onboarding plan:

  • Day 1: company Copilot seat assigned; editor signed in with the work account; the AI guideline document read, with the data rules explained by a person, not just linked.
  • Day 2: the repository's MCP servers start (the team device-lab server prompts for their own token); they run one prompt file end to end, for example the endpoint prompt on a practice ticket.
  • Week 1: their first AI-assisted PR uses the template's "what I verified" section, and the reviewer gives feedback on the verification as well as the code.
  • Week 2–4: they bring one prompt or instruction improvement as a PR to the shared files. Contributing to the setup is how people stop treating it as someone else's rules.
  • Throughout: a buddy at their own site and one at the other, so questions about the setup don't wait for the overlap hour.
AdvancedAgents across the time-zone gap

It's tempting to say "the cloud agent works overnight, so the gap disappears". Some of that is true: a task assigned at the end of the Bengaluru day can have a draft PR waiting for Austin's morning. But the gap moves to review. Every agent PR still needs a human owner, a reviewer who isn't the requester, and workflow approval before CI runs, so plan it like any handoff:

  • Assign agent tasks with an owner at the receiving site, who reviews it in their morning and either takes it forward or closes it.
  • Write the task as a spec, with acceptance criteria and the tests to add (chapter 03's Spec first), because nobody will be awake to answer the agent's questions.
  • Keep agent tasks small: an overnight 1,200-line PR is a morning lost to review. The size limit is the same as for people.
  • Count them in the handoff note: "two agent PRs waiting for review, owners X and Y" belongs in the same structure as human work.
AdvancedBehavioural version: distributed teams
Fix the setup

A Bengaluru developer reports: "My assistant keeps running pnpm test, but Austin's runs npm test, and the lab MCP server stopped working after I pulled main." The repository has AGENTS.md (pnpm), .github/copilot-instructions.md (npm), and .vscode/mcp.json pointing the lab server at npx -y @team/lab-mcp@latest. What's happening, and what do you change?

Show answer

Two instruction files disagree, and an unpinned MCP server package updated under everyone. Make one test command canonical, pin the server version, and add the CI check.

What’s happening
  1. Different assistants read different files: one follows AGENTS.md (pnpm test), another .github/copilot-instructions.md (npm test). Neither developer is wrong; the repository contradicts itself.
  2. Fix: decide the real command, put it in both files (or have one file point to the other), and let the CI check compare them on every PR.
  3. @team/lab-mcp@latest means every launch can fetch a new version. Someone published a release, and everyone who restarted their editor got it; the Bengaluru developer restarted first.
  4. Fix: pin an exact version in .vscode/mcp.json, and upgrade it through a PR that a lead reviews, the same as any dependency.
  5. Add CODEOWNERS for AGENTS.md, the instruction files, prompt files and .vscode/mcp.json, with leads at both sites, so the next change is reviewed during someone's working day.

Written fresh for a role that asks for GitHub Copilot and MCP experience. MCP examples were run with @modelcontextprotocol/sdk 1.32.2; Copilot facts come from GitHub's documentation, cited with dates.

RohitDownloads · All chapters

Sync progress across devices

Type the same private phrase on your Mac and your phone, and your Learned ticks follow you between them — on every study site.

The phrase never leaves this device: only a fingerprint of it is sent, and the server stores a fingerprint of that. Anyone who knows the phrase could see or change your ticks, so pick something you don’t use elsewhere.

Sync is on in this browser.

Sync ID

This ID must be the same on every device. If another device shows a different one, its phrase is different (capital letters count): tap “Turn off here” on it and type the phrase again exactly.