<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Processes · David Brandt</title>
  <link href="https://icelander.net/v1/processes/feed.xml" rel="self"/>
  <link href="https://icelander.net/v1/processes"/>
  <id>https://icelander.net/v1/processes/feed.xml</id>
  <author><name>David Brandt</name></author>
  <updated>2026-11-03T00:00:00Z</updated>
  <entry>
    <title>The unreviewable pull request</title>
    <link href="https://icelander.net/v1/processes/the-unreviewable-pull-request"/>
    <id>tag:icelander.net,2026:the-unreviewable-pull-request</id>
    <published>2026-11-03T00:00:00Z</published>
    <updated>2026-11-03T00:00:00Z</updated>
    <category term="processes" label="Processes"/>
    <summary>Big tasks grow into pull requests nobody can review. I used to split them into stacks by hand, after the fact, and hated it. Now an agent splits the stack, other agents review each layer, and my own review is finally worth something.</summary>
    <content type="html">&lt;p&gt;&lt;em&gt;Let it grow, then have an agent carve it up&lt;/em&gt;&lt;/p&gt;&lt;p&gt;Thought 2019, written November 2026. Processes.&lt;/p&gt;&lt;p&gt;It starts with a task that fits on a sticky note. Somewhere along the way I notice that the code I&#39;m touching is a mess, and since I&#39;m in there anyway, I clean it up. The cleanup exposes an edge case nobody handled, so I handle it. Handling it properly needs tests that don&#39;t exist, so I write them. The tests find two bugs. I fix them. Then I remember a feature that was part of the original ask and somehow fell off the plan, and I add that too.&lt;/p&gt;
&lt;p&gt;Eventually I open a pull request with 400 files and 20,000 lines changed.&lt;/p&gt;
&lt;p&gt;That&#39;s on a good day. On a bad day, I&#39;ve also found a better name for one of the core concepts along the way. What everyone has been calling a &amp;quot;user&amp;quot; is really an &amp;quot;account&amp;quot;, so I fix it, and a codebase-wide search and replace changes a line here and there in a thousand files. Add the generated code and the test snapshots that get regenerated because the word appears in them, and the PR isn&#39;t 20,000 lines any more. It&#39;s 400,000. Something microscopic did that. One word.&lt;/p&gt;
&lt;p&gt;Every one of those lines is, as far as I can tell, an improvement. Together, they&#39;re unreviewable.&lt;/p&gt;
&lt;p&gt;Nobody reads 20,000 lines, let alone 400,000. A reviewer handed a PR like that has two honest options, both bad: spend a week on it, or skim it, leave a comment about a variable name so it looks read, and approve. The research on this is old and the metric is crude, but it points the same way. In &lt;a href=&quot;https://smartbear.com/learn/code-review/best-practices-for-peer-code-review/&quot;&gt;SmartBear&#39;s study of a team at Cisco&lt;/a&gt;, reviewers stopped finding much after 200 to 400 lines. My good-day PR was fifty of those, stapled together. The bad-day one was a thousand.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;A manager I had years ago was hostile to PRs like that. Hard-core hostile. A monster didn&#39;t get reviewed. It got sent back to be split.&lt;/p&gt;
&lt;p&gt;I didn&#39;t like it at the time. Naturally.&lt;/p&gt;
&lt;p&gt;GitHub had no concept of a stack back then. A chain of PRs was a chain of branches, each based on the one before it, and every fix to the bottom one had to be rebased up through the rest by hand. The same went for every time the master branch moved under me: I had to propagate the merge up through every PR in the chain, again and again, until the reviewer deigned them worth his time. Skip a round, and the drift quickly became unmanageable. Not that it wasn&#39;t tough enough already.&lt;/p&gt;
&lt;p&gt;All of that was tedious, and it was the smallest of my problems.&lt;/p&gt;
&lt;p&gt;The big one was that the split can&#39;t be planned up front. With a large task, you only know exactly what you&#39;re doing in retrospect, after you&#39;ve done it. En route, you have a broad idea, and a broad idea doesn&#39;t tell you where the seams are. So I&#39;d finish the work, then pluck it apart, hunk by hunk, &lt;code&gt;git add -p&lt;/code&gt; until my eyes glazed over. The job was to take a week&#39;s worth of exploratory work, with its detours, dead ends and second thoughts, and retrofit it into a perfect string of pearls: the refactor, then the tests, then the two bug fixes, then the feature. Each pearl in its place, lined up as if there had never been anything but 100% certainty about the path and the destination.&lt;/p&gt;
&lt;p&gt;That was the horror.&lt;/p&gt;
&lt;p&gt;It wasted a lot of time. I learned quickly that his way was better anyway.&lt;/p&gt;
&lt;p&gt;What surprised me was that splitting didn&#39;t just make the review easier. It made the work better. Each PR in the chain was a milestone, and a milestone has to stand on its own: it builds, its tests pass, and its description says what it does without leaning on the next eleven. When a refactor is cut loose from the feature that motivated it, you notice the parts of the refactor that only made sense for a version of the feature that later changed shape. When the tests have to stand alone, you notice they&#39;re half-finished. Debug leftovers, dead code, the abandoned first attempt that&#39;s still half in there: a monster hides all of that, and a milestone doesn&#39;t. Every PR came out more correct, more maintainable, more beautiful. More all sorts of good things.&lt;/p&gt;
&lt;p&gt;And it took load off the reviewer, which matters more than it sounds. In a review process that&#39;s actually worth something (not the rubber stamp practiced in most places), the reviewers are the more experienced engineers. They&#39;re overloaded, their time is limited, and what they know is some of the most valuable knowledge in the building. Handing them 20,000 lines wastes the scarcest thing you have.&lt;/p&gt;
&lt;p&gt;He was right. It just took me a while to stop resenting the hours it cost.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Today, Opus generally has more knowledge, more time and more skill than those reviewers did. You&#39;d think that makes the problem go away. It doesn&#39;t. Opus also does a better job on a focused PR than on a multi-hundred-file monstrosity. Its attention runs out later than a human&#39;s, but it runs out, and it runs out quietly. An overwhelmed human knows they&#39;re overwhelmed. An overwhelmed model writes a confident review of whichever parts it happened to look at closely.&lt;/p&gt;
&lt;p&gt;So the rule stands. What changed is who pays for it.&lt;/p&gt;
&lt;p&gt;I still let the task grow. I&#39;ve stopped fighting that. Refactoring along the way, handling the edge cases, adding the feature everyone forgot: that&#39;s how I find out what the task actually was. Then, once I know what I did, I tell an agent to split it into a stack. It strings the pearls without complaint, and it has no trouble pretending we knew where we were going all along. Then I have an agent review each PR in the stack. Then an agent resolves the merge conflicts the review fixes cause, and merges the stack, bottom up.&lt;/p&gt;
&lt;p&gt;GitHub now supports &lt;a href=&quot;https://github.github.com/gh-stack/&quot;&gt;stacked pull requests&lt;/a&gt; as a first-class concept, in public preview since July. That helps, but the tooling was never the hard part. The hard part was the retrofitting, and that&#39;s exactly what I delegate.&lt;/p&gt;
&lt;p&gt;The best side effect is the one I didn&#39;t expect. With the work cut into tractable bites, my own valuable time and inferior mind can provide a much better review of the agents&#39; work. I can follow a 600-line PR that does one thing. I could never follow the monster, and I wrote it. Well, obviously I didn&#39;t write it. I asked Claude to.&lt;/p&gt;
&lt;p&gt;The rest of this is the parts that took me a while to get right.&lt;/p&gt;
&lt;h2&gt;1. The monster is the specification&lt;/h2&gt;
&lt;p&gt;Don&#39;t delete the monster branch after the split. Until the stack is merged, it&#39;s the source of truth, and it makes the split cheap to check: the top of the stack must produce exactly the same tree as the monster. &lt;code&gt;git diff monster stack-top&lt;/code&gt; should print nothing.&lt;/p&gt;
&lt;p&gt;That one command turns &amp;quot;did the agent drop a hunk somewhere in 400 files?&amp;quot; from a question I&#39;d have to read my way to into one a machine answers in a second. Divergence from the monster is allowed only on purpose, as review fixes made after the split, in their own commits.&lt;/p&gt;
&lt;h2&gt;2. Every layer builds, passes and ships&lt;/h2&gt;
&lt;p&gt;A stack isn&#39;t a pile of diffs cut at arbitrary points. Each layer must compile, pass its tests, and be safe to merge and deploy without the layers above it. CI runs on every layer, not just the top. That&#39;s what makes a layer reviewable on its own, and it&#39;s what lets the bottom of the stack land while the top is still being argued over.&lt;/p&gt;
&lt;p&gt;If something can&#39;t ship half-done, it goes behind a flag, or it gets the &lt;a href=&quot;https://martinfowler.com/bliki/ParallelChange.html&quot;&gt;parallel change&lt;/a&gt; treatment: expand, migrate, contract. That used to be the extra work everybody skipped. Agents don&#39;t skip it, and they don&#39;t sigh.&lt;/p&gt;
&lt;p&gt;The payoff comes later. When something breaks in production three weeks on, &lt;code&gt;git bisect&lt;/code&gt; lands on a PR that does one thing, and the revert takes out one thing, not a feature, a refactor and two unrelated bug fixes.&lt;/p&gt;
&lt;p&gt;Not that anybody in their right mind reaches for &lt;code&gt;git bisect&lt;/code&gt; unless they&#39;re truly desperate. Humans, that is. Agents don&#39;t care.&lt;/p&gt;
&lt;h2&gt;3. A refactor or a behavior change, never both&lt;/h2&gt;
&lt;p&gt;This is the most useful seam in any split. Kent Beck put it best: &lt;a href=&quot;https://x.com/KentBeck/status/250733358307500032&quot;&gt;&amp;quot;make the change easy (warning: this may be hard), then make the easy change.&amp;quot;&lt;/a&gt; The stack should read in that order.&lt;/p&gt;
&lt;p&gt;A refactor PR changes no behavior, and proves it: the existing tests pass unchanged. If a refactor touches a test&#39;s assertions, it isn&#39;t a refactor. Mechanical changes (renames, moves, formatting, generated code) get their own PRs, because they&#39;re reviewed differently. Nobody should read a mechanical change line by line. You check how it was produced and that the tests still pass. That leaves the PR that changes behavior small, and that&#39;s where the real review goes.&lt;/p&gt;
&lt;h2&gt;4. A rename gets a PR of its own&lt;/h2&gt;
&lt;p&gt;The rename from the bad day is the extreme case, and the most instructive one. By line count, it&#39;s the biggest change in the monster. By content, it&#39;s the smallest: one word. Inside the monster, it&#39;s poison. The one line in file 612 that changes behavior sits between two lines that only changed a word, and nobody, human or model, reliably spots it in 400,000 lines of noise.&lt;/p&gt;
&lt;p&gt;Pulled out on its own, the same thousand files make one of the easiest PRs there is to review, as long as nobody reads it. The PR says exactly how it was made: the search, the replacement, the command that regenerated the code and the snapshots. The reviewer (usually an agent) reruns all of that on the base branch and diffs the result against the PR. If the diff is empty, it&#39;s done. What&#39;s left is the interesting part: the places where the search got it wrong. A &amp;quot;user interface&amp;quot; is not an &amp;quot;account interface&amp;quot;. Those hand-made exceptions go in a commit of their own, listed in the description, and they&#39;re the only part anyone reads line by line.&lt;/p&gt;
&lt;p&gt;The rename goes at the bottom of the stack, so every layer above it is written and reviewed in the final vocabulary. The reviewer of layer 7 shouldn&#39;t have to translate. I found the name last. It goes first.&lt;/p&gt;
&lt;p&gt;It merges first, too, and fast. A thousand-file PR conflicts with everything anyone else is working on, so every day it waits, it rots a little more, and so does every other branch in the repository.&lt;/p&gt;
&lt;p&gt;The exception is a rename that crosses a boundary. A database column, a field in a public API, a key in serialized data, a setting in someone else&#39;s config: those are behavior changes wearing a rename&#39;s clothes. They get the parallel change treatment from section 2, and a real review.&lt;/p&gt;
&lt;p&gt;Take the rename out, and the 400,000-line monster is a 20,000-line monster again. Still a monster, but one that can be carved.&lt;/p&gt;
&lt;h2&gt;5. Not everything belongs in the chain&lt;/h2&gt;
&lt;p&gt;A monster is several things that grew together, and not all of them depend on each other. The two bug fixes the new tests found don&#39;t need the feature. Usually they don&#39;t even need the tests. Those come out as independent PRs against main and merge first, today, where they&#39;re worth something.&lt;/p&gt;
&lt;p&gt;A stack should be a chain only where there&#39;s a real dependency. Everywhere else it&#39;s a tree, or a few unrelated PRs. Every layer taken out of the chain is one less layer to restack when something below it changes, and one less thing waiting on the slowest review in the line.&lt;/p&gt;
&lt;h2&gt;6. One idea per PR, not N lines per PR&lt;/h2&gt;
&lt;p&gt;Line count is a proxy. Two to four hundred lines is a decent smell test, but a 380,000-line rename is easier to review than a 40-line change to a locking scheme. What I ask for is one idea per PR, a title that states it without an &amp;quot;and&amp;quot;, and a description that says what the PR does, what it deliberately doesn&#39;t do, and where it sits in the stack. If the agent can&#39;t write the title without an &amp;quot;and&amp;quot;, it&#39;s two PRs.&lt;/p&gt;
&lt;p&gt;Too small is a failure too. A stack of sixty trivial PRs has lost the story just as thoroughly as one PR with sixty ideas in it. A reviewer should be able to read the stack&#39;s titles from bottom to top and see the string of pearls: the change, told in the order it should have been built, even though it wasn&#39;t. If they can&#39;t, the split is wrong, whatever the line counts say.&lt;/p&gt;
&lt;h2&gt;7. A fresh reviewer for every layer&lt;/h2&gt;
&lt;p&gt;Each layer gets its own review session, one that didn&#39;t write the code and didn&#39;t do the split. It gets the plan or design document, so it knows where the stack is headed, and it gets its own diff, which is what it judges.&lt;/p&gt;
&lt;p&gt;&amp;quot;This gets fixed in PR 9&amp;quot; is not an acceptable answer in the review of PR 4. If PR 4 is only correct because of PR 9, the split is wrong, and that&#39;s a finding against the split, not a reason to approve.&lt;/p&gt;
&lt;h2&gt;8. Someone still has to look at the whole thing&lt;/h2&gt;
&lt;p&gt;This is what splitting costs. Twelve reviewers, each judging one slice, can each be right and still miss how slice 3 and slice 9 interact. Cross-cutting concerns (consistency, naming, error handling that&#39;s done three different ways in three layers) don&#39;t belong to any one layer.&lt;/p&gt;
&lt;p&gt;So once the layers are reviewed, one more review goes over the whole stack against the design. That sounds like reviewing the monster after all, but it isn&#39;t. Every piece in it has already been vetted, so the reviewer only has to think about the joints. It&#39;s a smaller, sharper question, and both agents and humans answer it better.&lt;/p&gt;
&lt;h2&gt;9. Fixes flow up, merges flow down&lt;/h2&gt;
&lt;p&gt;A review fix in layer 3 conflicts with layers 4 through 12. That cascade was the smallest of my problems when I maintained stacks by hand, but it was still a problem, and every cascade cost an afternoon. Now restacking is the agent&#39;s job (with &lt;code&gt;gh stack sync&lt;/code&gt; doing most of the rebasing), and the rule I give it is simple: fix the problem in the lowest layer where it exists. Never patch it at the top. A fix at the top leaves every layer in between lying about what it does.&lt;/p&gt;
&lt;p&gt;Merges go bottom up, and a layer merges as soon as it&#39;s approved. Every day an approved layer sits unmerged is a day main moves under it, and that drift has to be propagated up through every layer above. It used to be my job. It&#39;s an agent&#39;s now, but the drift is no smaller for it.&lt;/p&gt;
&lt;h2&gt;10. Where my own attention goes&lt;/h2&gt;
&lt;p&gt;Not evenly. That&#39;s the point of having layers.&lt;/p&gt;
&lt;p&gt;For mechanical layers, I check the evidence: the tree diff came back empty, the rename reproduces from its own command, the tests didn&#39;t change. Then I skim. For refactor layers, I check that the tests didn&#39;t change, and I read the parts the review flagged. The layer that changes behavior gets a real read, line by line, and so does anything that touches money, security or deleting data, however mechanical it claims to be.&lt;/p&gt;
&lt;p&gt;I also read the reviews before the diffs, and I read the author agent&#39;s reasons for dismissing a finding more carefully than either. &amp;quot;This is intentional&amp;quot; is the most expensive sentence in the whole process when it isn&#39;t true.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;These days I let my tasks grow into monsters without guilt, because I know what happens to them next. They get sent back to be split. Every single one.&lt;/p&gt;
&lt;p&gt;I&#39;ve turned into that manager. The difference is that my reports are agents, and they don&#39;t take it personally.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Tests are written for the maintainer</title>
    <link href="https://icelander.net/v1/processes/tests-are-written-for-the-maintainer"/>
    <id>tag:icelander.net,2026:tests-are-written-for-the-maintainer</id>
    <published>2026-11-01T00:00:00Z</published>
    <updated>2026-11-01T00:00:00Z</updated>
    <category term="processes" label="Processes"/>
    <summary>How I wrote unit tests before AI took over the writing: every test in the same shape, variables named by their role, one branch per test, and copy-paste over clever helpers. A test is written once and read for years, so I wrote it for the reader.</summary>
    <content type="html">&lt;p&gt;&lt;em&gt;The unit testing methodology I considered good practice, before the advent of AI-assisted development&lt;/em&gt;&lt;/p&gt;&lt;p&gt;Thought 2010, written October 2026. Processes.&lt;/p&gt;&lt;p&gt;&lt;em&gt;A note before you start: this describes software development as it was before AI-driven development replaced human development. Humans wrote the code, humans wrote the tests, and humans had to maintain both. That world is gone. My agents write my tests now, test-first, and not one of them has complained that it isn&#39;t how they think. What follows is the last unit testing methodology I wrote for human hands, published as a record of how it was done when we still did it ourselves.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;A test is written once and read many times: in code review, when it fails, and when someone changes the code it protects a year later. That last reader decides whether the test survives. If it is hard to update, because it was never written to be maintained and tests the implementation rather than the intent, it gets commented out or deleted. Most of the unit tests I have had to deal with were written for none of those readers.&lt;/p&gt;
&lt;p&gt;They were written once, by someone who had already done the fun part. The implementation was finished, its author believed it was correct, and the test was hammered together afterwards, by copy-paste, in whatever mocking and testing framework the project happened to use. The result was rarely short, because the mocks were a sprawl of incomprehensible goo, and it was illegible, because it had no organization at all. Whether a test checked the intent of the code or its implementation was anyone&#39;s guess. Usually it checked a subset of the implementation.&lt;/p&gt;
&lt;p&gt;Nobody really knew what those tests verified. They had not been written first, so none of them had ever been seen to fail against the code they were testing, and few of them verified that they exercised the branch they were named after. When anyone ran a coverage tool at all, they ran it over the whole project and took line coverage per folder as a sign that things were well tested.&lt;/p&gt;
&lt;p&gt;The code around the tests was no better. Shared fixtures existed, but they were odd and only partly reusable, and neither the fixtures nor the test doubles had tests of their own. Across a codebase there was no consistency, because nobody liked writing tests and nobody cared how they looked.&lt;/p&gt;
&lt;p&gt;I don&#39;t think the answer to that is TDD. Writing the test before the code is a nightmare for most people; it is not how we think. The answer I settled on was to make tests so uniform that writing a good one takes no inspiration, and reading one takes no effort.&lt;/p&gt;
&lt;p&gt;I eventually wrote that down as a unit testing style guide for C#. It is the last version of the methodology I wrote for humans: C# was the last programming language I used while people were still the primary readers and writers of automated tests. This article describes what is in it. It is deliberately strict, and some of it will look like ceremony.&lt;/p&gt;
&lt;h2&gt;The premise: optimize for the maintainer&lt;/h2&gt;
&lt;p&gt;The usual objection to a strict test format is that it is verbose. A ten-line test becomes twenty-five. Section comments, prefixed variable names and an extra variable for every expected value all look like overhead.&lt;/p&gt;
&lt;p&gt;They are overhead for the writer, once. For everyone who reads the test afterwards, they are signposts. A section header you can glance past costs nothing. An ambiguous test that you have to decode line by line, with the production code open beside it, costs a lot, and it costs that again for every person who reads it. Sooner or later a maintainer decides the cost is not worth paying, and the test is gone.&lt;/p&gt;
&lt;p&gt;That trade, a few more lines for the writer in exchange for a test anyone can read in one pass and update with confidence, drives everything below.&lt;/p&gt;
&lt;h2&gt;1. Every test has the same shape&lt;/h2&gt;
&lt;p&gt;The root is the familiar Arrange, Act, Assert. AAA is fine for a three-line test of a pure function, and the guide allows exactly that. The trouble is that AAA is coarse. Once a test has mocks, captured values, an environment to build and interactions to verify, &amp;quot;Arrange&amp;quot; becomes a wall of code in which inputs, scaffolding and collaborators are mixed together.&lt;/p&gt;
&lt;p&gt;So the guide splits each stage into named sections, always in this order, each introduced by a comment:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;What goes there&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Arrange&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GIVEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The inputs and conditions that drive the scenario&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CAPTURE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Placeholders for values that a mock or fake will record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MOCKING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mocks and fakes, and how they behave&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SETUP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The environment the code under test runs in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SYSTEM UNDER TEST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Construction of the thing being tested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Act&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WHEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The single action under test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assert&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EXPECTATIONS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What the results should be, declared before any assertion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;THEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assertions on results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LOGGING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assertions on what was logged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEHAVIOR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verification of interactions with mocks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Empty sections are omitted, and sections are never merged. Section headers are full sentences that state intent rather than mechanics, such as &lt;code&gt;// GIVEN: a customer whose balance is below the threshold&lt;/code&gt;. A long section is split into sub-sections with headers of their own, such as &lt;code&gt;// MOCKING: the accounting API rejects the record&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here is what that looks like for a service that should schedule a shipment when an order is submitted:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;/// &amp;lt;summary&amp;gt;
/// Verifies that OrderProcessor publishes a shipment when an order flagged for immediate shipping is submitted.
/// &amp;lt;/summary&amp;gt;
[Fact]
public async Task Test_SubmitOrder_SchedulesShipment_WhenShipImmediatelyIsSet()
{
    // GIVEN: an order that should be shipped immediately
    Guid givenOrderId = Guid.NewGuid();
    Order givenOrder = new(givenOrderId, ShipImmediately: true);

    // CAPTURE: the shipment message sent to the message bus
    ShipmentScheduled? capturePublishedShipment = null;

    // MOCKING: the repository holds the given order, and the bus accepts one shipment
    Mock&amp;lt;IOrderRepository&amp;gt; mockRepository = new(MockBehavior.Strict);
    mockRepository.Setup(repo =&amp;gt; repo.GetAsync(givenOrderId)).ReturnsAsync(givenOrder).Verifiable();
    Mock&amp;lt;IMessageBus&amp;gt; mockMessageBus = new(MockBehavior.Strict);
    mockMessageBus
        .Setup(bus =&amp;gt; bus.PublishAsync(It.IsAny&amp;lt;ShipmentScheduled&amp;gt;()))
        .Callback&amp;lt;ShipmentScheduled&amp;gt;(message =&amp;gt; capturePublishedShipment = message)
        .Returns(Task.CompletedTask)
        .Verifiable();

    // SETUP: expose the repository and the message bus to the processor
    IOrderRepository envRepository = mockRepository.Object;
    IMessageBus envMessageBus = mockMessageBus.Object;

    // SYSTEM UNDER TEST
    OrderProcessor sut = new(envRepository, envMessageBus);

    // WHEN
    await sut.SubmitOrderAsync(givenOrderId);
    ShipmentScheduled? actualShipment = capturePublishedShipment;

    // EXPECTATIONS
    Guid expectedOrderId = givenOrderId;

    // THEN
    Assert.NotNull(actualShipment);
    Assert.Equal(expectedOrderId, actualShipment!.OrderId);

    // BEHAVIOR
    mockRepository.Verify();
    mockMessageBus.Verify();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is long. It is also a test I can review in one pass without running it, because every block announces its purpose and every block is where I expect it to be. When a hundred tests share that shape, I stop reading the structure and only read the differences.&lt;/p&gt;
&lt;h2&gt;2. A variable&#39;s name says what role it plays&lt;/h2&gt;
&lt;p&gt;The second idea is that a reader should know, from a variable&#39;s name alone, which section it came from and what it is allowed to be used for:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;given*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An input to the scenario&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;capture*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A placeholder that a mock or fake fills in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mock*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A mock object, used only in &lt;code&gt;SETUP&lt;/code&gt; and &lt;code&gt;BEHAVIOR&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fake*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A hand-written test double&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;env*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A dependency as the code under test sees it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sut&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The thing being tested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actual*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Something the test observed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expected*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Something the test predicted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tmp*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A staging value that never leaves its section&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The payoff is in the assertions. &lt;code&gt;THEN&lt;/code&gt; compares &lt;code&gt;expected*&lt;/code&gt; values with &lt;code&gt;actual*&lt;/code&gt; values, and nothing else. If an assertion compares against a &lt;code&gt;given*&lt;/code&gt; variable, or reads a property straight off an &lt;code&gt;env*&lt;/code&gt; collaborator, it is visibly wrong, and a reviewer catches it without understanding the domain.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;env*&lt;/code&gt; layer looks like the most pedantic rule in the guide, and the guide restates it in a chapter of its own. Mocks are first assigned to a variable of the interface type, and only that variable reaches the code under test:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;IOrderRepository envRepository = mockRepository.Object;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without it, constructor calls turn into rows of &lt;code&gt;.Object&lt;/code&gt; accessors that the reader has to map back to their declarations. With it, the &lt;code&gt;SYSTEM UNDER TEST&lt;/code&gt; section reads like production wiring, in domain types, and the mocking framework stays inside &lt;code&gt;MOCKING&lt;/code&gt; and &lt;code&gt;BEHAVIOR&lt;/code&gt; where it belongs.&lt;/p&gt;
&lt;p&gt;The same thinking applies to &lt;code&gt;EXPECTATIONS&lt;/code&gt;. Even when the expected value is just a copy of a given value, it gets its own &lt;code&gt;expected*&lt;/code&gt; variable. That looks redundant until the day the expectation stops being a copy, and then the test already has the place to say so.&lt;/p&gt;
&lt;h2&gt;3. One test, one branch, and prove you reached it&lt;/h2&gt;
&lt;p&gt;Test names describe a specific branch of execution: &lt;code&gt;Test_ProcessPayment_ReturnsDeclined_WhenBalanceIsLow&lt;/code&gt;, never &lt;code&gt;Test_Process&lt;/code&gt; or &lt;code&gt;Test_Works&lt;/code&gt;. One test covers one branch. Parameterized tests are allowed when the same behavior really is being exercised across several datasets, not as a way to cram different scenarios into one method.&lt;/p&gt;
&lt;p&gt;Naming the branch is not enough, though. The failure mode I care about most is a test that passes because its setup never reached the branch it claims to test. The happy path ran, the assertion happened to hold, and the edge case is uncovered while the suite reports green.&lt;/p&gt;
&lt;p&gt;This is also why line coverage across a project says so little. A line counts as covered if any test happened to run it, whether or not that test checked the result. Coverage that means something is measured per branch, and it only counts branches a test exercises on purpose, because only those tests actually verify that the outcome was the one intended.&lt;/p&gt;
&lt;p&gt;So each test has to show that it took the path it is named for. In C# the practical tools are logging and strict mocks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Assert on logs.&lt;/strong&gt; Code that logs the branch it takes can be checked in a &lt;code&gt;LOGGING&lt;/code&gt; section, and that is usually easier to write and read than reconstructing the path from mock calls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All mocks are strict or verifiable, and every setup is verified.&lt;/strong&gt; An unexpected call fails the test. An expected call that did not happen fails the test. A mock that was set up and never verified is treated as a bug in the test.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without TDD, this is what stands in for watching a test fail first. A test that proves it took its branch, and compares that branch&#39;s outcome with a declared expectation, has very little room to pass by accident.&lt;/p&gt;
&lt;p&gt;Each test also carries a documentation comment in the form &lt;em&gt;&amp;quot;Verifies that &lt;code&gt;&amp;lt;SUT&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;behavior&amp;gt;&lt;/code&gt; when &lt;code&gt;&amp;lt;trigger&amp;gt;&lt;/code&gt;&amp;quot;&lt;/em&gt;, so the prose maps directly onto the &lt;code&gt;SYSTEM UNDER TEST&lt;/code&gt;, &lt;code&gt;WHEN&lt;/code&gt; and &lt;code&gt;THEN&lt;/code&gt; sections.&lt;/p&gt;
&lt;h2&gt;4. Mock as little as possible, and prefer fakes when mocks get gnarly&lt;/h2&gt;
&lt;p&gt;The mocking rules are short:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Never mock what you don&#39;t have to. Prefer real instances and shared fixtures.&lt;/li&gt;
&lt;li&gt;Only mock what the code under test uses directly. Its dependencies have their own tests.&lt;/li&gt;
&lt;li&gt;When a mock needs nested argument matchers, reflection or callback gymnastics to verify one interaction, write a fake.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The guide&#39;s example is a structured log line. Verifying it through a mocking framework takes a block of &lt;code&gt;It.Is&amp;lt;It.IsAnyType&amp;gt;(...)&lt;/code&gt; matchers and reflection over anonymous types. A hand-written &lt;code&gt;FakeLogger&lt;/code&gt; that records entries and exposes &lt;code&gt;AssertExercised(&amp;quot;Foo&amp;quot;, &amp;quot;bar&amp;quot;)&lt;/code&gt; turns the same check into one line that reads like the requirement. Fakes cost maintenance and can drift from the real thing, and the guide says so, but for verification-heavy collaborators they are usually the clearer choice.&lt;/p&gt;
&lt;p&gt;Test data comes from a shared fixture factory: small functions such as &lt;code&gt;Fixtures.Order()&lt;/code&gt; that each return a valid object, which a test then adjusts in its &lt;code&gt;GIVEN&lt;/code&gt; section. When a model changes, the fixtures change in one place. Fixtures and fakes are code that every test relies on, so they get tests of their own.&lt;/p&gt;
&lt;h3&gt;Test the seams&lt;/h3&gt;
&lt;p&gt;Mocking has a blind spot, and it sits exactly where two components meet. In the caller&#39;s unit tests, the callee is mocked out by hand. In the callee&#39;s unit tests, its inputs are set up by hand. Both suites can be green while the caller calls the callee wrongly, because nothing checks that the caller&#39;s idea of the callee matches the real one, for every branch.&lt;/p&gt;
&lt;p&gt;So the seams between components need tests of their own. This matters most in untyped languages, and with general-purpose mocking frameworks that do not enforce that a mock matches the call signature of the thing it replaces. There, a mock can accept a call the real component would reject, and the unit tests on both sides will never notice.&lt;/p&gt;
&lt;h2&gt;5. Copy-paste beats clever helpers&lt;/h2&gt;
&lt;p&gt;This one surprises people. The guide prefers duplicated setup over helper methods.&lt;/p&gt;
&lt;p&gt;This is not the copy-paste I complained about at the start. Copying undisciplined code spreads the mess. What the guide copies is boilerplate: the same sections, in the same order, with the same setup, instead of poor shared code that tries to do the job for every test at once. A test that looks exactly like its neighbours is boring, and boring is the point. When everything that is the same looks the same, the actual differences stand out.&lt;/p&gt;
&lt;p&gt;Start with a happy-path test that spells out the whole arrangement. Copy it for each edge case, change only what differs, and say what differs in the section header: &lt;code&gt;// GIVEN: customer balance is below the threshold&lt;/code&gt;. Two tests that are ninety percent identical, side by side, are easier to review than this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// The kind of helper the guide asks you not to write
private async Task AssertPaymentAsync(decimal balance, PaymentStatus expectedStatus) { ... }

[Fact]
public Task Test_ProcessPayment_ReturnsDeclined_WhenBalanceIsLow()
    =&amp;gt; AssertPaymentAsync(2.00m, PaymentStatus.Declined);

[Fact]
public Task Test_ProcessPayment_ReturnsApproved_WhenBalanceIsSufficient()
    =&amp;gt; AssertPaymentAsync(25.00m, PaymentStatus.Approved);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both tests are short, and neither shows what it does. To find out what they arrange, which mocks they configure and what they assert, you have to open the helper and work out what &lt;code&gt;2.00m&lt;/code&gt; stands for. Every test that uses the helper depends on it, and changes to it ripple through all of them.&lt;/p&gt;
&lt;p&gt;Shared fixtures don&#39;t contradict this. A fixture builds a piece of test data and nothing else. What the guide avoids is shared code that hides the sections of a test, so that the reader can no longer see what was arranged, what was done and what was checked.&lt;/p&gt;
&lt;p&gt;With structured sections and a decent IDE, duplication is cheap to write and cheap to change. Refactor into a helper only when the helper is easier to understand than the code it replaces. For the same reason, shared &lt;code&gt;[SetUp]&lt;/code&gt; methods are avoided: they create hidden coupling between tests.&lt;/p&gt;
&lt;h2&gt;6. Tests are deterministic, except where they shouldn&#39;t care&lt;/h2&gt;
&lt;p&gt;The same inputs must produce the same results on every run. Anything nondeterministic, such as the current time, random values, generated IDs or static singletons, is put behind an interface and injected, so a test can freeze it. When code mixes pure calculation with reads from the outside world, it is split into a method that gathers inputs and a method that computes, and the pure one is tested directly.&lt;/p&gt;
&lt;p&gt;There is one deliberate nuance. Sometimes it is useful to &lt;em&gt;allow&lt;/em&gt; nondeterminism in something that should not affect the outcome. The shipment test above does this with its order id: any id should work, so it gets a new one on every run. If a test like that flakes, the flake is information: it exposes hidden coupling or a misunderstood contract. Fix the bug it reveals or tighten the contract, but don&#39;t just pin the value and move on.&lt;/p&gt;
&lt;h2&gt;7. Tests mirror production&lt;/h2&gt;
&lt;p&gt;Finding a test should never require a search. Test namespaces mirror production namespaces with a &lt;code&gt;.Tests&lt;/code&gt; suffix, and folders mirror namespaces. A test class is named after what it covers: &lt;code&gt;FileHandlerTests&lt;/code&gt; for the class, &lt;code&gt;FileHandlerHashingTests&lt;/code&gt; for one region of it (a &lt;code&gt;#region&lt;/code&gt; in C#), and &lt;code&gt;FileHandlerOpenTests&lt;/code&gt; for one method. The file name matches the class name.&lt;/p&gt;
&lt;p&gt;Tests should also be able to reach what they test without tricks. Where production code would normally reach for &lt;code&gt;private&lt;/code&gt; methods, it uses &lt;code&gt;internal&lt;/code&gt; ones instead, and the production project exposes its internals to the test assembly (with &lt;code&gt;InternalsVisibleTo&lt;/code&gt;, in C#). A &lt;code&gt;private&lt;/code&gt; method has no real value that an &lt;code&gt;internal&lt;/code&gt; one lacks, and the difference matters only to tests: they can be allowed to call internals directly, while private methods can only be reached through reflection and similar trickery.&lt;/p&gt;
&lt;h2&gt;8. Breaking the rules&lt;/h2&gt;
&lt;p&gt;The guide ends with a chapter on when to break it. Deviation is fine when it improves clarity or determinism for a case the standard shape can&#39;t express cleanly, as long as the reason is written down next to the code and revisited later. Most shortcuts trade a line saved today for ambiguity later: asserting on a &lt;code&gt;given*&lt;/code&gt; value directly, skipping &lt;code&gt;EXPECTATIONS&lt;/code&gt; because the expected value equals the input, or passing &lt;code&gt;mock.Object&lt;/code&gt; straight into the constructor. Each saves a moment for the writer and costs one for every reader after.&lt;/p&gt;
&lt;h2&gt;What to take from this&lt;/h2&gt;
&lt;p&gt;What I would carry into any codebase, in any language:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Optimize tests for the maintainer&lt;/strong&gt;, not the writer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Give every test the same shape&lt;/strong&gt;, with labelled sections in a fixed order, so reviewers read differences instead of structure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Name variables by role&lt;/strong&gt;, so a wrong assertion looks wrong.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test one branch per test and prove the branch was taken&lt;/strong&gt;, through logs, strict mocks or whatever your platform offers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer real objects and fixtures to mocks, and fakes to gnarly mocks.&lt;/strong&gt; Test your fixtures and fakes, and test the seams that mocks hide.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Duplicate setup before you abstract it.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inject anything nondeterministic&lt;/strong&gt;, and treat a flake in something that shouldn&#39;t matter as a finding.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make tests easy to find&lt;/strong&gt; by mirroring production structure, and easy to reach by making internals visible to them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Break the rules only on purpose&lt;/strong&gt;, and say why in the code.&lt;/li&gt;
&lt;/ol&gt;
</content>
  </entry>
  <entry>
    <title>There is no TODO</title>
    <link href="https://icelander.net/v1/processes/there-is-no-todo"/>
    <id>tag:icelander.net,2026:there-is-no-todo</id>
    <published>2026-11-01T00:00:00Z</published>
    <updated>2026-11-01T00:00:00Z</updated>
    <category term="processes" label="Processes"/>
    <summary>I deleted the folder called TODO and stopped trusting the backlog, because a backlog is a second map of the system that drifts by design. Knowledge-driven development keeps one map: documents that describe what the system should be, with every statement marked by whether it&#39;s true yet. It&#39;s an experiment, and it isn&#39;t battle-tested yet.</summary>
    <content type="html">&lt;p&gt;&lt;em&gt;Knowledge-driven development, part 1: the backlog is a grep&lt;/em&gt;&lt;/p&gt;&lt;p&gt;Thought September 2026, written October 2026. Processes.&lt;/p&gt;&lt;p&gt;On Friday, August 28, 2026, I deleted a directory called &lt;code&gt;TODO&lt;/code&gt;. Well, obviously I didn&#39;t delete it myself. I asked an agent to.&lt;/p&gt;
&lt;p&gt;It held 22 items, each one a small Markdown file describing something the system ought to do and didn&#39;t, or did and shouldn&#39;t. They were good items, carefully written and graded by severity from S1 down to S4. And every single one of them was in the wrong place.&lt;/p&gt;
&lt;p&gt;The 22 items now live in the nine documents that describe the capabilities they were about. The directory is gone. In its place is a test that fails the build if anyone creates a new one, or starts a comment in any source file with &lt;code&gt;TODO&lt;/code&gt;, &lt;code&gt;FIXME&lt;/code&gt;, &lt;code&gt;HACK&lt;/code&gt; or &lt;code&gt;XXX&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That was the small backlog. The big one was in Asana: more than 200 tasks, rotting away, with nobody reading them and nobody updating them. Most of them were out of date, of course, the way backlogs always are. One asked for a document to be updated with the details of a production issue. The document had since been rewritten from scratch, and the production system decommissioned. Tickets have a half-life. Nobody has ever measured it, because measuring it would mean reading the tickets.&lt;/p&gt;
&lt;p&gt;Deleting the folder is the whole method in miniature. The Asana tasks are the whole problem, and not in miniature.&lt;/p&gt;
&lt;p&gt;Knowledge-driven development is an experiment. I started it that Friday in August, and it isn&#39;t battle-tested yet. This series describes what I&#39;m doing and why.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. The backlog is a second map&lt;/h2&gt;
&lt;p&gt;Every software team I&#39;ve worked with has kept two descriptions of the system. One is the system itself, plus whatever documentation survived contact with it. The other is the backlog: tickets, epics, stories, a board with columns. The first describes what is. The second describes what should be, minus what is, as of whenever someone last updated it.&lt;/p&gt;
&lt;p&gt;These are two maps of the same territory, drawn by different people, for different readers, in different tools, and updated by different acts. Changing the code is one act. Closing the ticket is another. Only one of them is enforced by anything, and it isn&#39;t the ticket.&lt;/p&gt;
&lt;p&gt;So the maps drift. A ticket says a thing is broken that was fixed three sprints ago. A feature exists that no ticket ever mentioned, because someone built it on a Friday. The board says 80% done and the system says otherwise. &lt;a href=&quot;https://en.wikipedia.org/wiki/Map%E2%80%93territory_relation&quot;&gt;The map is not the territory&lt;/a&gt;, and the backlog is a second map, drawn from memory, of the first one.&lt;/p&gt;
&lt;p&gt;And the people drawing it are trying to look good and justify their paychecks.&lt;/p&gt;
&lt;p&gt;Two descriptions updated by two acts will diverge, however disciplined the people updating them are, and a description kept by the people it&#39;s used to judge will flatter them. The usual cure is more discipline anyway: grooming sessions, ticket hygiene, a definition of done that includes closing the ticket. Every team that has ever run a ticket tracker has paid for that, usually in meetings.&lt;/p&gt;
&lt;p&gt;Processes that aren&#39;t valued by the people who have to put in the effort feel like imprisonment. The prisoners escape, and the guards are left humiliated and confused.&lt;/p&gt;
&lt;p&gt;There&#39;s a scene in &lt;a href=&quot;https://en.wikipedia.org/wiki/The_Matrix&quot;&gt;The Matrix&lt;/a&gt; where a child bends a spoon just by looking at it, then tells Neo not to try bending it himself, because that&#39;s impossible. Instead, he should realize the truth: &amp;quot;There is no spoon.&amp;quot;&lt;/p&gt;
&lt;p&gt;The backlog is the spoon.&lt;/p&gt;
&lt;h2&gt;2. The backlog is a queue for hands you don&#39;t have&lt;/h2&gt;
&lt;p&gt;Drift is the first problem with a backlog. The second is that it no longer has a reason to exist.&lt;/p&gt;
&lt;p&gt;A backlog, and the sprint planning built on top of it, is a tool for managing work that isn&#39;t being done because there are no hands and brains to do it. Sometimes for years. The work waits in a queue, and every two weeks somebody grooms the queue.&lt;/p&gt;
&lt;p&gt;An agent can finish that work as fast as a developer can guide it and feed it tokens. In my case, the limiting factor should be the developer. It isn&#39;t, because my organization doesn&#39;t feed me enough tokens for that to be true. Organizations are comfortable paying salaries to add hands. They&#39;re not comfortable paying for tools that multiply those hands at a lower cost.&lt;/p&gt;
&lt;p&gt;Still, hands and brains were the bottleneck the backlog was built to manage, and they aren&#39;t any more. The bottleneck now is a line in a budget. Nobody has told the backlog.&lt;/p&gt;
&lt;p&gt;Given a serious development team, the backlog is pointless.&lt;/p&gt;
&lt;p&gt;So are the planning rituals around it. The entire design of a major new system could be implemented before the planning-and-cost-assessment phase is over. Building the thing is now faster than estimating it, which turns the cost assessment into a historical document. The result can be reviewed in a real environment, and adjusted to meet the stakeholders&#39; needs in the next phase.&lt;/p&gt;
&lt;p&gt;What that takes is a description of what the system is meant to be, and an account of how much of it is true yet.&lt;/p&gt;
&lt;h2&gt;3. Knowledge-driven development is one map&lt;/h2&gt;
&lt;p&gt;Knowledge-driven development removes the second description. What&#39;s left is one description, and it&#39;s the spec: everything the system wants to be in its absolute end state, as far as anyone currently knows it. It&#39;s written in the present tense, as if all of it were already true, and every statement carries a mark saying whether it is yet. A ⚠️ paragraph marks the gap between what the system should do by design and what has been built so far, written in the voice of the person who will use it, in the place they&#39;ll look for it. A ❌ paragraph is a bug report, in the place the bug will be found. A ✅ paragraph is a claim someone has verified, and the only kind you&#39;re allowed to build on.&lt;/p&gt;
&lt;p&gt;The description is split by audience, not by tool. The product owner gets the feature documents. The operator gets the operator&#39;s manual. The integrator gets the API contract. The maintainer gets all of those, plus the architecture: the things that really should be documented about a system. A class tree isn&#39;t one of them. Any IDE will draw you one of those out of the box.&lt;/p&gt;
&lt;p&gt;The specification, the backlog, the release notes and the onboarding material are the same set of facts, filtered for different readers. They always were. We just kept them in four places and paid four times to keep them roughly in sync, which we didn&#39;t.&lt;/p&gt;
&lt;p&gt;When I want to know every known bug in the system, I don&#39;t open a board. I ask Claude, and Claude runs this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;grep -rn &#39;❌&#39; documentation/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And when I want to know what&#39;s left to build:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;grep -rn &#39;⚠️&#39; documentation/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s the closest thing I have to a backlog, and it&#39;s a grep. It can&#39;t drift from the documentation, because it &lt;em&gt;is&lt;/em&gt; the documentation. Nothing on it is waiting for hands, either. It gets shorter as fast as I&#39;m allowed to feed the agents tokens.&lt;/p&gt;
&lt;p&gt;I call it knowledge-driven development on purpose. &amp;quot;Documentation&amp;quot; makes people think of the thing written after the code, by nobody, read by nobody, and out of date before the pull request merges. These documents are written before the code, read on every change, and they&#39;re the only description of the system that&#39;s allowed to exist. They&#39;re what the system knows about itself, including what it knows it can&#39;t do yet.&lt;/p&gt;
&lt;h2&gt;4. Files beat tickets&lt;/h2&gt;
&lt;p&gt;Agents don&#39;t read tickets. Not unless you ask them to, and even then a ticket is a poor way of communicating with one. Files are better.&lt;/p&gt;
&lt;p&gt;An agent&#39;s context is loaded from files. The implementing agents work from the documents the planner wrote, and the repository&#39;s agent instructions tell every agent to read the documents about whatever it&#39;s changing. A ⚠️ in the document about the capability being changed gets read whether anyone remembers it or not. A ticket about the same capability gets read when somebody remembers it exists, finds it, and points the agent at it, and what the agent gets then is a description written for a planning meeting, about code that may have changed since. Putting the requirement where the agent will read it is the difference between a rule that applies and a rule that applies when somebody remembers it.&lt;/p&gt;
&lt;p&gt;For a human team, a ticket tracker is a tolerable bad idea. Humans don&#39;t really remember the ticket they read on Monday when they&#39;re editing the file on Thursday, but they can usually find it again, and they can claim they&#39;re working on it. What they find is rarely worth the search. In most teams, a human-written ticket is very often nothing but a subject line, or a single paragraph if you&#39;re lucky. It doesn&#39;t, in any form or fashion, convey the intent and context needed to resolve it correctly.&lt;/p&gt;
&lt;p&gt;Most &lt;code&gt;TODO&lt;/code&gt; items and most tickets are of no interest to most of the organization anyway. A ticket tracker is a road atlas of the entire country, kept in another building, where every reader cares about one page and it&#39;s a different page for each of them. A ⚠️ in the document the agent is reading is a sign on the corner where you&#39;re standing.&lt;/p&gt;
&lt;p&gt;Agents write nearly all of my code and nearly all of my tests. They read the signs every time, because the signs are in the documents they read before they work. They&#39;d read the atlas too, if I sent them to the other building.&lt;/p&gt;
&lt;p&gt;I don&#39;t.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;There is no separate backlog. The 22 items that used to sit in a folder called &lt;code&gt;TODO&lt;/code&gt; are now sentences in the documents that describe what they&#39;re about, each one marked, each one findable by grep, each one in the place an agent will read it at the moment it matters. So are the &lt;code&gt;TODO&lt;/code&gt; comments that used to be scattered through the source files, each one moved into the document that owns the code it sat in. Whatever wisdom the Asana tasks contained has been distilled and stored in the repository as well. (Same caveat.) The tasks that were too detailed for Asana are gone from it. What&#39;s left there are the major work items, and each one refers to the document that describes it.&lt;/p&gt;
&lt;p&gt;My agents do still visit the other building now and then, to update Asana for the project managers and product owners who live there. I hear it&#39;s lovely this time of year. They go there to write, not to read. Asana is still a second map. It&#39;s just not the one the code is built from.&lt;/p&gt;
&lt;p&gt;The rest of this series is how that works in practice: &lt;a href=&quot;/v1/processes/seven-marks-three-monkeys&quot;&gt;the marks&lt;/a&gt;, &lt;a href=&quot;/v1/processes/the-plan-is-where-the-work-lives&quot;&gt;how I run a feature through them&lt;/a&gt;, &lt;a href=&quot;/v1/processes/what-it-replaces&quot;&gt;what they replace&lt;/a&gt;, and &lt;a href=&quot;/v1/processes/where-it-breaks&quot;&gt;where they break&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The folder is gone, and the build will fail if it comes back.&lt;/p&gt;
&lt;p&gt;There is no TODO.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Four marks</title>
    <link href="https://icelander.net/v1/processes/seven-marks-three-monkeys"/>
    <id>tag:icelander.net,2026:seven-marks-three-monkeys</id>
    <published>2026-11-01T00:00:00Z</published>
    <updated>2026-11-01T00:00:00Z</updated>
    <category term="processes" label="Processes"/>
    <summary>Knowledge-driven development has four marks, saying whether a statement is verified, missing, broken or blocked. Each one goes on the smallest statement it&#39;s about, so the dragons are only where they live.</summary>
    <content type="html">&lt;p&gt;&lt;em&gt;Knowledge-driven development, part 2: here be dragons, and nowhere else&lt;/em&gt;&lt;/p&gt;&lt;p&gt;Thought September 2026, written October 2026. Processes.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Knowledge-driven development is an experiment of mine, and it isn&#39;t battle-tested yet. &lt;a href=&quot;/v1/processes/there-is-no-todo&quot;&gt;Part 1&lt;/a&gt; says where it started.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Medieval cartographers almost never wrote &amp;quot;here be dragons&amp;quot; on their maps. As far as anyone can tell, &lt;a href=&quot;https://en.wikipedia.org/wiki/Here_be_dragons&quot;&gt;exactly one surviving globe&lt;/a&gt; does it, in Latin, somewhere off the coast of Asia. Everywhere else, the unknown was just left blank, or decorated with a sea monster and a shrug.&lt;/p&gt;
&lt;p&gt;That&#39;s still one globe more than most backlogs. A ticket tracker tells you what someone intended to build. It doesn&#39;t tell you, standing in a particular corner of the system, whether the ground under your feet has been surveyed.&lt;/p&gt;
&lt;p&gt;The marks started with a few hundred Asana tasks. I was looking at them, realized they were doing more harm than good, and decided I wanted a format that was more concise, and of value in other ways as well.&lt;/p&gt;
&lt;p&gt;Knowledge-driven development is mostly a vocabulary for that. It has four marks.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. The four marks&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mark&lt;/th&gt;
&lt;th&gt;Means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Implemented and verified.&lt;/strong&gt; The behavior exists and someone has seen its tests pass.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⚠️&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Not implemented.&lt;/strong&gt; The sentence describes the intended behavior. Nothing behind it works yet. This is the backlog item.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Implemented but broken.&lt;/strong&gt; A known defect.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⛔&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Blocked.&lt;/strong&gt; The gap marked just before it can&#39;t be closed from here. Always follows a ⚠️ or ❌, never stands alone, and always says what would unblock it.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In map terms: ✅ is surveyed land. ⚠️ is here be dragons. ❌ is the bridge that&#39;s on the map and isn&#39;t in the river. ⛔ is a road closed by somebody else&#39;s council, with a sign saying what has to happen before it reopens.&lt;/p&gt;
&lt;p&gt;The mark goes on the smallest statement that is wholly true or wholly false. A section heading carrying one ⚠️ for eleven paragraphs, nine of which work, is worse than no mark at all, because it makes the two broken paragraphs unfindable. A bullet takes the mark at the start of the bullet. A paragraph takes it at the start of the sentence that&#39;s untrue, not at the start of the paragraph.&lt;/p&gt;
&lt;h2&gt;2. The mark is the whole commentary&lt;/h2&gt;
&lt;p&gt;You write the sentence as the system &lt;em&gt;should&lt;/em&gt; be, in the present tense, in the detail someone would need to use it. You mark it. Then you stop. You don&#39;t explain the mark. No &amp;quot;what it does instead&amp;quot;, no &amp;quot;the customer currently sees&amp;quot;, no account of how you found out. The glyph already says &lt;em&gt;this sentence is not true yet&lt;/em&gt;. A paragraph restating that in prose turns a specification back into a bug report, which is the thing the page replaced.&lt;/p&gt;
&lt;p&gt;The instinct runs the other way. A page that just cost somebody a day of debugging wants to &lt;em&gt;say so&lt;/em&gt;. It mustn&#39;t. The incident goes in the changelog entry that introduced the mark, and the diagnosis goes in the pull request that fixes it. What the page owes its reader is the sentence they can build against, and one glyph telling them whether to trust it.&lt;/p&gt;
&lt;p&gt;An ⚠️ that says &amp;quot;not implemented&amp;quot; is worthless. An ⚠️ that describes the intended behavior is a specification a cheaper agent can implement from. Its quality bar is that an agent can build from it without asking much. The mark is metadata. The paragraph is the deliverable.&lt;/p&gt;
&lt;h2&gt;3. ✅ is expensive on purpose&lt;/h2&gt;
&lt;p&gt;✅ may only be written by someone who has read the code and seen the tests pass. A plan saying so doesn&#39;t count, and neither does an agent reporting success or a pull request merging.&lt;/p&gt;
&lt;p&gt;That&#39;s the strictest mark in the convention, because ✅ is the one readers build on.&lt;/p&gt;
&lt;h2&gt;4. Graduation&lt;/h2&gt;
&lt;p&gt;When a document contains no ⚠️ and no ❌, every ✅ in its body is deleted. The status link at the top is replaced by one line, carrying the page&#39;s only green check:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ This component is fully implemented, tested and free of known defects.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A page covered in green ticks is a page that has stopped carrying information. The ticks only ever existed to make the gaps legible, so when the gaps go, the ticks go with them. Nobody stamps &amp;quot;surveyed&amp;quot; on every hill of a finished map. One stamp on the cover will do.&lt;/p&gt;
&lt;p&gt;Graduation is what keeps the marked part of the corpus small enough to stay true. A convention that only ever adds marks becomes wallpaper, and nobody reads wallpaper. A fully implemented, defect-free document carries one mark, by its status line, which is what most documents should eventually look like, and why a page with marks in its body is worth stopping on.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Here be dragons, then. But only where there are dragons, only on the square of the map where they live, and with a description of the dragon detailed enough that someone could build it.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>The plan is where the work lives</title>
    <link href="https://icelander.net/v1/processes/the-plan-is-where-the-work-lives"/>
    <id>tag:icelander.net,2026:the-plan-is-where-the-work-lives</id>
    <published>2026-11-01T00:00:00Z</published>
    <updated>2026-11-01T00:00:00Z</updated>
    <category term="processes" label="Processes"/>
    <summary>My plan for a feature is one prompt. A planning agent writes the documentation of the system as it will be, marked as not yet true, and the agents that build it turn the marks green in the same commits as the code. The review is the marks that moved.</summary>
    <content type="html">&lt;p&gt;&lt;em&gt;Knowledge-driven development, part 3: twenty lines of Markdown, two thousand lines of trust&lt;/em&gt;&lt;/p&gt;&lt;p&gt;Thought September 2026, written October 2026. Processes.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Knowledge-driven development is an experiment of mine, and it isn&#39;t battle-tested yet. &lt;a href=&quot;/v1/processes/there-is-no-todo&quot;&gt;Part 1&lt;/a&gt; says where it started.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here is the entire prompt I give a planning agent when I want a feature built:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Plan how to implement this feature, and write it up using knowledge-driven development.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It goes to a top-tier model at high effort, with whatever description of the feature I&#39;ve got. That&#39;s usually a paragraph. Sometimes it&#39;s a sentence. I&#39;ve written longer commit messages.&lt;/p&gt;
&lt;p&gt;What comes back is the documentation of the system as it will be once the feature exists, written in the present tense, with almost every new sentence marked ⚠️. That&#39;s the plan.&lt;/p&gt;
&lt;p&gt;It&#39;s also the best way I&#39;ve found to keep a fleet of agents pointed in the same direction.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. What the planning agent writes&lt;/h2&gt;
&lt;p&gt;The agent doesn&#39;t produce a list of steps. It edits the documents that already describe the system, or creates the ones that should exist, and describes the feature &lt;em&gt;in them&lt;/em&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The feature document gets a section describing the new capability from the point of view of whoever will use it, statement by statement, each one ⚠️.&lt;/li&gt;
&lt;li&gt;The user journeys that pass through the feature get new steps, each one ⚠️.&lt;/li&gt;
&lt;li&gt;The subsystem documents get the new ways things can fail: what goes wrong, what it looks like from the outside, and what an operator does about it. All ⚠️.&lt;/li&gt;
&lt;li&gt;The API contract gets the new operations, with their request and response shapes, each one marked ⚠️.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If something can&#39;t be built from where we are (it needs a decision from someone outside, a piece of hardware that isn&#39;t on my desk, a third party that hasn&#39;t shipped), the agent writes it as ⚠️⛔ and says what would unblock it. That&#39;s how blockers reach me: as a sentence in the document that owns them.&lt;/p&gt;
&lt;p&gt;The quality bar for those ⚠️ paragraphs is that an agent can build from them without asking much. They don&#39;t need to be perfect. The agent is fast, and I can review what it built fairly quickly afterwards and correct its direction.&lt;/p&gt;
&lt;h2&gt;2. What the work orders used to be&lt;/h2&gt;
&lt;p&gt;I didn&#39;t always do it this way. Until recently I had agents write plans as work orders for the fleet: a list of self-contained steps, each with its own definition of done, with the parallel steps marked and the riskiest step first.&lt;/p&gt;
&lt;p&gt;They worked. They were also a second map.&lt;/p&gt;
&lt;p&gt;A work order describes a sequence of changes. Once the changes are made, it describes nothing. It becomes a historical document about a plan, sitting next to a system that has since moved on. The next agent to touch the feature doesn&#39;t read it, because nothing points to it, and if it did read it, the work order would tell it what someone intended three weeks ago, not what&#39;s true now.&lt;/p&gt;
&lt;p&gt;A knowledge-driven plan doesn&#39;t expire, because it&#39;s the description of the system, and implementing it means making its sentences true. When the last ⚠️ in a feature turns into ✅, nothing gets archived. The plan is the documentation now, which is what it was all along.&lt;/p&gt;
&lt;p&gt;The work orders are gone. I don&#39;t miss them.&lt;/p&gt;
&lt;h2&gt;3. What the implementing agents do&lt;/h2&gt;
&lt;p&gt;Every agent that comes after the planner treats the design documents as its primary guidance, ahead of my original paragraph, the chat history or any ticket.&lt;/p&gt;
&lt;p&gt;That works because of how the documents reach the agents. The implementing agents are handed the documents the planner wrote, and the repository&#39;s agent instructions tell every agent to read the documents about whatever it&#39;s changing. Nobody has to remember to point the agent at them.&lt;/p&gt;
&lt;p&gt;An implementing agent works like a Mars rover. When NASA&#39;s &lt;a href=&quot;https://www-robotics.jpl.nasa.gov/media/documents/SAPP_paper.pdf&quot;&gt;Spirit and Opportunity&lt;/a&gt; had no attitude estimate worth trusting, they worked one out from scratch: the accelerometers told them which way was down, and a camera searched the sky for the sun to tell them which way they were facing. &lt;a href=&quot;https://www.jpl.nasa.gov/images/perseverances-sol-200-auto-nav-drive&quot;&gt;Perseverance&lt;/a&gt; drives the same way. It keeps imaging the terrain around it, maps it, and chooses its own path to the destination, around whatever rocks it finds.&lt;/p&gt;
&lt;p&gt;None of them needs the story so far. They look around, work out where they are, and go. My agents start from the same nothing. They don&#39;t need the chat history, the plan so far or my original paragraph. They read the documents, find the ⚠️ marks, and start building. It works brilliantly.&lt;/p&gt;
&lt;p&gt;The one rule they must follow is that the mark moves in the same commit as the code: shipping a capability turns its ⚠️ into ✅ in the same pull request.&lt;/p&gt;
&lt;p&gt;Defects don&#39;t come out of the commits. They turn up in user testing, or arrive in a chat message, an email, an Asana task or a phone call. More and more often I find them myself, because my systems have good observability: I ask an agent what went wrong since yesterday, and then I tell it to fix it. Wherever a defect comes from, the usual answer is the same. Claude diagnoses it and fixes it, and the defect is gone before anyone thinks of writing it down. Defects are short-lived in an AI-driven world, tokens allowing.&lt;/p&gt;
&lt;p&gt;When a fix has to wait, I ask Claude to update the knowledge documents instead. That&#39;s what a ❌ is for: a known defect that&#39;s still there.&lt;/p&gt;
&lt;p&gt;This also means the implementing agents can be cheaper than the planner. The planner did the judgment: what the feature &lt;em&gt;is&lt;/em&gt;, where it belongs, how it fails. The implementers are translating well-specified sentences into code and tests, and that&#39;s work a smaller model at lower effort does well.&lt;/p&gt;
&lt;h2&gt;4. The backlog and the sprint board&lt;/h2&gt;
&lt;p&gt;I don&#39;t have a separate backlog. My project manager does have a sprint board, and I keep it up to date. Well, Claude does. But it&#39;s a second map, and however well it&#39;s kept, it&#39;s never going to be a holistic picture of what&#39;s actually going on in the code base.&lt;/p&gt;
&lt;p&gt;My own sprint board is a grep that Claude runs for me. The backlog is every ⚠️ in the repository. The sprint is the ⚠️ marks in the documents the planning agent just wrote. Progress is the count of those marks going down. Something blocked is ⚠️⛔ and says what it&#39;s waiting on. A defect that&#39;s waiting for a fix is a ❌, in the place the defect lives.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;grep -rn &#39;⚠️&#39; documentation/Features/my-new-feature.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It updates itself in the same commits as the code, which is more than anyone can say for the other one.&lt;/p&gt;
&lt;h2&gt;5. How I review&lt;/h2&gt;
&lt;p&gt;A feature pull request built by agents can easily be two thousand lines of code and tests. I can&#39;t review two thousand lines of anything with any real attention, and nobody else can either, whatever they put in their performance review.&lt;/p&gt;
&lt;p&gt;So I don&#39;t start there. I start with two short things: the delta to the knowledge files, and the change logs.&lt;/p&gt;
&lt;p&gt;That diff is usually short. Twenty lines, maybe forty. It tells me, in plain sentences, what the agent claims to have done: these ⚠️ became ✅, this ❌ is a defect left for later, this one is now ⚠️⛔ and says what it&#39;s waiting on. Every mark that moved is a claim, and every claim names the behavior it&#39;s about.&lt;/p&gt;
&lt;p&gt;The change logs come from a rule my agents follow: every prompt that changes the code base gets a change log file. It says what I asked for and how the agent interpreted it, and, where it matters, what the change needs: how to test it, data to migrate, compatibility issues. I read those carefully, because they tell me whether the agent misunderstood me.&lt;/p&gt;
&lt;p&gt;In a forest of agent-made changes, those two are the most concise way I know to see what a pull request thinks it&#39;s changing. Sometimes that&#39;s enough to show that the agent got me wrong. One decided to write a data migration for something I could have done by opening a web page and toggling a feature flag.&lt;/p&gt;
&lt;p&gt;Then the claims get checked, because a ✅ means someone read the code and saw the tests pass, and agents report success the way a teenager reports that their room is clean.&lt;/p&gt;
&lt;p&gt;I don&#39;t check them myself. That would be too much work. I ask another agent to do it.&lt;/p&gt;
&lt;p&gt;Sending one teenager to inspect another teenager&#39;s room sounds like a terrible idea. It works because this one has never met the first one.&lt;/p&gt;
&lt;p&gt;Every pull request that ships a capability carries a documentation hunk. That&#39;s a feature: it&#39;s the only part of the diff written for the reviewer. A pull request that changes behavior and moves no mark is either not visible to anyone, or has left the documentation lying. Either way, it&#39;s the first question the review asks.&lt;/p&gt;
&lt;p&gt;Reading the knowledge delta and the change logs first turns review from reading two thousand lines of code and guessing what they were for, into reading a few dozen lines of claims and intent, and handing the checking to an agent. That&#39;s a job I can actually do.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The plan isn&#39;t a document that gets written and forgotten. It&#39;s where the work lives.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>What it replaces</title>
    <link href="https://icelander.net/v1/processes/what-it-replaces"/>
    <id>tag:icelander.net,2026:what-it-replaces</id>
    <published>2026-11-01T00:00:00Z</published>
    <updated>2026-11-01T00:00:00Z</updated>
    <category term="processes" label="Processes"/>
    <summary>Knowledge-driven development replaces the TODO comment, the backlog, plans, separate specs and hand-written release notes, because each was a second description of the system that drifted. It keeps the things that record the past or can be run as tests.</summary>
    <content type="html">&lt;p&gt;&lt;em&gt;Knowledge-driven development, part 4: a short obituary for the ticket&lt;/em&gt;&lt;/p&gt;&lt;p&gt;Thought September 2026, written October 2026. Processes.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Knowledge-driven development is an experiment of mine, and it isn&#39;t battle-tested yet. &lt;a href=&quot;/v1/processes/there-is-no-todo&quot;&gt;Part 1&lt;/a&gt; says where it started.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Most of what knowledge-driven development does, it does by deleting something. The &lt;code&gt;TODO&lt;/code&gt; folder went first. The work orders went next. Then my backlog, my sprint board and most of what a ticket tracker is for.&lt;/p&gt;
&lt;p&gt;They were all designed for a team of humans who remember things between Monday and Thursday. That&#39;s not the team I have any more. But each of them did something useful, and the useful part had to go somewhere when the rest went in the bin.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. The ticket tracker&lt;/h2&gt;
&lt;p&gt;A ticket tracker keeps the intent outside the repository, which is why it &lt;a href=&quot;/v1/processes/there-is-no-todo&quot;&gt;drifts&lt;/a&gt;. For agents, it&#39;s worse than drift. A ticket is invisible at the moment of editing. The agent changing the code never sees it, however carefully someone wrote it.&lt;/p&gt;
&lt;p&gt;And it usually wasn&#39;t written carefully. A typical ticket looks like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Export fails sometimes&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Accounting says some nights the export is missing. Can we look at this?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That&#39;s a subject line and a shrug. It doesn&#39;t say what the export should do, what &amp;quot;missing&amp;quot; means, or what anyone has already tried. Hand it to an agent, and the agent will fix &lt;em&gt;something&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The same problem, in the knowledge document that describes the export:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;❌ When the accounting system is unavailable at 02:00, the export retries every 15 minutes, and the night&#39;s records are sent once it&#39;s back.&lt;/p&gt;
&lt;p&gt;⚠️ If the export still hasn&#39;t succeeded by 06:00, the operator on call gets an alert.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The ❌ says the retry exists and doesn&#39;t work. The ⚠️ says the alert hasn&#39;t been built yet. Neither explains itself, because the sentence is the specification, and an agent can build from it. Both sit in the page an agent reads before it touches the export.&lt;/p&gt;
&lt;p&gt;On my team, &lt;em&gt;who&lt;/em&gt; is me and a fleet of agents, and &lt;em&gt;when&lt;/em&gt; is now. So the tracker doesn&#39;t own anything the code depends on. My agents still keep Asana up to date for the project managers and product owners who use it, but nothing in the repository ever reads it back. It&#39;s a second map, and they only ever write to it.&lt;/p&gt;
&lt;p&gt;The project managers loved the status updates, which are current and tailored to what they need. Then they had a heart attack over the number of tasks the agents had added to the current sprint. Now the agents keep only the major work items in the sprint. The countless microtasks go into the knowledge documents, not into Asana, and the Asana task refers to the document instead of repeating it.&lt;/p&gt;
&lt;p&gt;Many of those microtasks aren&#39;t worth keeping at all: an agent can work out the small steps for itself when it implements the feature. And once a microtask is done, the code is the record of it.&lt;/p&gt;
&lt;h2&gt;2. The &lt;code&gt;TODO&lt;/code&gt; comment&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;TODO&lt;/code&gt; is a mark that failed to say anything. It records that somebody was unhappy at that line, and nothing else: not whether the code works, whether it&#39;s broken, what it should do instead or who could fix it.&lt;/p&gt;
&lt;p&gt;Like plenty of other programmers, I&#39;ve stumbled over this periodically since the stone age of computing: a few hours spent debugging yet another production problem, and then the &lt;code&gt;TODO&lt;/code&gt; comment that had identified it all along.&lt;/p&gt;
&lt;p&gt;So every &lt;code&gt;TODO&lt;/code&gt; comment in the source moved into the knowledge document that owns its code, as a ⚠️ or ❌ that says what the comment never did. Before:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// TODO: handle timeouts
var result = await accounting.SendAsync(batch);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After, in the export&#39;s document:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;❌ When the accounting system doesn&#39;t answer within 30 seconds, the export abandons the attempt and schedules a retry.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The comment knew there was a problem, and kept it to itself. The sentence says what the system should do about it, in the place someone will read it.&lt;/p&gt;
&lt;p&gt;The knowledge-driven vocabulary is &lt;code&gt;TODO&lt;/code&gt; with a type system and a reader in mind. ⚠️ says it isn&#39;t built and describes what it should be. ❌ says it&#39;s built wrong and states the correct behavior. ⛔ says why it can&#39;t be fixed from here, and what would unblock it.&lt;/p&gt;
&lt;p&gt;So &lt;code&gt;TODO&lt;/code&gt; isn&#39;t discouraged. It&#39;s a build failure.&lt;/p&gt;
&lt;h2&gt;3. Plans and work orders&lt;/h2&gt;
&lt;p&gt;My agents used to write work orders: numbered steps, each with its own definition of done, the riskiest first and the parallel ones marked. A step looked like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Step 4 of 7.&lt;/strong&gt; Add retry logic to &lt;code&gt;ExportJob&lt;/code&gt;: every 15 minutes until 06:00. Done when the unit tests pass. Can run in parallel with step 5.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The same intent, written the knowledge-driven way:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ When the accounting system is unavailable, the export retries every 15 minutes until 06:00.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The step was true for one afternoon. Once it was done, it described a piece of history that nobody would read again. The sentence stays true for as long as the export works that way, and the day someone changes the retry interval, the sentence changes with it.&lt;/p&gt;
&lt;p&gt;The ordering and the notes about what can run in parallel went too. The implementing agents work those out from the documents, like everything else, which is the subject of &lt;a href=&quot;/v1/processes/the-plan-is-where-the-work-lives&quot;&gt;part 3&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;4. Specs and README-driven development&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://tom.preston-werner.com/2010/08/23/readme-driven-development.html&quot;&gt;README-driven development&lt;/a&gt; was the right idea at the wrong scale. A README is one document for one audience: the person deciding whether to use the thing. It says nothing about how the system fails, how to operate it, or what&#39;s half built. It covers the front door. Knowledge-driven development is the same idea applied to every audience at once, with marks, so the documents survive past version 0.1.&lt;/p&gt;
&lt;p&gt;Spec-driven development has the opposite problem. The spec is a separate artifact, read once by the agent that implements it and never again. Six months later, the spec says what the feature was meant to be in March, and the code says what it became. Nobody reconciles the two, because nobody opens the spec. In knowledge-driven development, the spec &lt;em&gt;is&lt;/em&gt; the manual, and it keeps being read, because it&#39;s what people and agents open to find out how the system works. Being read is the only thing that has ever kept a document current.&lt;/p&gt;
&lt;h2&gt;5. Release notes and onboarding&lt;/h2&gt;
&lt;p&gt;These fall out for free. The release notes for a change are the marks that moved. For the export fix above, they&#39;re the diff to one page:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ When the accounting system doesn&#39;t answer within 30 seconds, the export abandons the attempt and schedules a retry.&lt;/p&gt;
&lt;p&gt;✅ When the accounting system is unavailable at 02:00, the export retries every 15 minutes, and the night&#39;s records are sent once it&#39;s back.&lt;/p&gt;
&lt;p&gt;⚠️ If the export still hasn&#39;t succeeded by 06:00, the operator on call gets an alert.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Two fixed, one still to build. Nobody had to write that up, and nobody had to check that the write-up matched the code.&lt;/p&gt;
&lt;p&gt;Onboarding is reading the documents for the part of the system you&#39;re about to touch. They&#39;re the only description of the system there is, and they say what doesn&#39;t work.&lt;/p&gt;
&lt;p&gt;A new engineer, human or otherwise, doesn&#39;t have to learn which parts of the wiki to trust. They can grep for the parts not to.&lt;/p&gt;
&lt;h2&gt;6. What stays&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions&quot;&gt;Architecture decision records&lt;/a&gt; and the changelog stay. They record the past, and the past doesn&#39;t drift. They never get marks, because a ✅ in a document about the past would be a claim about the present in the wrong place.&lt;/p&gt;
&lt;p&gt;Executable specifications stay, and win where they apply. A &lt;a href=&quot;https://docs.python.org/3/library/doctest.html&quot;&gt;doctest&lt;/a&gt; or a &lt;a href=&quot;https://cucumber.io/docs/gherkin/&quot;&gt;Gherkin&lt;/a&gt; scenario can&#39;t drift, because it&#39;s a test. Where a statement can be a test, make it a test and drop the mark. Knowledge-driven development is for the rest: things that aren&#39;t executable yet, things that are broken, things about people rather than code.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www-cs-faculty.stanford.edu/~knuth/lp.html&quot;&gt;Literate programming&lt;/a&gt; was never in the running. It needs its own toolchain, and nobody maintains the prose half.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Everything I deleted was a second description of the system, kept somewhere other than the system and updated by some act other than changing the code. All of it drifted, and the people it was written for stopped reading it.&lt;/p&gt;
&lt;p&gt;The ticket had a good run. It just never had a reader who mattered as much as the agent that couldn&#39;t see it.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Where it breaks</title>
    <link href="https://icelander.net/v1/processes/where-it-breaks"/>
    <id>tag:icelander.net,2026:where-it-breaks</id>
    <published>2026-11-01T00:00:00Z</published>
    <updated>2026-11-01T00:00:00Z</updated>
    <category term="processes" label="Processes"/>
    <summary>Where knowledge-driven development breaks: marks pile up when the token budget runs out, a wrong mark is worse than none, and marks go stale like any other document. Agents check the marks in review and before anyone trusts an old page, and where a rule has no build gate, an agent checks it instead, tokens allowing.</summary>
    <content type="html">&lt;p&gt;&lt;em&gt;Knowledge-driven development, part 5: a wrong map is worse than a blank one&lt;/em&gt;&lt;/p&gt;&lt;p&gt;Thought September 2026, written October 2026. Processes.&lt;/p&gt;&lt;p&gt;The thing about knowledge-driven development that irritates me most is the marks that don&#39;t go away. A ⚠️ or a ❌ is meant to dissipate fast. Mine pile up, and the only reason is that I don&#39;t have the token budget to clean my desk.&lt;/p&gt;
&lt;p&gt;The list below is short partly because the method is young. I&#39;ve been running it for a couple of months, not years. It isn&#39;t battle-tested yet, and the problems I haven&#39;t met yet aren&#39;t on it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. A wrong mark is worse than no mark&lt;/h2&gt;
&lt;p&gt;A ✅ next to something nobody ran is a lie that the next reader builds on. That would kill the method, because ✅ is the mark people trust.&lt;/p&gt;
&lt;p&gt;A blank map tells you to be careful. A wrong map tells you to walk off the cliff with confidence.&lt;/p&gt;
&lt;p&gt;A false ✅ has never bitten me. I don&#39;t let the documents and the code drift that far apart, and I&#39;m a stickler for unit tests and branch coverage. The first defense is that ✅ is &lt;a href=&quot;/v1/processes/seven-marks-three-monkeys&quot;&gt;expensive to write&lt;/a&gt;. No build can check a ✅ on a paragraph about how an operator handles a jammed door. Nobody&#39;s coverage tool has an opinion about doors.&lt;/p&gt;
&lt;p&gt;An agent does, if you ask it. Checking the marks is part of every &lt;a href=&quot;/v1/processes/the-plan-is-where-the-work-lives&quot;&gt;review&lt;/a&gt;: the agent that checks the claims reads the code behind each mark that moved. A wrong mark only gets serious when nobody updates the document, and the re-review is there to make sure that never happens.&lt;/p&gt;
&lt;p&gt;So I don&#39;t take an old document seriously until it&#39;s been checked. When I want to know a partially implemented feature in detail, and the page describing it hasn&#39;t been touched in a while, I have an agent give it a once-over against the code first. Then I read it.&lt;/p&gt;
&lt;h2&gt;2. Marks go stale&lt;/h2&gt;
&lt;p&gt;Marks don&#39;t rot on their own. They go stale the way any document does: the code moves and the page doesn&#39;t. With several agents working on parallel branches, the author of a change often doesn&#39;t know which pages describe the thing they touched. A stale ⚠️ against a capability that now exists sends the next agent off to build it twice.&lt;/p&gt;
&lt;p&gt;There&#39;s no need for a mechanical fix. Ask an agent to review the page, on request or on a schedule. Mechanical checks of that kind break anyway.&lt;/p&gt;
&lt;h2&gt;3. One kind of document isn&#39;t enough&lt;/h2&gt;
&lt;p&gt;&amp;quot;Upgrade to the next framework version&amp;quot; belongs to none of the capability documents. Neither does &amp;quot;the build is slow&amp;quot; or &amp;quot;the test runner leaks file handles&amp;quot;. Work that isn&#39;t &lt;em&gt;about&lt;/em&gt; a capability has no natural place in them.&lt;/p&gt;
&lt;p&gt;That&#39;s why knowledge-driven development needs more than one type of document: capabilities, features, requirements, architecture, the build, operations, and so forth. The framework upgrade becomes a page in the architecture documents, describing the system on the new version, marked ⚠️ until it&#39;s true. The slow build becomes a page describing the build as it should be. Same marks, same greps, same rules.&lt;/p&gt;
&lt;p&gt;Adding a type is cheap. An agent reverse-engineers the current state of that area into a knowledge document, and a human reviews it. The real decision is which types are worth having in your system.&lt;/p&gt;
&lt;h2&gt;4. It doesn&#39;t schedule anything&lt;/h2&gt;
&lt;p&gt;A document says what&#39;s missing. It never says when it&#39;ll be done or who owns it, and it isn&#39;t supposed to.&lt;/p&gt;
&lt;p&gt;In an ideal world, I wouldn&#39;t schedule anything. I&#39;d throw agent tokens at every ⚠️, and the problems would disappear. In the real world, I schedule them against a meagre token budget. When I do, I create a task in Asana, and the task points at the knowledge document. The task says when. The document says what, and whether it&#39;s true yet.&lt;/p&gt;
&lt;h2&gt;5. Gates, or an agent&lt;/h2&gt;
&lt;p&gt;A convention maintained by attention decays at the rate attention decays. A few rules earn a build failure. The rule against &lt;code&gt;TODO&lt;/code&gt; was written down, and needed one.&lt;/p&gt;
&lt;p&gt;When I wrote the specification for the gates, there were seven of them: the coverage cross-check, the test-location gate, the API contract scan, the vocabulary gate, the deferred-work gate, the orphan-⛔ check and the graduation check. One of them existed. The rest were described in the present tense and marked ⚠️, which is to say the enforcement spec was written using the method it enforces.&lt;/p&gt;
&lt;p&gt;The missing ones aren&#39;t serious. Where a rule has no gate, I ask an agent to check the documents against it, or to do the work the rule was protecting, and it does. If I have the tokens.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;A spec nobody maintains is a lie with a schema. A knowledge document nobody maintains is the same lie with a green tick on it. The difference is that you can ask an agent to check this one before you believe it.&lt;/p&gt;
</content>
  </entry>
</feed>

