← Blog

The software factory is a control problem, too

· 32 min read

AI engineeringCoding agentsSoftware factoryControl loops.NET

Two engineering write-ups from this year keep getting quoted as proof that the software factory has arrived. In September, GitHub described moving the Copilot agent runtime from TypeScript to Rust: about 430,000 lines of TypeScript in, 832,378 lines of production Rust out, 128 pull requests, roughly fourteen and a half weeks, done primarily by a single developer, about $120,000 in tokens. In July, Microsoft shipped TypeScript 7 – the compiler ported from TypeScript to Go, with full builds typically 8 to 12 times faster. Add Bun's move from Zig to Rust, which I went through in the loop engineering post, and the headline writes itself: agents rebuild whole products now.

I read the same three stories differently. None of them shows that a model can be left alone with a codebase. All three show what gets built when something deterministic says, every few minutes, whether the work is still right: a compiler, and a test suite that existed before the work started. And the one that isn't told as an agent story at all – the TypeScript team's own posts describe a methodical port and don't say agents wrote it – is the most useful of the three, because it shows the recipe is older than the agents.

That is the link to Loop engineering is a control problem. A loop is one sensor, one controller and one actuator. A software factory is what you get when every ticket goes through loops like that and they all read from the same memory. What follows is how I would build one for an enterprise team: a design and an opinion, resting on those three cases and on the earlier posts here.

The thesis: a software factory is not a bigger agent. It is a pipeline of control loops over one shared memory, and it is exactly as good as its sensors. It works where every station has a deterministic check the agent cannot edit, the output is a small pull request, and a named person answers for what ships. Take the sensors away and you still have a factory – one that produces unmeasured code at industrial pace. Take the person away and nobody answers for it: responsibility is the one thing a factory can't produce, and it stays with a human.
on a schedule: performance · security · clean-up · docsmodelbehind an adaptersoftware factorycontextrepo + rules fileknowledge basethe why · runbooksskillshow we do X hereshared memorytriggersticket · alert · cronsandboxagents run hereagents – one per stationplanbuildreviewSlackTeamspeople askfrom chatpull requestsmall · reviewedreview lessonsworth keepingengineersUX / designersproduct owneruserengineers readand mergehumans inthe loopThey still talk to each other. The factory replaces none of those conversations.
Fig. 1 · The factory as I draw it. Inside: the shared memory, the triggers, a sandbox the agents run in, and a model behind an adapter. Outside: the people. One thing leaves the building – a pull request – and what review teaches flows back in.

An old word, and two things it means now

The word is older than most of the people using it. Hitachi opened its Software Works in 1969 and was the first company to call the thing a software factory. System Development Corporation published "The Software Factory" in 1975. Microsoft's version, Software Factories, was a 2004 book about assembling applications from patterns, models, frameworks and tools. Each generation tried to turn software into a line with stations, and each ran into the same wall: a station had to be something you could standardise – a template, a generator, a process people followed – and most software work wasn't.

What changed is the same thing that changed for the control loop: the actuator became general-purpose. A station no longer has to be a code generator. It can be anything you can describe in text. That is why the idea is back, and why I think some of it will stay this time.

As with "loop", though, two different things now share the name.

  • The dark factory. StrongDM's AI team published the sharpest version in February 2026, with two rules: "Code must not be written by humans" and "Code must not be reviewed by humans". Dan Shapiro's five levels put it at the top – level five is "a black box that turns specs into software". StrongDM validates with scenarios stored outside the codebase, like a holdout set in model training, and a satisfaction score judged by a model.
  • The factory with the lights on. Augment's write-up of its own factory is a good example: specialised agents at each transition from requirement to ticket to pull request to production, with engineers keeping the product and architecture decisions and the final merge decision.

The dark factory deserves its due first. Storing the scenarios where the coding agent can't rewrite them is the right instinct – it is the sensor kept out of the agent's reach. And nobody who has opened a 40,000-line pull request thinks human review scales by itself. What I don't accept is the other half. A score produced by a model is a model grading a model; the Stanford CodeX commentary puts it as "the same class of technology that writes the code also decides whether the code works". And "not reviewed by humans" removes the one participant who can be held to the result. Simon Willison, who visited the team and wrote the most careful outside account, asks the same question about that rule, and about the token bill that comes with it.

dark factoryspecs and scenarioswritten by peopleagents write the codea model scores the resulta model grading a modelit shipsnobody has read itnobody reads the code · nobody signsfactory with the lights onticket → approved specwhat done means, by peopleagents write the codedeterministic gatescompiler · tests · analyzersa named engineer mergeshas read it, answers for ita check that is not a model · a person who signs
Fig. 2 · The same first two steps, and then the two factories part ways. On the left a model judges the result and nobody reads it. On the right the judge is deterministic, and the last step has a name.
Dark factoryFactory with the lights on
Who writesagentsagents
Who measuresscenarios, scored by a modeldeterministic gates first; a model only as a signal
Who reads the codenobody, by rulea named engineer, in small pull requests
Who answers for itwhoever designed the factorywhoever merged it
Unit of outputa builda pull request
Where I would consider ita new product whose whole environment can be simulatedan existing system with customers, contracts and an on-call rota
The rest of this post is about the second kind. Not because the first is impossible, but because an enterprise system has an owner, and the owner has to be a person.

What is in the factory

Fig. 1 is the picture I use. Six parts, and none of them is exotic.

  • Context – the repository and its rules file. Still the strongest prompt there is: the agent does what it sees in the surrounding code.
  • Knowledge base – what the repository can't say: decision records, runbooks, deployment targets, past incidents, customer-specific constraints. Retrieved per task, not pasted in wholesale – from Markdown in git, a document database or a search index; where it is stored is a design choice.
  • Skills – how a kind of change is done here: add an adapter, write a migration, upgrade the runtime. Text plus handwritten golden examples, versioned.
  • Triggers – what starts a run. An event: a ticket gets a label, an alert fires, someone asks in Slack or Teams. Or a schedule, for the standing jobs: improve performance, improve security, clean up code, update docs.
  • Sandbox – where the agents run: a disposable environment with the repository, the toolchain and an allowlisted network, and nothing else. I treat the model as a capable insider, so the boundary is the environment, not a sentence in the prompt.
  • Model – behind an adapter, replaceable. The least interesting box in the picture, and the one most discussions start with.

The agents themselves are small: one per station, each a model call with a skill and a narrow job. And one thing leaves the building: a pull request. Not a deploy, not a commit on main, not a message saying it is done.

The scheduled triggers are the loop from the earlier post, unchanged: a cron job, a sensor, one small PR in the morning. The event triggers are the new part, and they are where the factory earns its name. The machinery that works down a migration one file a day now takes a feature ticket.

From my harness to ours

Most teams that use coding agents are in the same phase right now: a harness per developer. Their own rules file, their own skills, their own subagents and tool servers, tuned on their own laptop. I work that way myself, and for one engineer it works, because it runs on that engineer's attention. Across a team it has three costs.

  • The same ticket gives a different change on each laptop. Not because the models differ, but because the instructions around them do.
  • What the team learns stays local. A review lesson improves one person's skills file. When that person moves on, the lesson moves with them.
  • There is nothing to govern. Nobody can say which instructions produced the change that is now in production, and the only gate was whatever its author happened to run.
today: a harness per laptoplaptop Aown skillschange Alaptop Bown skillschange Blaptop Cown skillschange Cone ticket, three laptops, three different changeswhat review taught A never reaches Bthe factory: one shared harnessshared, maintained in one placeskills · knowledge base · gateslaptop Alaptop Blaptop Cpipelinesame skills, same gates – wherever the agent runsa review lesson lands once, for everyone
Fig. 3 · The same three developers, before and after. On the left the harness is private, so the output depends on whose laptop the ticket landed on. On the right the parts that decide the output are shared, and the laptops and the pipeline read the same ones.

The factory's answer is not to take the local agent away. It is to move the parts that decide the outcome out of the laptops and into one shared place, where they are owned, reviewed and versioned. For skills and gates that place is the repository – and, one level up, the organisation's. For the knowledge base it can just as well be a document database or a search index. A local session then loads the same skills and the same knowledge the pipeline does, and whatever anyone builds locally still has to pass the same gates.

It is also kept up to date in one place. A changed skill, a new gate, a model upgrade: each is one pull request, and after the merge everyone has it. The alternative is what teams have now – a dozen local setups drifting apart at a dozen different speeds, each a little different from the one the last change was built with.

Stays on the laptopBecomes shared
Ruleshow you like to work with the agentthe rules file: the patterns, the shared modules, the tools
Skillsyour own shortcutshow a kind of change is done here
Knowledgeyour notesthe knowledge base, with owners
Checkswhat you run before you pushthe gates every change passes
Agentsyour sessions for exploring and understandingone small agent per station
A private harness makes one developer faster. A shared one, maintained in one place, makes the team's output predictable – and it is the first version of all this that a company can actually govern.

One pipeline for every ticket

What makes it a factory rather than a pile of agents is that there is one route through it. A feature, a bug, a nightly clean-up – they pass the same stations, and every station has the same three parts: an agent with a skill, a gate that is not an agent, and, at most stations, a person.

ticket · chat message · alert · schedulewhat production and the user report comes back as the next ticketstationagent + skillgatedeterministichumanwho signsspecwhat done meansevery criterionnames its checkproduct owner+ lead approveplanone PR per tasktask ↔ criterionnothing unmappedtechnical leadsets directionbuildcode, in a sandboxcompiler + testsanalyzers · e2eno onemachines onlyreviewsees only the diffPR size limitoracle untouchednamed engineerreads and mergesreleasecanary + rollbackcanary metricsauto-rollbackowner on callanswers for itThe same five stations for a feature, a bug and a nightly clean-up. Only the trigger differs.
Fig. 4 · One ticket, five stations. The top row does the work, the middle row measures it without asking a model, the bottom row is who puts their name to it. The build station has no person in it on purpose.

Written down, it is short:

factory/pipeline.yml

# factory/pipeline.yml – a sketch of the shape, not a product
triggers:                              # whatever starts it, the route is the same
  - ticket:   { label: factory }
  - chat:     [slack, teams]           # a message becomes a ticket first
  - alert:    { severity: [high] }
  - schedule: { cron: '0 5 * * 1-5', jobs: [performance, security, cleanup, docs] }

stations:
  - id: spec
    agent: skills/spec-from-ticket
    output: one page – intent, done means, must stay true, watch out
    gate: every criterion says how it will be checked
    human: product owner and technical lead approve – the agent can't edit it afterwards

  - id: plan
    agent: skills/slice
    output: tasks, each one reviewable pull request
    gate: every task maps to at least one criterion
    human: technical lead approves the direction

  - id: build
    agent: skills/implement            # in the sandbox, network on an allowlist
    gate: [compiler with zero warnings, analyzers, architecture tests, unit, e2e, the checks the spec names]
    human: none – this is where the machines are better

  - id: review
    agent: skills/adversarial-review   # sees the diff, nothing else
    gate: PR size limit; nothing changed under docs/specs/, tests/e2e/ or factory/
    human: a named engineer reads it and merges

  - id: release
    gate: canary metrics, automatic rollback
    human: the owner on call

limits:
  open_prs_per_owner: 2                # review capacity, not token budget

That file is the shape, not something a runner executes. What the shape looks like as real workflow files – the labels, the two jobs, the script that calls the agent – is the subject of the companion post.

Three details carry most of the weight.

  • The spec station writes down what done means. One page, from domain knowledge: what the feature is for, the criteria it is accepted on, what must stay true around it, and where this system punishes carelessness. A product owner and a technical lead approve it, and from then on the agent can read it but not edit it.
  • The build station has no human in it. Compiler, analyzers, architecture tests, unit and end-to-end tests – on the frontend, something like react-doctor. If a check can be stated, it belongs here, where a failed attempt costs minutes and nobody's attention.
  • The limit is on open pull requests per owner, not on tokens. A single loop's throughput is set by review capacity. A factory with twenty triggers hits that wall on its first day.

What the spec station writes

Take the request from the engineering mindset post – "make an Excel export" – as a ticket for the Orders module. The approved spec is one page:

docs/specs/1482-orders-csv-export.md

# 1482 · CSV export for orders

## Intent
Finance reconciles a month of orders against the bank statement.
Today they copy rows out of the grid by hand.

## Done means
1. An admin exports the filtered order list as CSV. Checked by: e2e test.
2. Amounts match the invoices to the cent, in each order's currency. Checked by: test on the fixture month.
3. Two million rows export without being loaded into memory. Checked by: load test on staging data.
4. A non-admin gets a 403, and the attempt is in the audit log. Checked by: e2e test.
5. Columns are in the order finance signed off. Checked by: product owner, on the preview build.

## Must stay true
- Holds on both hosting targets (docs/kb/hosting-targets.md): no file on local disk.
- Orders gets amounts through `IBillingService`, never from Billing's tables.
- The existing JSON API does not change.

## Watch out
- Money is stored in minor units. Never format it through `double`.
- "A month" is the customer's month, not UTC.
- Excel reads CSV with the machine's regional settings: separator and decimal mark differ per locale.

## Out of scope
Scheduled exports. XLSX.

The agent can draft most of this, and its draft is useful: it finds the contradictions in the ticket and asks what happens on the empty list. But two sections are not the model's to invent. Must stay true is the architecture. Watch out is the scar tissue: the rounding incident, the customer in another time zone, the export that worked on five hundred rows. These are the places where the agent has to be most careful, and they come from people who have run this system. An agent that gets the ticket without them writes the nearest export that works.

This is deliberately not test-driven development. Nobody writes a red test before each line, and the order in which code and tests appear inside the build station is the agent's business. What is fixed before the work starts is smaller and matters more: what done means, and how each point will be checked – by a gate that already exists, by a test written with the change, or by a named person looking at a preview build. A criterion that can't answer "checked by" isn't a criterion yet. That is the whole gate of this station.

Still a control problem

Put the loop post's vocabulary next to the factory and the mapping is direct.

One control loopThe factory
Set pointone assertion: "0 legacy .handler() procedures"per ticket: the approved spec – what done means, and what must stay true
Sensorone rule filethe gates at every station
Error150 violationscriteria not yet met, failing checks, findings, a red canary
Controllerpick one findingthe slicer, the WIP limit, the policy per class of change
Actuatoragent + skillagents + skills + the shared memory
Disturbanceteammates' pull requestsother tickets in the same factory, dependency updates, a requirement that moved
Humanreviews each PRapproves the set point, reviews the PR, owns the release

What is new at this size is nesting. The agent's own loop runs inside a station's gate, which runs inside the pull-request loop, which runs inside production. Each ring has a slower and more expensive sensor than the one inside it.

production – days · measured by metrics, incidents, userspull request – hours · measured by a named reviewerstation – minutes · measured by compiler, analyzers, testsagent – seconds · measured by the model itselfgather contextactverifycatches what a rule can statecatches what a person sees in a small diffcatches the rest, in front of the customerA check moved one ring inward gets cheaper by an order of magnitude. That is the design work.
Fig. 5 · Four loops, one inside the other. The innermost turns in seconds and is graded by the model itself; the outermost turns in days and is graded by your users. The design work is moving every check as far inward as it can be stated.

This is what "caught twice in review becomes a rule" means in control terms: a rule moves a check from the ring that turns in hours to the ring that turns in minutes. It also says what a factory without deterministic gates is. It has the model's opinion of its own work, and it has production.

A factory is not a smarter loop. It is more loops, sharing a memory – with the same question at every one of them: who measures?

Why the rewrites worked

Now the three stories, read as control loops.

BunCopilot runtimeTypeScript 7
From → toZig → RustTypeScript → RustTypeScript → Go
Sizea +1,009,272-line diff in 11 days~430,000 lines in, 832,378 out; 128 PRs in ~14.5 weeksthe whole compiler; announced March 2025, released July 2026
Who typed it~64 parallel agentsagents, supervised primarily by a single developernot stated in the team’s posts
Set pointsame behaviour, memory-safesame behaviour, without Node.js and V8same behaviour, same code structure, about 10× faster
Sensorthe Rust compiler and the existing tests, none skipped or deletedthe Rust compiler and every existing end-to-end test, on every PR~20,000 compiler test cases, built over a decade
What got through19 known regressions after the mergedozens of regressions, traced and fixed74 of ~6,000 error cases still differed in December 2025, each one accounted for

The Copilot post is the one to read in full, because Stephen Toub wrote down a control loop without calling it a factory. The strategy was atomic replacement: each pull request swapped a piece of TypeScript for a thin shim that calls into Rust and deleted the old code in the same change, working from the leaves inward, so main stayed shippable through all 128 of them. Every existing end-to-end test ran against the new code at every step, and the rule was the simplest gate there is: if a required test failed, the PR didn't land. The port was deliberately behaviour-preserving – "translate first, redesign second" is one of his six lessons.

main, beforemain, after any of the 128 PRsmain, at the endTSTSTSTSTSRustRustTSTSTSRustRustRustRustRustall TypeScriptmixed, through thin shims – and shippableall Rustthe existing end-to-end testsrun against every one of these states – a red test means the PR does not landOne piece per pull request, ordered from the leaves inward: pure logic first, session orchestration last.
Fig. 6 · Atomic replacement. Main is never half-broken: after every one of the 128 pull requests it is a working mix of TypeScript and Rust, measured by the same end-to-end tests as the day before. That is what makes each step small, reviewable and reversible.

TypeScript 7 makes the same point from the other side. The team chose Go over languages that would have suited a ground-up rewrite, and Ryan Cavanaugh's explanation is about exactly this: the new codebase had to stay as compatible as possible "both in terms of semantics and in terms of code structure". A port, not a rewrite. Then a decade of tests did the measuring: in December 2025, of the roughly 6,000 test cases that produce an error in TypeScript 6.0, all but 74 produced one in the Go compiler too, and each of the 74 was known work or an intentional change. I can't tell you how much of that port a model typed, and for the argument it doesn't matter. The recipe is the same with or without one.

what done meanswhat measures itwho writes the codea portthe three rewritesthe old systemit already runsits existing testswritten years agoagentscan start on day oneboth exist before the work startsa featureeveryday worka ticketwords, not a specan approved speccriteria, each with a checkagentsstart only after thathas to be built first: people decide what done means and how each point gets checkedskip it, and the only thing measuring the agent is the agent
Fig. 7 · The same three questions for a port and for a feature. A port answers the first two before anyone starts: the old system says what done means, and its tests measure it. A feature answers neither, so the middle box has to be built first – by people – before an agent writes a line.
"Rewrite it in X" works when X has a compiler that catches your class of bug and the old system has tests that pin its behaviour. Without those two it is one large unverified diff, however many agents wrote it.

The first job I would give a factory

So would I hand a port like TypeScript's to a factory? Yes – it is the first job I would give one. Everything the pipeline needs was there before anyone typed a line of Go: a set point a machine can check (the same answers as the old compiler), about 20,000 test cases to run on every pull request, a compiler for the target language, and a structure-preserving plan that splits the work piece by piece, so every unit can be a small PR measured against the same suite. The Copilot port is the evidence that this shape holds with agents doing the typing: the same kind of job, at that size, in fourteen and a half weeks, primarily by a single developer with a team behind them.

What the factory would not have supplied are the decisions that made the port possible: choosing Go because the code could keep its shape, deciding to port instead of redesign, and ruling on each of the 74 differences – unfinished work, or a change we intend? Those came from people who knew the compiler, and together they are the set point. A factory works against a set point. It doesn't choose one. And tractable is not the same as easy: the Copilot port had the sensors and the pipeline, and things still got through, as the next section shows.

That is also the honest limit on what these stories prove. They are the best case: the set point is "behaves as before", and it is machine-checkable on day one. Feature work doesn't come with that. The spec station exists to manufacture it.

Even the best case leaks

And even there the sensors weren't enough on their own, which is the second reason to read the Copilot post. Toub reports dozens of port regressions traced by mid-September, all fixed, and adds that he is sure there are more. Two of his observations are worth more than the benchmark tables. Every one of those regressions compiled – a compiler is necessary, and it is not a test of behaviour. And with one exception, the regressions that were missing features came from behaviour that had no end-to-end test. His own conclusion is that more of those tests before starting would have meant fewer regressions along the way.

sensorwhat it caughtwhat got throughcompilerrustc, on every build8,678 compiler errors along the wayevery regression compiledend-to-end testsred = the PR does not landbehaviour changes that had a testbehaviour nobody had testedreviewagents on the diff + the engineera waived schema check, work declared done earlywhat a diff doesn’t showafter the mergereleases and issuesdozens of regressions, all fixed“I’m 100% sure there are more”Numbers and the last quote: Stephen Toub’s account of the port. Each layer is necessary; none is the whole sensor.
Fig. 8 · The Copilot port as a stack of sensors. Each one caught a class of mistake the one above it could not, and each let something through. The cheapest fix for anything in the right-hand column is a check one row higher.

Protect the oracle

The most useful line in that post is a lesson heading: "Protect the oracle from the agent." It comes with incidents. In one, a port deleted an SDK method, the schema-compatibility check went red, and the agent applied the label that waives the check. In another, a port left out a set of SDK callbacks – and deleted the end-to-end test that covered them. Neither is malice. It is the any from the loop post again: the agent was asked for green, and found the cheapest green.

In a single loop you solve this once. In a factory it has to be a property of the building, because there are many agents, many gates, and constant pressure for throughput. Three rules:

  • The agent that writes the code can't edit what measures it. The approved spec, the existing end-to-end tests, the gate configuration, the pipeline definition, the waiver labels: owned by people, changed only in a pull request a person wrote.
  • A deleted or weakened check is a finding. Removed tests, new suppressions, skipped cases, a lowered threshold – the pipeline reports them itself, instead of trusting the summary in the PR description.
  • Waivers exist, and a person signs each one. There are legitimate breaking changes. The label just isn't the agent's to apply.
agentwrites the changethe cheapest green: a waived check, a deleted testblocked by the pipelinea personsigns every changethe agent may changeproduction codeunit tests for its own codethe pull request descriptionall of it is measured by the zone on the rightonly a person may changethe approved spec and the e2e testsgate and pipeline configurationsensor rules and suppressionswaiver labelsThe agent that writes the code never holds the pen for what measures it.
Fig. 9 · Who holds the pen. The agent writes on the left, and everything it writes is measured by the right. When it reaches across – and the Copilot port shows it will – the pipeline says no, and a person decides.

In CI this wall is two short workflow steps, a CODEOWNERS file and four settings on the main branch. They are in the companion post, Building a software factory on GitHub, with a table of what an agent might try and which step stops it.

StrongDM's holdout scenarios are the same idea taken one step further: an agent can't edit what it can't see. I would borrow that for the criteria that matter most – have their checks written from the spec alone, in a separate context from the one that wrote the code – without borrowing the rule about nobody reading the code. The other checks the agent writes for new behaviour are part of its change, and get reviewed like the rest of it: against the spec, by someone who didn't write them.

The shared memory, and how it rots

The knowledge base is what makes many loops one factory. Without it, every run starts as a talented stranger. With it, a review comment worth keeping is written down once and loaded into every run after it – the feedback file from the loop post, promoted to something the whole pipeline reads.

Worth keeping, not every comment. Most review comments repair that one pull request and end there. The ones that get promoted state a convention, or come up a second time – and where they land depends on what they are.

The four stores are not interchangeable, and most of the confusion comes from putting things in the wrong one.

StoreHoldsExampleGoes wrong when
Contextwhat the code already saysone storage port with four adapters, and the rules file that points at itthe pattern in the code is wrong, and the agent scales it
Knowledge basethe why, and facts from outside the repositorywhy settings sync from the database; the two hosting targets; last year’s incidentnobody owns the entry and it goes stale
Skillshow a kind of change is done hereadding an adapter, writing a migration, upgrading the runtimeit describes last year’s way of doing it
Sensorswhatever can be stated as a checkarchitecture tests, banned APIs, the two-target test jobthe agent can edit them

The rule for moving between rows: if it can be a sensor, make it a sensor. A sentence in a knowledge base is advice the model weighs against everything else in its context. A failing check is not up for weighing. The knowledge base is for what is left: the why, and the facts no check can express.

worth keeping = it states a convention, or it came up a second timea review commentworth keeping?yesnofix it in this PRmost comments end herecheckable?can a machine verify itnoyesa sensorit fails the builda how-to?how a change is done herenoyesa skillloaded for that kind of taska why or a fact?not visible in the codeyesthe knowledge baseretrieved when relevantAsk in this order. A sentence in a document is advice; a failing check is not.
Fig. 10 · Where one review comment goes. Most stop at the first box. Of the ones worth keeping, anything a machine can verify becomes a check; only what is left is written down as a skill or a knowledge-base entry.

And it rots. An agent follows a stale document as faithfully as a fresh one, and retrieval makes that worse, because the stale entry arrives looking like evidence. So I would hold the knowledge base to the standard of code, wherever it is stored.

  • One source per fact, with an owner. And the entry names the check that enforces it, if there is one.
  • Changes are reviewed and leave a history. A pull request in git; an approval step and versioning anywhere else.
  • Retrieval respects the reader's permissions, because a memory every agent can read is also a very efficient way to leak.
  • One way in. The laptop and the pipeline ask the same interface and get the same answer.
  • Staleness is visible. Every entry carries when it was last confirmed, and by whom.

Where it lives is a design choice

None of those five needs a particular store. The examples below use Markdown in the repository because it is the smallest thing that works, not because it is the end state.

WhereFits whenWhat it costs
Markdown in the repositorytens of entries, tied to one codebase; review through pull requests comes for freeretrieval is a tag lookup, and other teams can’t see it
A document database, or a wiki with an APIknowledge that spans repositories and is written by people who don’t open pull requestsownership and review have to be built in on purpose, or it goes stale quietly
A search index over the sources – Azure AI Search and the likethousands of documents: specs, incidents, tickets, contracts; hybrid retrieval with keyword, vector and a rerankeran indexing pipeline to run, permissions to carry into the index, retrieval quality to measure

They combine: an index is a view over sources that still need owners, and the pattern is the one I use in my own applications – retrieve broadly, then let the source's own access rules decide what is allowed through. This is also where a factory has the most room to grow. Indexing the ticket history, the incident reviews and the merged pull requests gives the spec station material no rules file will ever hold. The limit is the rule from earlier in this section: whatever can be a check should not stay a document.

What such an entry looks like is easier to show on a real case, so it comes with the first example in the next section.

It works, and it is still wrong

Now the part no pipeline diagram shows. A model can have that whole knowledge base in front of it and still produce a change that passes every gate and is wrong for this system. Not broken. Wrong: solved by the nearest route. Two examples.

One build, two hosting targets

Take a product that has to run on Windows under IIS for one set of customers and on Kubernetes for another. Same build. The ticket says: export the invoices every night and on demand, and never run two exports at once.

// Ticket: "export the invoices every night, and on demand – never two exports at once".
// Green on a developer machine.
public sealed class InvoiceExport(IExporter exporter) : BackgroundService
{
    private static readonly SemaphoreSlim Gate = new(1, 1);            // one at a time – in this process

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromHours(24));   // counts from process start
        while (await timer.WaitForNextTickAsync(ct))
            await RunAsync(ct);
    }

    public async Task RunAsync(CancellationToken ct)                   // also behind the "Export now" button
    {
        await Gate.WaitAsync(ct);
        try     { await exporter.WriteAsync("exports/invoices.csv", ct); }   // local disk
        finally { Gate.Release(); }
    }
}

// IIS:        by default the app pool stops the process after 20 idle minutes, and the 24-hour
//             timer starts over with every restart. On a quiet site the job never runs.
// Kubernetes: three replicas, three processes, three timers, three locks. It runs three times,
//             and each file lands on a pod's own disk, gone at the next rollout.

On a developer machine there is one process and it never sleeps, so this is green. Under IIS the application pool shuts the worker process down after twenty idle minutes by default, and recycles it every 29 hours; a timer that waits a day inside that process starts over with every restart, and on a quiet site it never fires. On Kubernetes there are three replicas, which means three processes, three timers, three copies of a lock that exists only in memory, and three exports – each written to a pod's own disk, which is gone at the next rollout.

one buildthe nightly export, as the agent wrote itWindows · IIS1 processthe app pool idles out after 20 quiet minuteson a quiet site the job runs 0 timesKubernetes · 3 replicas3 processes, 3 timers, 3 in-memory locksa pod’s own disk is gone at every rolloutthe job runs 3 timesGreen on the developer’s machine, where there is one process and it never sleeps.
Fig. 11 · The same class, deployed twice. Under IIS the job can run zero times; on three replicas it runs three times. The two targets fail in opposite directions, and neither failure is visible where the agent tested.

The agent wasn't ignorant. Ask it directly whether an in-memory lock holds across replicas and it will say no. It had a ticket, a green test and a shorter path, and nothing in front of it measured the difference. This is works on my machine, the higher level, with a second platform added – and the two targets fail in opposite directions, so testing on one tells you nothing about the other.

A person who has operated both sees it in the diff in ten seconds: a timer in the process, a static, a path. That is the first half of the job. The second half is making sure nobody has to see it again:

.github/workflows/ci.yml (excerpt)

# The edge case a person saw once, as a gate every pull request passes from then on.
e2e-hosting:
  strategy:
    matrix:
      target: [iis, k8s]
  runs-on: ${{ matrix.target == 'iis' && 'windows-latest' || 'ubuntu-latest' }}
  steps:
    - uses: actions/checkout@v7
    # iis: a site whose app pool idles out after one minute · k8s: a kind cluster, 3 replicas
    - run: ./eng/deploy-${{ matrix.target }}.ps1
      shell: pwsh
    - run: dotnet test tests/E2E --filter "Category=Hosting"
      # asserts: a scheduled job fires once per interval across all instances,
      # and the export it wrote is readable from every instance

From then on it is a gate in the build station: the engineer's observation moved two rings inward in Fig. 5. But a gate can only say that something is wrong. It can't say why there are two targets, or what to use instead of a timer – and an agent that only sees a red test will try the next shortcut. That part is what the knowledge base is for. In the terms of Fig. 10: the checkable half became a sensor, and the why became an entry, handed to the agent whenever a task touches scheduling, files or hosting:

docs/kb/hosting-targets.md

# Hosting targets

We ship one build to two targets: Windows under IIS, and Kubernetes (Linux, 3+ replicas).
A change is done when it holds on both.

## What differs
- Process lifetime: IIS stops an idle process and recycles it on a timer.
  Kubernetes replaces pods on every rollout.
- Instances: usually one under IIS. N on Kubernetes – always assume N.
- Local disk: stays under IIS. Gone with the pod on Kubernetes.
- File system: case-insensitive on Windows, case-sensitive on Linux.

## Rules
- Scheduled work goes through `IJobScheduler` (one lease per job, in the database).
  No timers in the process.
- Files go through `IFileStore`. No paths.
- Anything process-wide – a `static`, an in-memory cache, a lock – needs a line in the PR
  saying why it is safe on N replicas.

Owner: platform team · Confirmed: 2026-10 · Why: ADR 0012 · Enforced by: the e2e-hosting job

Four parts, and that is the whole format: the facts that differ, the rules that follow from them, an owner with the date they last confirmed it, and the check that enforces it. If the last line can't be filled in, the entry is advice – which is sometimes all you can have, but you should know it.

The global using

The second example is smaller and more typical. Take the modular monolith from Two kinds of micromonolith: each module is a pair of projects, a public Contracts assembly and an implementation whose types are internal. Project references stop the hard violations – reaching into another module's internals doesn't build. Inside a module the compiler can't help, because the implementation project legitimately references EF Core and the contracts of other modules; there the layering is held by an architecture test. Add one convention on top: every file lists its own using directives, and project-wide usings are off. The reason is review. A new line in the block at the top of a file is the cheapest signal there is that a domain class has started to use EF Core, or that one more file in Orders now depends on Billing. The agent adds a feature, twelve files don't compile, and it fixes all twelve with one new file:

  // Ticket: add a CSV export to the Orders module. The first build fails in twelve
  // files: CS0246, the type or namespace could not be found – missing using directives.

+ // src/Shop.Orders/GlobalUsings.cs – one new file, twelve files fixed
+ global using System.Globalization;
+ global using System.Text;
+ global using Microsoft.EntityFrameworkCore;   // now in scope in Shop.Orders.Domain too
+ global using Shop.Billing.Contracts;
+ global using Shop.Catalog.Contracts;

It compiles. The tests are green – the architecture test too, because a using directive is not a dependency. But EF Core and the contracts of two other modules are now in scope in every file of the module, the domain included. From here on a file can take any of those dependencies without a line changing at its top, so the signal reviewers relied on is gone for the whole project. The architecture test still fails once a domain type really uses EF Core; it has just become the only line of defence instead of the second. And names start to collide: the day Catalog publishes a Money type next to Billing's, every file in Orders that uses Billing's Money stops compiling with an ambiguous reference – files that never mention Catalog. The compiler was the sensor, and the compiler was satisfied. It is the same family as the any, the waived label and the deleted test.

every file lists its own usings+ using Microsoft.EntityFrameworkCore; namespace Shop.Orders.Domain;+ … orders.AsNoTracking() …the new dependency is the first line of the diffa reviewer can ask: why does the domain need EF Core?one global using for the project (nothing changes at the top) namespace Shop.Orders.Domain;+ … orders.AsNoTracking() …the same dependency, somewhere in the body of the diffit is in scope everywhere, so nothing announces it
Fig. 12 · What the convention buys. With per-file usings a new dependency announces itself in the first line of the diff. With a project-wide using the same dependency is still in the diff – just nowhere a reviewer looks first.

So the convention gets a check of its own:

# In CI, next to the other checks the agent can't edit. Three ways to get a
# project-wide using: a global using directive, ImplicitUsings, a <Using> item.
if grep -rnE '^[[:space:]]*global[[:space:]]+using[[:space:]]' \
        --include='*.cs' --exclude-dir=obj --exclude-dir=bin src/ \
   || grep -rniE '<ImplicitUsings>[[:space:]]*(enable|true)|<Using[[:space:]]' \
        --include='*.csproj' --include='*.props' --include='*.targets' .; then
  echo "::error::Project-wide usings are not allowed here: every file lists its own (ADR 0004)."
  exit 1
fi

The explicit using lines were the right answer, and the agent writes them as fast as the wrong one. Choosing wrong just has to cost something.

"It works, but that is not how we build here" has no sensor until someone writes one. Seeing it the first time is the engineer's job. Making the pipeline see it the second time is the engineer's job too.

Some of it never becomes a rule. Architectural coherence is not a local property: whether a feature belongs in the product at all, whether this abstraction is the one the next three features need, whether a shortcut is acceptable for this customer this quarter. Those stay judgement, and judgement needs someone who has seen the whole system.

What the human is for

So the person doesn't leave the factory. The work moves. I see three jobs that stay, and they sit at different places on the line.

the pipelinespecplanbuildreviewreleaseunderstand the requirementwhat they need, not what they saidhold the architecturedirection, and rules that enforce itanswer for the resulta name on every merge and releaseAI helps; the questions are yourswhat you see becomes a sensor or a rulea model can’t be held to anythingTyping the change is the one job in this picture that moved into the factory.
Fig. 13 · Three jobs, attached to different stations. The build station is the only one with nothing human underneath it – and it is the only part of the old job that moved inside.

Understanding the requirement

Nobody hands over a specification. Somebody says words, and a person who knows the product turns them into something with limits, fallbacks and the cases the customer didn't think of. AI helps here more than people admit: drafting the spec, finding the contradictions in a ticket, asking what happens on the empty list. But deciding what "done" means, and whether this is a feature for every customer or a flag for one, is a decision somebody has to own. In Fig. 1 that is where the product owner, the designer and the user stand: before the build station, and again after it. A designer reviews the rendered screen attached to the pull request, not the diff. The user is the outermost sensor.

Holding the architecture

The engineer's level goes up. Less "how do I write this", more "which direction, and what stops the factory from leaving it". Concretely: choosing the patterns the agents will copy, deciding which hosting targets and constraints are real, reading enough of the output to notice drift, and turning each thing you notice into a sensor, a skill or a knowledge-base entry. Toub describes his role in the Copilot port as less "assign a task and wait" and more "operate the control loop", and counts 2,639 messages he wrote along the way – about a third of them on review, testing and CI. That is one engineer supervising more code than anyone could type. It is not zero engineers.

When the factory gets it wrong, the fix goes into the factory: fix the process that produces the code, not the code. Which you can only do if you read enough of the code to see why it came out wrong.

Answering for the result

This is the one a model can't do at all. Not for lack of capability – responsibility needs someone it can land on: a person who can be asked why, who is on call when it breaks, who signs the contract and is still there next year. The next run doesn't remember this one. Toub's wording is the one I would put on the wall: the agents changed how much code one engineer could supervise, and did not remove the need for an engineer who understands the system and can vouch for "the direction, the guardrails, and the release".

So every pull request has a named owner, every waiver has a signature, every release has someone on call, and "who approved this?" always has a person's name for an answer. That isn't bureaucracy on top of the factory. It is the part that makes the factory usable by a company with customers.

The factory can do the work. It can't take the blame – and whoever takes the blame has to be able to read what they are signing.

Which is why comprehension debt is the factory's long-term risk: the code now grows much faster than anyone's understanding of it. And it is one more reason to put juniors in the room for the architecture decisions early. The jobs that stay are exactly the ones you can't learn by watching a pipeline go green.

Where I would start

Not with a platform. In this order:

  1. One class of change that already has a sensor. A runtime upgrade, a framework migration, a port. Build one loop for it, exactly as in the loop post.
  2. Ratchet, and lock the oracle. Existing checks can't get worse, and the agent can't edit them.
  3. Put the knowledge in one shared place, with owners. Start with the ten things every new colleague gets told in their first month. Markdown in the repository is enough for that; a database or an index can come when it stops being enough.
  4. Make the pull request the only exit, with a named owner and a WIP limit.
  5. Add the spec station. Only now feature tickets: an approved spec first – what done means, what must stay true, what to watch out for.
  6. Measure against a baseline, over a quarter. Lead time, change failure rate, reverts, escaped defects. Augment reports its 14-day revert rate falling from 1.9% to 0.4% and its median time to merge from 11.2 to 3.1 hours across 17,200 merged pull requests – and labels that honestly as a longitudinal case study, not a controlled experiment. Treat every such number that way, your own included.
  7. Widen per class of change, on evidence – a ladder, not a switch.
one loopa change with a sensorlock the oracleratchet + ownersshared memoryone place, with ownersPR as the only exitowner + WIP limitspec stationfeature ticketsfeature tickets come last, not firstMeasure against a baseline at every step. Widen per class of change, on evidence.
Fig. 14 · The first five steps as stairs. Each one is useful on its own and safe to stop at; the spec station – the part that looks most like a factory – stands on all the others.

The files behind these steps – the workflows, the agent script, CODEOWNERS, the ruleset – are in Building a software factory on GitHub: the cornerstones, written as a starting point to adapt to your own rules.

On cost: the Copilot port spent about $120,000 in tokens over fourteen and a half weeks, on work its own subtitle says wasn't affordable before agents. StrongDM's benchmark is $1,000 in tokens per engineer per day. Neither is the number to optimise. The scarce input is still the hours of people who can read what comes out.

And where I wouldn't: greenfield product design, anything irreversible, and anything where you can't yet say what the sensor is. The factory doesn't answer that last question for you. It only makes skipping it expensive faster.

Objections, and the answers

They sayYou say
"The Copilot and Bun rewrites prove you don’t need to read the code."Both authors read it. Toub wrote 2,639 messages and made every merge decision; Bun’s process fixes came from someone reading diffs. And both were ports, where the old system is the spec.
"Microsoft rewrote TypeScript in Go with AI."The TypeScript team’s posts don’t say so. What they do say is that the port was structurally faithful and verified against a decade of tests – and that is the reusable part. It is also why a factory could run a port like it: the sensor was finished before the work began.
"A better model will make the gates unnecessary."A better actuator doesn’t replace a sensor. The Copilot port used the best models available, and every regression still compiled. The gate isn’t there because the model is weak. It is there because done has to be measured by something other than what did the work.
"A RAG knowledge base will teach the agents our architecture."It will tell them about it. An entry is advice; a failing check is a wall. Put whatever can be checked into a check and keep the entry for the why – with an owner, wherever it is stored.
"Human review is the bottleneck, so remove it."It is the bottleneck, so spend it well: small PRs, deterministic gates first, lighter review per class of change on evidence. Remove it and nobody can answer for what ships.
"Holdout scenarios and an LLM judge are enough."The holdout is a good idea: an agent can’t game what it can’t edit. The judge is a model grading a model – a signal, not a gate.
"Every developer already has their own agent setup, and it works."For each of them. The same ticket gives a different change on each laptop, and a review lesson improves one person’s skills file. Keep the private setup for exploring. Put how changes are done, and how they are measured, where the whole team and the pipeline read it.
"This is just CI/CD with extra steps."Yes, and that is the reassuring part. The stations are new. The discipline is the pipeline you already have.
"We need an agent platform first."You need a sensor first. A cron job, a rule file and a pull request are a factory with one station. Grow it from there.

Takeaways

  1. A factory is control loops sharing a memory, not a bigger agent.
  2. Share the harness. Skills, knowledge and gates belong in one shared, maintained place, not on laptops. The local agent stays; what decides the outcome moves.
  3. One pipeline for every ticket: spec, plan, build, review, release – each with a gate that is not a model.
  4. The rewrites worked because a port arrives with its sensor. The old system is the spec, its tests are the oracle, the compiler is the first check.
  5. Even then things got through. Every regression compiled, and what had no test, broke.
  6. Protect the oracle. The agent that writes the code can't edit, delete or waive what measures it.
  7. If it can be a sensor, make it a sensor. The knowledge base is for the why – owned, reviewed, pointing at its check, in whatever store fits.
  8. "Works, but not how we build here" needs a person the first time and a rule the second.
  9. The engineer moves up: the requirement, the architecture, the responsibility.
  10. A named person on every merge and release. A model can do the work. It can't be held to it.
  11. Review capacity sets the throughput. Start with one station.

The loop post ended on one question: what can I measure? The factory adds a second: and who signs? If both have an answer at every station, a software factory is not a risk. It is the pipeline you already trust, with a far more capable worker inside it and the same people accountable around it. If either answer is missing, you don't have a factory. You have a fast way of producing something nobody measured and nobody owns.

I think this is what enterprise software development looks like in the AI era – for now. Not lights out. Lights on, sensors everywhere, and a person at the door.

Sources