{"id":16732,"date":"2026-09-24T00:00:00","date_gmt":"2026-09-24T07:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/ise\/?p=16732"},"modified":"2026-09-24T08:40:04","modified_gmt":"2026-09-24T15:40:04","slug":"documentation-is-the-new-source-code","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/ise\/documentation-is-the-new-source-code\/","title":{"rendered":"Documentation Is the New Source Code"},"content":{"rendered":"<h2>Why writing things down is the highest-leverage thing you can do when building with AI and HVE tools<\/h2>\n<p>For most of my career, &#8220;good documentation&#8221; was a virtue \u2014 somewhere between\nwriting tests and updating your status report. Nice to have. Often skipped.\nQuietly resented in code review.<\/p>\n<p>That calculation has flipped. When your day-to-day involves Copilot, Claude,\nCursor, agentic CLIs, MCP servers, and a dozen &#8220;high-velocity engineering&#8221;\n(HVE) tools that read your repo before they write a single line, <strong>the\ndocumentation you write is the system prompt for every future change<\/strong>.<\/p>\n<p>Skip it and the AI guesses. Write it and the AI executes.<\/p>\n<p>This post explains why we invested early in documentation \u2014 scoped instruction\nfiles, skills, and technical guides \u2014 and the concrete payoff we&#8217;ve seen.<\/p>\n<h2>1. The shift: docs are no longer just for humans<\/h2>\n<p>Last month I asked an agent to add a new endpoint to our service. With no\nconventions written down, it invented its own error-handling pattern, its\nown logger, and a folder structure we&#8217;d abandoned some time ago. Same prompt,\nsame agent, a week later \u2014 after we&#8217;d written the conventions into\n<code>copilot-instructions.md<\/code>: one PR, green CI, merged the same afternoon.\nNothing about the model changed. Only the input did.<\/p>\n<p>That gap is what this section is about. Four things shifted at roughly the\nsame time to make this flip possible:<\/p>\n<ol>\n<li><strong>Coding assistants got context-aware.<\/strong> Tools like <a href=\"https:\/\/azure.microsoft.com\/en-us\/products\/github\/copilot\">GitHub Copilot<\/a>,\n<a href=\"https:\/\/claude.com\/product\/claude-code\">Claude Code (Anthropic)<\/a> and <a href=\"https:\/\/cursor.com\">Cursor (Cursor AI)<\/a> now automatically read <code>README.md<\/code>, <code>AGENTS.md<\/code>,\n<code>.github\/copilot-instructions.md<\/code>, <code>*.instructions.md<\/code>, <code>SKILL.md<\/code>, MCP\nserver descriptions, ADRs \u2014 anything they can find. The model&#8217;s behavior\nis a pure function of what lands in its context window, and docs are now\nthe highest-signal thing you can put there. Think of the context window\nas the new compiler input: garbage in, plausible-looking garbage out.<\/li>\n<li><strong>Agents started running multi-step tasks.<\/strong> A single prompt now triggers\ndozens \u2014 sometimes a few hundred \u2014 file reads, edits and tool calls in\none turn. At that scale the model has to <em>infer conventions<\/em> at every\nstep, and every wrong inference compounds into the next.<\/li>\n<li><strong>Teams adopted &#8220;high-velocity&#8221; workflows.<\/strong> Short-lived branches, daily\nPRs, agent-authored changes, async review. Hallway knowledge no longer\nhas time to spread by osmosis \u2014 by the time you&#8217;d have explained the\nconvention to a teammate, three agent-authored PRs have already shipped\nwithout it.<\/li>\n<li><strong>Models became interchangeable.<\/strong> You&#8217;re going to swap your assistant\nevery six months \u2014 GPT (OpenAI) to Claude (Anthropic) to Gemini (Google) to whatever ships next.\nPrompt-engineering tricks tuned to one model don&#8217;t transfer. A good\n<code>copilot-instructions.md<\/code> does. Docs are the only artifact in your repo\nthat survives the model swap.<\/li>\n<\/ol>\n<p>The combined effect: <strong>whatever isn&#8217;t written down gets re-invented, badly,\non every task.<\/strong> The model picks the most popular pattern from its training\ndata, not the one your team actually uses.<\/p>\n<p>Documentation used to <em>compete<\/em> with code for engineering time. Now it\n<em>compiles into<\/em> it \u2014 every doc you write becomes input to every change the\nAI makes.<\/p>\n<h2>2. What &#8220;documentation as system prompt&#8221; looks like in practice<\/h2>\n<p>Here&#8217;s the layered setup we use in our repo. Every layer is read by humans\n<em>and<\/em> by AI tools \u2014 often automatically.<\/p>\n<p>The guiding principle is simple: <strong>things the model needs on every task go\nin the system prompt; things it occasionally needs live one grep away.<\/strong>\nCheap-to-load context is always-on; expensive-to-load context is\non-demand.<\/p>\n<table>\n<thead>\n<tr>\n<th>#<\/th>\n<th>Layer<\/th>\n<th>File(s)<\/th>\n<th>When the model reads it<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>1<\/td>\n<td>Repo-wide instructions<\/td>\n<td><code>.github\/copilot-instructions.md<\/code><\/td>\n<td>Every task<\/td>\n<\/tr>\n<tr>\n<td>2<\/td>\n<td>Scoped instructions<\/td>\n<td><code>.github\/instructions\/*.instructions.md<\/code><\/td>\n<td>Edits matching the <code>applyTo<\/code> glob<\/td>\n<\/tr>\n<tr>\n<td>3<\/td>\n<td>Skills (workflow recipes)<\/td>\n<td><code>.github\/skills\/&lt;name&gt;\/SKILL.md<\/code><\/td>\n<td>When the workflow is invoked<\/td>\n<\/tr>\n<tr>\n<td>4<\/td>\n<td>ADRs<\/td>\n<td><code>ADRs\/NNN-*.md<\/code><\/td>\n<td>When linked or searched<\/td>\n<\/tr>\n<tr>\n<td>5<\/td>\n<td>System, feature &amp; how-to docs<\/td>\n<td><code>docs\/**<\/code> (architecture, features, guides, runbooks)<\/td>\n<td>Deep lookups, on demand<\/td>\n<\/tr>\n<tr>\n<td>6<\/td>\n<td>Code-adjacent docs<\/td>\n<td>docstrings, schema comments<\/td>\n<td>Every time the file is opened<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>Layer 1 \u2014 Repo-wide AI instructions<\/h3>\n<p><code>.github\/copilot-instructions.md<\/code> is\nthe first file most AI tools open. Ours is short, opinionated, and\ninformation-dense:<\/p>\n<ul>\n<li>Architecture overview (request flow in one line, key directories with one\nsentence each)<\/li>\n<li>Configuration system (where YAML lives, how it&#8217;s loaded)<\/li>\n<li>Environment variables (table with required \/ default \/ description)<\/li>\n<li>Developer workflows (exact commands to run backend, frontend, tests)<\/li>\n<li>CI gates (so the model doesn&#8217;t propose changes that will fail lint)<\/li>\n<li>Git conventions (branch naming, commit prefixes, PR template)<\/li>\n<li>Cross-cutting conventions (error handling, parallelism, logging,\ntelemetry)<\/li>\n<li>Pointers to ADRs and <code>docs\/<\/code><\/li>\n<\/ul>\n<p>Notice what isn&#8217;t in it: tutorials, motivation, history. Those belong in\ndeeper docs. The top-level file is for <strong>rules the model must follow on\nevery task<\/strong>.<\/p>\n<p>A redacted excerpt to make it concrete \u2014 the top of ours opens with the\nrequest flow and an env-var table, in that order:<\/p>\n<pre><code class=\"language-markdown\">## Request flow\r\nController \u2192 RequestContext \u2192 SingleProcessPipeline.run \/\r\nOrchestrator.run() \u2192 result\r\n\r\n## Environment variables\r\n| Name           | Required | Default | Description              |\r\n|----------------|----------|---------|--------------------------|\r\n| OPENAI_API_KEY | yes      | \u2014       | Auth for the model calls |\r\n| DB_ENDPOINT    | yes      | \u2014       | Cache + state storage    |\r\n| LOG_LEVEL      | no       | INFO    | Standard Python levels   |<\/code><\/pre>\n<p>That&#8217;s the first thing the agent sees on every task. Everything else in\nthe file is rules and pointers.<\/p>\n<h3>Layer 2 \u2014 Scoped instruction files<\/h3>\n<p>For larger codebases, a single file isn&#8217;t enough. We use VS Code&#8217;s\n<code>applyTo<\/code> glob pattern to scope instructions:<\/p>\n<pre><code class=\"language-text\">.github\/instructions\/backend.instructions.md   \u2192  applyTo: backend\/**\r\n.github\/instructions\/frontend.instructions.md  \u2192  applyTo: frontend\/**<\/code><\/pre>\n<p>The agent only sees the backend rules when editing Python, only sees the\nfrontend rules when editing TypeScript. This keeps each context window lean\nand prevents cross-contamination (no React advice leaking into a FastAPI\ncontroller).<\/p>\n<p>The trade-off is real: scoped files only fire when the agent edits a\nmatching path. Anything truly cross-cutting \u2014 logging, error handling,\ntelemetry, secret handling \u2014 still belongs in the repo-wide file.\nOver-scope and you&#8217;ll watch the model cheerfully violate your logging\nconventions on every cross-cutting change.<\/p>\n<h3>Layer 3 \u2014 Skills (workflow recipes)<\/h3>\n<p><code>.github\/skills\/&lt;name&gt;\/SKILL.md<\/code> files are the equivalent of\n&#8220;runbooks for the model.&#8221; Each one says: <em>here is a task you might be asked\nto do; here&#8217;s the exact procedure<\/em>. We have an internal\n<code>code-review-excellence<\/code> skill, and we lean on a handful of off-the-shelf\nones (<code>create-pull-request<\/code>, <code>address-pr-comments<\/code>,\n<code>summarize-github-issue-pr-notification<\/code>).<\/p>\n<p>At their simplest they look like this:<\/p>\n<pre><code class=\"language-markdown\">---\r\nname: create-pull-request\r\ndescription: Open a PR using our template, labels, and reviewer rules.\r\n---\r\n\r\n1. Ensure the branch name matches `feat\/`, `fix\/`, or `chore\/` prefix.\r\n2. Fill the PR body using `.github\/PULL_REQUEST_TEMPLATE.md`.\r\n3. Add the area label (`area:backend`, `area:frontend`, ...).\r\n4. Request review from code owners for the changed paths.<\/code><\/pre>\n<p>The win: instead of the model improvising &#8220;how to open a PR in this repo,&#8221;\nit follows the SKILL.md verbatim \u2014 including our PR template, label rules,\nand reviewer assignment.<\/p>\n<h3>Layer 4 \u2014 ADRs<\/h3>\n<p><code>ADRs\/<\/code> folder contains our numbered decision records. Each one\ncaptures <strong>why<\/strong> a choice was made \u2014 how we retrieve context for the\nmodel, how we orchestrate agents, how we version regenerated outputs,\nand so on.<\/p>\n<p>ADRs do something other docs can&#8217;t: they prevent the AI (and the next\nengineer) from <em>undoing<\/em> a deliberate decision. They work because they\nare <strong>durable, dated, and authoritative<\/strong> \u2014 the model treats a numbered\ndecision record as a stronger signal than a comment, a Slack quote, or a\nhalf-remembered convention.<\/p>\n<h3>Layer 5 \u2014 System, feature, and how-to documentation (<code>docs\/<\/code>)<\/h3>\n<p><code>docs\/<\/code> is the home for everything too big for the system\nprompt but big enough that the model (or a human) will search for it. We\nsplit it into three top-level areas:<\/p>\n<pre><code class=\"language-text\">docs\/\r\n\u251c\u2500\u2500 system\/        \u2190 how the whole thing fits together\r\n\u2502   \u251c\u2500\u2500 architecture.md\r\n\u2502   \u251c\u2500\u2500 configuration.md\r\n\u2502   \u251c\u2500\u2500 cosmos-cache-queries.md\r\n\u2502   \u251c\u2500\u2500 observability.md\r\n\u2502   \u2514\u2500\u2500 onboarding.md\r\n\u251c\u2500\u2500 features\/      \u2190 one folder per shipped capability\r\n\u2502   \u251c\u2500\u2500 report-generation\/\r\n\u2502   \u2502   \u251c\u2500\u2500 overview.md\r\n\u2502   \u2502   \u251c\u2500\u2500 pipeline.md\r\n\u2502   \u2502   \u2514\u2500\u2500 prompts.md\r\n\u2502   \u251c\u2500\u2500 table-extraction\/\r\n\u2502   \u2502   \u251c\u2500\u2500 overview.md\r\n\u2502   \u2502   \u2514\u2500\u2500 strategies.md\r\n\u2502   \u2514\u2500\u2500 regenerate-section\/\r\n\u2502       \u251c\u2500\u2500 overview.md\r\n\u2502       \u2514\u2500\u2500 versioning.md\r\n\u2514\u2500\u2500 guides\/        \u2190 how to use or implement something\r\n    \u251c\u2500\u2500 add-a-new-section-type.md\r\n    \u251c\u2500\u2500 write-a-new-extraction-strategy.md\r\n    \u251c\u2500\u2500 plug-in-a-new-llm-provider.md\r\n    \u2514\u2500\u2500 run-the-eval-harness-locally.md<\/code><\/pre>\n<p><code>docs\/system\/<\/code> answers questions like <em>&#8220;how does configuration load?&#8221;<\/em> or\n<em>&#8220;what does observability look like end-to-end?&#8221;<\/em> \u2014 cross-cutting context\nthe model needs when it&#8217;s reasoning about anything non-trivial.<\/p>\n<p><code>docs\/features\/<\/code> answers questions like <em>&#8220;how does\nregenerate-section actually work?&#8221;<\/em> \u2014 one folder per shipped feature, each\nwith an <code>overview.md<\/code> (what it does, why it exists, key invariants) and as\nmany companion files as the feature deserves. When a ticket lands that\ntouches a feature, the agent grep-finds the right folder and pulls in the\nrelevant context instead of guessing from code.<\/p>\n<p><code>docs\/guides\/<\/code> answers questions like <em>&#8220;how do I add a new section type?&#8221;<\/em>\nor <em>&#8220;how do I plug in a new LLM provider?&#8221;<\/em> \u2014 task-oriented walkthroughs\nthat take an engineer (or an agent) from intent to working code without\nre-deriving the design. These pay off twice: humans use them to onboard\nonto unfamiliar parts of the system, and agents follow them step-by-step\nwhen asked to implement the same kind of change.<\/p>\n<p>These docs are too long for the always-on system prompt, but the model\ncan \u2014 and does \u2014 open them on demand. They also serve their original\npurpose: onboarding humans, and giving new agent sessions a fast way to\nget oriented.<\/p>\n<h3>Layer 6 \u2014 Code-adjacent docs<\/h3>\n<p>Docstrings, type hints, config-schema YAML comments. Too small to be a\ndocument, too important to lose. They&#8217;re cheap, local, always co-located\nwith the thing they describe, and the model reads them every time it\nopens the file.<\/p>\n<hr \/>\n<p>Each layer is cheap on its own. Stacked, they give an agent a complete\npicture of how this codebase wants to be changed \u2014 without any single\nfile getting bloated, and without forcing the model to guess.<\/p>\n<hr \/>\n<h2>3. Where the leverage shows up<\/h2>\n<p>With those layers in place, the payoff shows up in six places \u2014 most of\nthem places we didn&#8217;t expect.<\/p>\n<h3>a. AI is a force multiplier \u2014 <em>of whatever you&#8217;ve written down<\/em><\/h3>\n<p>A well-documented repo means an agent can implement a feature in one shot.\nA poorly-documented repo means the same agent produces plausible-looking\ncode that violates the conventions, fails CI, and needs several rounds of\nhuman correction. The model isn&#8217;t worse \u2014 the input is.<\/p>\n<p>The shape of the payoff is concrete: convention-related review comments\n(&#8220;we don&#8217;t structure errors that way,&#8221; &#8220;use the shared logger,&#8221; &#8220;wrong\nfolder&#8221;) drop sharply once the conventions live in\n<code>copilot-instructions.md<\/code>. The hour you spent writing them down pays back\non every PR after.<\/p>\n<h3>b. Onboarding collapses \u2014 at least the mechanical half<\/h3>\n<p>A new engineer (or a new agent session) used to need a buddy, a wiki\npage, and three meetings. Now they read <code>onboarding.md<\/code>, point their\nassistant at the repo, and ship a real PR the same afternoon. The agent\nacts as the pair programmer who <em>already<\/em> knows the codebase.<\/p>\n<p>The honest caveat: docs collapse the <em>mechanical<\/em> part of onboarding \u2014\nrunning the stack, understanding the layout, making a first safe change.\nThe squishy part \u2014 who owns what, why a decision felt the way it did,\nwhere the bodies are buried \u2014 still needs a human. Don&#8217;t promise more\nthan the docs can deliver.<\/p>\n<h3>c. Decisions stop eroding \u2014 even across model swaps<\/h3>\n<p>Without ADRs, every six months someone re-litigates a settled choice.\nWith ADRs, the conversation is two minutes long: &#8220;Did you read ADR 007?&#8221;<\/p>\n<p>The AI-specific version is sharper: the model has no memory between\nsessions. Without an ADR, it will confidently re-propose the rejected\npattern \u2014 cheerfully asking <em>&#8220;have you considered\u2026?&#8221;<\/em> \u2014 every single time,\nand a different model next quarter will do it again, in a different\nvoice. The ADR is the model&#8217;s memory. Once it&#8217;s there, the agent cites\nit in its own suggestions and the loop closes.<\/p>\n<h3>d. Async velocity becomes safe<\/h3>\n<p>We increasingly hand whole tickets to agents on a Friday and merge the\nPR on a Monday, with no human in the loop on the convention-following\nparts. That only works when the conventions are written down \u2014 when\nthe agent can self-serve branch naming, commit style, label rules and\nPR layout without asking.<\/p>\n<p>Without the docs, async agent-authored work is fast and wrong. With\nthem, it&#8217;s fast and right.<\/p>\n<h3>e. Tools amplify good docs across the stack<\/h3>\n<p>MCP servers, search subagents, code-review skills \u2014 they all read your\ndocs. The clearest example we&#8217;ve seen: hooking up a docs-search MCP\nserver meant the agent stopped guessing at architecture questions and\nstarted quoting <code>docs\/system\/architecture.md<\/code> back at us in its plans.\nThe marginal return on a good doc keeps going up as more tools learn to\nread it.<\/p>\n<h3>f. Docs become the contract between humans and agents<\/h3>\n<p>When a convention lives in <code>copilot-instructions.md<\/code>, it&#8217;s no longer\nhallway knowledge \u2014 it&#8217;s a published interface. Reviewers check PRs\nagainst the doc instead of against memory. Agents follow the same doc\nacross sessions and across models. Disagreements happen at the doc, not\nat the code, which is a much cheaper place to have them.<\/p>\n<p>This is the deepest payoff and the one that took us longest to notice.\nDocs aren&#8217;t just instructions for the AI; they&#8217;re the shared spec that\nlets humans and agents collaborate without re-negotiating the rules on\nevery change.<\/p>\n<hr \/>\n<p>None of these payoffs require new tooling. They all require a few hours\nof writing \u2014 done once, paid back forever.<\/p>\n<hr \/>\n<h2>4. A style guide for AI-readable docs<\/h2>\n<p>Most style-guide advice was written for humans skimming. These are the\npatterns that change the <em>model&#8217;s<\/em> behavior \u2014 grouped by what they&#8217;re\ntrying to do.<\/p>\n<h3>Structure: make the shape obvious<\/h3>\n<ul>\n<li><strong>Lead with the dataflow.<\/strong> A one-line flow at the top \u2014 HTTP request\nchain, build pipeline, event sequence, whatever your equivalent is \u2014\norients the model before any rules. Put yours at the top of\n<code>copilot-instructions.md<\/code> and never bury it.<\/li>\n<li><strong>Tables beat prose.<\/strong> Env vars, config keys, file responsibilities \u2014\nuse tables. Prose hides structure the model has to re-derive; a table\nlets it answer <em>&#8220;what&#8217;s the default for <code>LOG_LEVEL<\/code>?&#8221;<\/em> in one glance\ninstead of parsing three sentences and guessing.<\/li>\n<li><strong>Show the exact command.<\/strong> Not &#8220;run the backend&#8221; \u2014 the literal\n<code>cd backend &amp;&amp; uvicorn src.api.main:app --reload --port 8000<\/code>. The\nmodel will paste what you wrote, so make sure what you wrote can be pasted.\n(This cuts both ways: if the command is stale, the model\nwill confidently run the stale one.)<\/li>\n<li><strong>Date your long-lived docs.<\/strong> A small <code>Last reviewed: 2026-05<\/code> at the\ntop of system and feature docs gives both humans and the model a\nsignal for likely-stale content. Cheap to add, huge payoff for trust\nover time.<\/li>\n<\/ul>\n<h3>Discipline: keep the prompt lean and honest<\/h3>\n<ul>\n<li><strong>Link, don&#8217;t duplicate.<\/strong> Top-level instructions point to ADRs and\nguides. The model follows links when it needs depth, but it doesn&#8217;t\ndrown in the system prompt.<\/li>\n<li><strong>Be the source of truth, not a pointer outward.<\/strong> <em>&#8220;See the team wiki\nfor details&#8221;<\/em> is a dead end \u2014 the agent can&#8217;t follow links to private\nwikis, internal SharePoint sites, or Slack threads. If the model needs\nthe information, the information has to live in the repo.<\/li>\n<li><strong>Encode the negatives.<\/strong> <em>&#8220;Don&#8217;t add docstrings to code you didn&#8217;t\nchange.&#8221;<\/em> <em>&#8220;Don&#8217;t create markdown files unless requested.&#8221;<\/em> Explicit\nprohibitions matter more for AI than for humans because the model&#8217;s\ndefault behavior is <em>helpful expansion<\/em>.<\/li>\n<li><strong>Keep it current or delete it.<\/strong> Stale docs are worse than none \u2014\nthey actively mislead the model. We treat doc drift as a CI bug:\nlink-check on every PR, a grep gate that fails if known-renamed\nsymbols still appear in <code>docs\/**<\/code>, and a warning when an env-var name\nin <code>copilot-instructions.md<\/code> no longer exists in the codebase.<\/li>\n<\/ul>\n<h3>Diagnose: write for the failure modes you&#8217;ve actually seen<\/h3>\n<ul>\n<li><strong>Write for the model&#8217;s mistakes.<\/strong> If you&#8217;ve seen the AI hallucinate\na path, mention the real one. If it keeps proposing a pattern you\nrejected, write the ADR. If it keeps inventing a fourth section type,\nlist the three valid ones explicitly. Every recurring AI mistake is a\nmissing line of documentation.<\/li>\n<\/ul>\n<hr \/>\n<p>None of these rules are clever. They just respect the fact that the\nmodel is a literal, fast, forgetful reader \u2014 and so we write docs accordingly.<\/p>\n<hr \/>\n<h2>5. Unit tests are documentation too \u2014 and they matter more than ever<\/h2>\n<p>Everything above applies doubly to <strong>tests<\/strong>. A test suite is the only\nform of documentation an AI agent can <em>execute<\/em> \u2014 and in an AI-assisted\nworkflow, that promotes tests from quality net to <strong>control surface<\/strong>.<\/p>\n<p>Comments lie, READMEs go stale, ADRs describe intent \u2014 but a green test\nis empirical proof that the code behaves a certain way right now.<\/p>\n<h3>a. Coverage shapes what the agent dares to change<\/h3>\n<p>This is the pattern that surprised us most. In well-tested modules, the\nagent makes bold, correct changes \u2014 it knows it&#8217;ll find out immediately\nif it broke something. In poorly-tested modules, it tiptoes: adds\nredundant guards and duplicates logic instead of refactoring.<\/p>\n<p>Coverage isn&#8217;t just about catching bugs. It changes the <em>kind<\/em> of work\nthe agent is willing to do. A well-tested module gets braver edits; an\nuntested one gets timid ones. If you want better agent-authored code,\nthe cheapest lever is more tests on the modules you want the agent to\ntouch.<\/p>\n<h3>b. Tests are the feedback loop that makes async velocity safe<\/h3>\n<p>When you hand a task to an agent \u2014 &#8220;implement this, run the tests, fix\nwhat breaks&#8221; \u2014 the test suite <em>is<\/em> the spec. The model iterates against\nit the same way you would: change code, run tests, read failures,\nadjust.<\/p>\n<p>Without tests, the regression cost lands on <em>you<\/em>, after the agent is\ndone. With tests, it lands on the <em>agent<\/em>, mid-task, where it&#8217;s cheap\nto fix.<\/p>\n<p>That same loop is what makes async velocity safe. Human review can&#8217;t\nkeep up when agents merge multiple PRs per day; CI is the gate. The\npath-filtered workflows in\n<code>.github\/workflows\/<\/code> \u2014 lint, type-check,\nunit tests with coverage \u2014 are what stop &#8220;ship fast with AI&#8221; from\nbecoming &#8220;ship bugs fast with AI.&#8221;<\/p>\n<h3>c. Tests pin behavior the model would otherwise &#8220;improve&#8221;<\/h3>\n<p><strong>LLMs love to refactor.<\/strong> Left to their own devices, they rename a\nhelper, &#8220;simplify&#8221; an edge-case branch, or reorder arguments to match\nthe most popular pattern from their training data.<\/p>\n<p>A unit test that locks the contract \u2014 input X yields output Y \u2014 turns\nthose silent regressions into red CI checks. Either the model preserves\nthe behavior, or it explicitly asks to change the test.<\/p>\n<p>That second one is the conversation you wanted to have \u2014 instead of the\nsilent regression you wouldn&#8217;t have caught.<\/p>\n<h3>d. Tests document the <em>unhappy<\/em> paths<\/h3>\n<p>Docs and ADRs naturally capture happy-path intent. Tests capture what\nhappens when the input is empty, malformed or huge; when the upstream\nservice times out; when two parallel callers race; when the config flag\nis missing.<\/p>\n<p>These are exactly the scenarios where agents hallucinate\nplausible-looking behavior. Concrete example from our repo: we added\none test for <code>extract_table(html='')<\/code> returning <code>[]<\/code> after watching\nthree different agents &#8220;fix&#8221; the empty-input case by raising an\nexception. The test cost five minutes; it&#8217;s stopped that mistake at\nleast a dozen times since.<\/p>\n<p>A failing test on a malformed-input case is worth a thousand words of\n&#8220;be careful with edge cases&#8221; in <code>copilot-instructions.md<\/code>.<\/p>\n<h3>e. Tests as executable examples<\/h3>\n<p>A test like <code>test_ensure_agent_updates_existing_when_name_matches<\/code>\nteaches the model the contract of <code>ensure_agent<\/code> better than any\ndocstring. When the agent needs to call that function in new code, it\ncan grep the tests for usage, copy the pattern, and know it works.<\/p>\n<p>We&#8217;ve leaned into this: the test file is often the fastest way for a\nnew agent session (or a new engineer) to learn what a module promises.<\/p>\n<h3>Practical rules we follow<\/h3>\n<ul>\n<li><strong>Test the public surface, not the implementation.<\/strong> Agents refactor\ninternals constantly; tests on internals create churn.<\/li>\n<li><strong>One assertion concept per test, with a descriptive name.<\/strong> The name\nis what the model reads first when scanning failures.<\/li>\n<li><strong>Prefer fast unit tests over slow integration tests.<\/strong> Agents will\nrun the suite many times per task; latency compounds.<\/li>\n<li><strong>Test the error path explicitly.<\/strong> A <code>raises(...)<\/code> assertion on bad\ninput is non-negotiable for any function the model might call.<\/li>\n<\/ul>\n<p>And one rule important enough to stand on its own:<\/p>\n<blockquote><p><strong>Treat a failing test as a sacred signal.<\/strong> Don&#8217;t let agents &#8220;fix&#8221; a\ntest by weakening it. Encode that in instructions verbatim: <em>&#8220;If a\ntest fails, fix the code, not the assertion, unless the requirements\nchanged.&#8221;<\/em><\/p><\/blockquote>\n<p>This is the most AI-specific rule in the whole post. The default\nbehavior of a helpful, unconstrained agent is to make the red thing\ngreen by any means available \u2014 including deleting the assertion that\ncaught the bug.<\/p>\n<hr \/>\n<p>If documentation is the system prompt, <strong>tests are the runtime\nverification that the system prompt was followed<\/strong>. You need both.<\/p>\n<hr \/>\n<h2>6. Things we got wrong (and learned from)<\/h2>\n<ul>\n<li><strong>We over-wrote at first.<\/strong> Early <code>copilot-instructions.md<\/code> was 600\nlines. The model started losing focus. We trimmed it ruthlessly and\npushed depth into linked guides.<\/li>\n<li><strong>We under-scoped.<\/strong> A single repo-wide instructions file made the\nmodel apply Python conventions in TypeScript files. Splitting via\n<code>applyTo<\/code> fixed it.<\/li>\n<li><strong>We forgot to mark prohibitions.<\/strong> Until we wrote &#8220;do not create\nmarkdown summaries of changes,&#8221; the agent kept generating <code>CHANGES.md<\/code>\nfiles we had to delete.<\/li>\n<li><strong>We let ADRs go stale.<\/strong> A few ADRs described a &#8220;proposed&#8221; state that\nhad since shipped. The model believed the proposed pattern was still\nthe future. Lesson: review ADR statuses quarterly.<\/li>\n<\/ul>\n<hr \/>\n<h2>7. A practical starting checklist<\/h2>\n<p>If you&#8217;re working in an HVE \/ AI-assisted setup and you want to capture\nsome of this leverage, in order:<\/p>\n<ol>\n<li><strong>Write a one-page <code>.github\/copilot-instructions.md<\/code><\/strong> (or\n<code>AGENTS.md<\/code>) today. See \u00a72 and \u00a74 for the shape \u2014 the short version\nis: architecture in one line, key directories one line each, env\nvars in a table, exact dev commands, cross-cutting conventions, and\nexplicit prohibitions.<\/li>\n<li>Add a PR template that the model will fill in by default.<\/li>\n<li><strong>Start an ADR folder.<\/strong> Write ADR 001 about something already\ndecided \u2014 a settled choice is the cheapest first ADR because there&#8217;s\nnothing to debate, only to record.<\/li>\n<li>When you find yourself correcting the model the same way twice,\nwrite it down. Either in instructions (rule) or in an ADR\n(decision).<\/li>\n<li>Scope instructions with <code>applyTo<\/code> the first time you catch the model\napplying Python conventions in TypeScript files (or vice versa).<\/li>\n<li><strong>Create <code>docs\/<\/code> with <code>system\/<\/code>, <code>features\/<\/code>, and <code>guides\/<\/code>\nsubfolders.<\/strong> Seed each with one real page \u2014 even a stub beats an\nempty folder, because it tells the model where new content belongs.<\/li>\n<li>Add a SKILL for any multi-step workflow you&#8217;d otherwise re-explain\n(creating PRs, addressing review comments, releasing).<\/li>\n<li><strong>Treat your test suite as documentation.<\/strong> Add a unit test for\nevery bug you fix and every contract you want the agent to\npreserve. Wire it into CI so agent-authored PRs cannot merge red.<\/li>\n<li>Treat docs the same as code: PRs, review, owners.<\/li>\n<\/ol>\n<p>You don&#8217;t need to do all nine this week. Item 1 alone will change how\nevery future PR feels.<\/p>\n<hr \/>\n<h2>TL;DR<\/h2>\n<p>Documentation is no longer a chore that competes with shipping \u2014 it&#8217;s\nthe <em>substrate<\/em> on which shipping now happens. Tests are the executable\nhalf: the only documentation an agent can run, and the only safety net\nfast enough for AI-velocity merges. The repos that ship cleanly with AI\nare the ones whose conventions, decisions, workflows <strong>and contracts<\/strong>\nare written down in ways that both humans and models can read \u2014 and\nverify.<\/p>\n<p>Write the README. Write the ADR. Write the SKILL.md. <strong>Write the\ntest.<\/strong> Each one is a contract \u2014 with your team, with your future self,\nand with whichever agent reads the repo six months from now.<\/p>\n<h2>Attribution<\/h2>\n<p>The image used in this post was created using <a href=\"https:\/\/openai.com\/policies\/terms-of-use\/?utm_source=chatgpt.com \">ChatGPT<\/a> GPT-5 (an OpenAI model and Microsoft partner).<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A field guide to building a layered, AI-readable documentation system \u2014 instruction files, skills, ADRs, and tests \u2014 that turns your repo into a reliable substrate for Copilot, agents, and high-velocity engineering workflows.<\/p>\n","protected":false},"author":136530,"featured_media":16733,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[33,3649,3671,3672,3400],"class_list":["post-16732","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cse","tag-ai","tag-copilot","tag-documentation","tag-hve","tag-ise"],"acf":[],"blog_post_summary":"<p>A field guide to building a layered, AI-readable documentation system \u2014 instruction files, skills, ADRs, and tests \u2014 that turns your repo into a reliable substrate for Copilot, agents, and high-velocity engineering workflows.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/16732","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/users\/136530"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/comments?post=16732"}],"version-history":[{"count":1,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/16732\/revisions"}],"predecessor-version":[{"id":16735,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/16732\/revisions\/16735"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media\/16733"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media?parent=16732"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/categories?post=16732"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/tags?post=16732"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}