Published 14 Oct 202623 min read

Software factories: machines that create machines

Overview

When I say engineering factory, I mean something that takes an accepted issue and returns a change that someone other than its author has verified. The useful contract has acceptance at the start and independent proof at the end. Code generation is one step inside it.

What I find most interesting is what such a factory can produce. Once the loop handles ordinary fixes reliably, you can point it at a different kind of job: building another system that does bounded work, such as a reviewer, a test harness, or a teammate that tracks commitments from meetings. That is what I mean by machines that create machines. Here, the machines are software systems with defined jobs. I have no measured speedup to report.

My current setup combines shared skills, a repository wiki, Beads work tracking and an execution loop. The next step is a config-driven factory connecting each repository to its own Linear work. The plan is finished. I am holding the implementation until I have done my own QA on it, and runtime proof remains part of the implementation work.

The smallest factory

The issue states the accepted outcome; the coordinator organizes the run; the delivered change carries independent evidence.

An accepted issue is work that someone with authority has agreed should happen, with a stated outcome. The coordinator is the process that owns a run. It reads the issue, decides how to split it, and owns publication: integrating the accepted change and writing back to external systems. A verified change is a diff together with evidence that it does what the issue asked.

Later additions supply context, separate patch work from publication, and add independent checks. Ordinary worktree separation is already part of my workflow; the stricter confinement boundary in the kit is still proposed. The coordinator alone publishes, and the process that wrote a patch never gets to declare that patch correct.

What the coordinator reads

A new agent session needs access to the facts that make your repository understandable: its architecture, current work, commands and known failure modes. OpenAI's write-up on harness engineering makes a similar argument: their agents did better when the repository itself carried knowledge and feedback loops, instead of long prompts carrying them (OpenAI, harness engineering).

The first is versioned skill procedures. A skill is a folder containing a SKILL.md file with a name, a description and instructions. Claude Code reads the descriptions up front and loads a skill's full body only when a task calls for it (Claude Code skills). My shared procedures have one canonical home in a versioned repository. Product AGENTS.md and CLAUDE.md files point agents to those procedures and carry the product's own rules. This is DRY, "Don't Repeat Yourself", applied to instructions: change a shared testing procedure at its source. References and aliases can expose the same source to both tools; each tool still needs its own execution checks.

The second is a repo-local wiki and knowledge graph. Each repository keeps its own knowledge folder plus a code graph built with a tool called graphify. Before an agent searches the source, it asks the graph a question, such as where the export is built or what calls it. The graph narrows the search. The agent still reads the real lines before changing anything.

Both new boxes feed the coordinator and neither one publishes. They change what the coordinator knows and leave its permissions alone.

Claims and isolated hands

Knowing the repository stops being enough once two agents can touch it at the same time. The next layer adds a ledger and a worker.

The ledger is Beads, a small issue tracker that lives in the repository and is designed for agents. Every non-trivial run gets a goal Bead, and each independently shippable piece of the run gets a child slice Bead with explicit dependencies. A worker claims its slice before it touches code. Claims record ownership, and a Bead's notes hold evidence and resume context. An isolated Git directory alone cannot prevent duplicate dispatch. The planned factory needs one atomic claim authority outside disposable worker checkouts.

I run these commands every day:

# Inspect the ready-work queue before choosing a slice.
bd ready
# Read one slice: dependencies, notes, evidence so far.
bd show <slice-id>
# Claim the selected slice through the tracker.
bd update <slice-id> --claim
# List the isolated checkouts that exist right now.
git worktree list

Each worker runs in its own Git worktree on its own branch. A worktree is a second checkout of the same repository in a separate directory, so a worker's half-finished edits never show up in the coordinator's checkout or in another worker's. The worker receives a brief, its slice, and the few skills that slice needs. It returns a proposed patch or feature branch, its checks and findings. Integration and outward writeback remain coordinator jobs.

A skill called eil drives the whole sequence. Its first step is triage. A request that needs one step is answered inline, and a multi-step change goes through plan, execute, verify and close. Most of the restraint in the system sits in that triage step, because running planning, review and a worktree for a typo costs more than it saves.

This is the single responsibility principle applied to processes. The worker produces patches, the ledger records who holds what, and the coordinator decides what gets published. When one process takes on two of those jobs, failures can hide inside it.

Proof the author doesn't control

This layer adds two boxes. The first is independent review with cross-vendor routes. My configured Claude-origin runs request Codex for adversarial review; Codex-origin runs request Claude. The route follows the coordinating run, so a mixed-model run must also check who actually authored the change. A recorded request establishes the selected route; it does not establish hidden provider execution. The reviewer's job is to find a reason the change is wrong. Its findings go to the coordinator, which turns each actionable finding into a new Bead linked to the slice it came from. That way a finding can't disappear when a chat scrolls away.

The second box is evidence. The coordinator gathers it by replaying checks itself instead of trusting the worker's report. For a web change, that means local tests and a look at the deployed production page. A green build shows that the build passed. It doesn't show that users now see the corrected table.

I wrote about earlier versions of this loop in how I run EIL and do not read the code, build the feedback loop. Those posts let the loop learn and promote its own improvements more freely than I now consider safe. Today a learned change to a procedure starts as a candidate. It is evaluated on frozen fixtures, fixed tasks and scoring rules, and reviewed, and a person decides whether to promote it. I explain that boundary in more detail near the end.

Inventory, with caveats

As of 14 October 2026 my harness catalogues 311 canonical top-level skill packages: 269 ordinary packages and 42 symlinked ones. It also has 66 agent cards, six persistent digital teammates, and 48 mapped stage roles for the eil loop. These are installed and catalogued definitions. They don't tell you how often any of it runs, whether anyone has adopted it, or how well it performs.

The same audit found incomplete wiring in the contracts of all six teammates. Those gaps need correction and evaluation before I treat the catalogue as a proven operating system. An inventory number reported without its audit reads as a capability claim, and these numbers describe a catalogue.

Teammates, subagents and skills

Persistent teammates supply work and receive the outcome report. I call them digital teammates, or DTMs. The three kinds of instruction have different lifetimes:

  • A skill is a reusable procedure. It has no memory and owns no outcome.
  • A subagent is temporary. The coordinator spawns it for one slice, and it ends when the slice ends.
  • A teammate is persistent and owns an outcome over time. Its contract names durable state and an outcome to resume across runs. Persistent ownership is the intended behavior; a role card by itself does not prove it happens.

I configure six teammate roles. Product owns the problem and its value: whether something is worth solving and who it's for. Sales owns buyer proof, meaning evidence a buyer would accept. GTM, or go-to-market, owns distribution and product-market-fit experiments. The CEO gate owns strategy and allocation recommendations, and a person makes the final call. The technical lead owns building and proving requirements once they are accepted, and it drives the engineering factory. Meeting follow-ups owns commitments: who promised what, and whether it happened.

In the diagram, teammates shape and recommend issues on the way in and receive an outcome report on the way out. Acceptance stays with a person. Each teammate loads only the skills its current job needs. The technical lead has no reason to carry a sales playbook in context while it fixes an export. When several of these roles work on one goal I sometimes call the group a hive, and I mean only that the roles are collaborating.

Skills worth stealing

You don't need 311 skills. These are the twenty I would teach someone building their own factory, grouped by job. These are the skill names exposed in my setup; the procedures come from several sources.

JobSelected skillsWhat they supply
Persistent ownershipdtm-os, dtm-roster, dtm-technical-leadRole, state, outcome and authority contracts
AI deliveryframework-ai-evals-and-deliveryEvaluation and delivery evidence
Start and scopeeil, eil-plan, eil-plan-adversaryTriage, a bounded plan and risk-triggered challenge
Build and testeil-execute, eil-implementer, eil-testerExecution, patch work and tests
Independent acceptanceeil-qa, eil-verifierDiff review and proof against the original outcome
Land and closeeil-merger, eil-closeNormal PR path, evidence and lifecycle closeout
Procedure changesdtm-skill-evalCurrent skill, candidate and no-skill comparisons
Navigation and workgraphify, beadsCode relationships and durable work tracking
Explanationprogressive-systems-explainerA small model that develops into a usable system
Final prosehumanizer, stop-slopEditing after the facts and technical artifacts are settled

I'd start with these and add from the wider catalogue only when a real task needs something.

One task, before and after

Consider one recurring job: a report export emits duplicate rows.

In the before picture, I hold everything together. I brief the agent and paste in the context it lacks. I guess which tests matter. I carry review comments back and forth and keep the tracker in sync by hand. Each handoff costs my attention and leaves me carrying context between sessions.

In the after picture, the issue arrives as a contract with a stated outcome. The factory confirms who is asking, takes one claim, and builds the patch in isolation. It replays the checks independently, opens a PR scoped to the slice, and links the evidence on the issue. I review the result and accept it. Admission must reject the wrong workspace and avoid starting a second run for the same issue. Publication must reject a worker holding an out-of-date claim. These checks need transactional state and real failure tests.

I'm not putting a speedup number on this because I don't have one. A run costs my attention, compute, review time, rework, and the operations needed to keep the factory running. Whether the after picture is cheaper is something I still have to measure.

The planned repo-to-Linear factory

Today a person starts most runs. The plan is to let an accepted Linear issue start one.

All three new boxes sit at the edge of the system. Linear holds the issue, and a human owner approves its scope there. A signed webhook carries the change to an admission check, and only admitted work becomes an accepted issue for the coordinator. On the way out, the change links its evidence back to the Linear issue, and the owner accepts the PR.

One kit, many repositories

The plan is a single neutral, versioned Factory Kit shared by every repository. Each consumer pins a release; it does not fork the core procedure. A product repository holds a small config file with no secrets in it and pins a trusted kit release. Credentials and grants stay with the operator, outside the product repository.

The planned profiles share mechanisms but keep product rules and account boundaries separate:

Factory profileWork producedAcceptance differs here
Education productCoordinated backend and client changesBoth applications work; undeclared repos stay outside scope
Provider-neutral teammate productA persistent teammate runtimeIdentity, memory and executor behavior meet the same contract
Model-improvement productCode for dataset, training and evaluation workflowsEngineering delivery and scientific model acceptance stay separate
Private deliverables workflowResearched briefs and reviewable documentsEvidence and privacy checks; sending requires its own authority

These are planned deployments of the kit, not four operating factories. The second profile aims for provider independence, but switching a model name is insufficient: each adapter must prove the same refusals, recovery and completion rules.

The config below is illustrative. It shows the shape of the proposal, and no installed command reads it today.

{
  "kit": {
    "release": "factory-kit@<pinned-version>",
    "digest": "sha256:<trusted-release-digest>"
  },
  "lifecycle": "paused",
  "host": "<one-named-host>",
  "tracker": {
    "kind": "linear",
    "binding": "<this-product-only>",
    "status_writes": false,
    "phase_labels": "proposed"
  },
  "workers": {
    "per_profile": 1,
    "kit_max": 2,
    "stop_hook_timeout_s": 15,
    "stop_hook_writes": "outbox/untrusted"
  },
  "targets": ["proposed:report-export"]
}

The lifecycle starts paused, the targets are proposals, and status writes are off. The file has no account field, and that omission is deliberate: config can't switch the authenticated account. If the wrong identity is signed in, the operator fixes authentication outside the repository.

Setup works as a diff followed by an apply. The kit shows exactly what it would change, then applies it, and running apply again with nothing new does nothing. If I've edited a generated file by hand, apply keeps my edit. Before any of that, a doctor command checks identity first: which account is signed in, which workspace, which repository.

This is inversion of control and dependency injection applied at the system level. The kit's core depends on a tracker interface. The repository config names which binding applies, and the operator's environment injects the credentials. For automation, the plan proposes a deterministic, typed adapter over Linear's API. For interactive discovery, when I'm exploring a workspace by hand, an MCP connector, a Model Context Protocol interface exposing tools to the agent, helps. The two serve different jobs, and the coordinator shouldn't need to know which one sits behind the interface.

Identity is the hard part

Linear's agent platform is in developer preview. An agent authenticates as an app actor instead of as a human user, and it works through agent sessions (Linear agents). OAuth determines which identity the app acts as and which scopes it holds (Linear OAuth 2.0).

The review exposed three account boundaries. A desktop connector that works on my laptop doesn't prove that a headless worker, running without an interactive session, can authenticate, because they use different credentials on different paths. Each product gets its own verified Linear binding, and no binding is allowed to span two products. Separate admin tokens don't provide isolation, because an admin token keeps admin rights wherever it's stored. Isolation needs a restricted token or actor, plus an allowlist that runs before any request leaves the process.

Admission before work

Linear signs each webhook with an HMAC, a keyed cryptographic signature over the raw request body, and includes a timestamp, so the receiver can reject forged or replayed deliveries (Linear webhooks). Verification has to run on the raw bytes before any parsing. Admission comes after verification. The following is pseudocode for the proposed check:

# Proposed control flow, not an installed implementation.
def admit(raw_body, headers, binding, api, state, now):
    require(signature_valid(raw_body, headers, binding))
    event = normalize(parse(raw_body))
    require(abs(now - event.timestamp) <= MAX_SKEW_SECONDS)
    require(event.workspace == binding.workspace)
    require(event.issue_id in binding.allowed_issue_scope)

    # Verify the execution identity, not merely the event's author.
    require(api.current_identity_matches(binding))
    issue = api.read_allowed_issue(event.issue_id)
    require(current_approval_matches_scope(issue, binding))
    require(api.dependencies_done(issue))

    # One transaction deduplicates delivery and acquires ownership.
    # The coordinator creates the generation; the webhook cannot choose it.
    return state.reserve_once(
        delivery=event.delivery_id,
        issue=issue.id,
        approved_scope_digest=issue.scope_digest,
    )

# A delayed worker must still own the current generation at publication.
def publish(result, state, api, binding):
    require(api.current_identity_matches(binding))
    issue = api.read_allowed_issue(result.issue_id)
    require(current_approval_matches_scope(issue, binding))
    require(issue.scope_digest == result.scope_digest)
    require(state.is_current(result.claim, result.scope_digest))
    require(independent_evidence_accepts(result))
    return prepare_scoped_pr(result)

The API adapter verifies the signed-in runner before fetching permitted issue content. A transaction records delivery and ownership together, so two concurrent events cannot both pass a separate read-then-claim check. Publication also needs a current claim and scope. These functions illustrate the contract; leases (expiring ownership), retries, cancellation and crash recovery remain implementation work. A denial should leave evidence, so I can see why nothing happened. In this illustrative publication denial, the worker already ran, but its ownership expired before it returned:

{
  "decision": "denied",
  "reason": "stale claim generation",
  "issue": "EXP-41",
  "generation": { "held": 4, "presented": 3 },
  "worker_started": true,
  "published": false
}

This is the event-driven part of the design. The webhook is an event, and the admission check is its one consumer. Publish/subscribe requires another boundary. Pub/sub decouples publishers from subscribers through a topic or broker. A single HTTP receiver demonstrates event-driven ingress; it does not establish that broader architecture. If I later want separate consumers, for example one that admits work and another that maintains a metrics journal, that will be its own design with its own delivery and ordering guarantees.

Workers can't reach the coordinator

The plan starts with one host, named in config. It runs one worker per profile and no more than two kit workers at once. Workers, and any child processes their hooks spawn, are confined away from the coordinator's credentials, claims and journal.

Claude Code hooks run commands at lifecycle events, including the point where an agent stops (Claude Code hooks). A worker's Stop hook gets at most 15 seconds and can write only to an outbox of untrusted proposals. The coordinator reads the outbox, verifies the proposals independently, and then publishes. The coordinator checks a worker's report against independent evidence.

The plan leaves three things out: automatic enrollment into a private fleet catalogue, automatic kit upgrades, and any guarantee about running across several hosts.

What the board may show

Linear boards group issues into columns, usually by status, and custom views let you filter that layout and save it (board layout, custom views). It's tempting to let the factory move cards between columns, and the plan rules that out for now.

Native status stays under human control unless I can prove that an update is atomic and safe. The factory may show its own phase with mutually exclusive labels (ready, running, review, verify, blocked), and only after I've proven the exact label operations are safe. If Linear's API would replace the entire label list and wipe out a label a person added a second earlier, the view stays a private, read-only draft. A journal and a readback do not create compare-and-swap, an update that succeeds only if the stored version still matches the version you inspected. A person could add a label after the factory reads the issue and before it writes. The readback could confirm the factory's write while that human edit has already been lost.

Columns show state. Permission to work comes only from admission: an approved scope, a verified actor, the right repository, finished dependencies, and a current claim generation. Each team keeps its existing workflow, and the factory adapts to it.

This rule is stricter than an older plan of mine, in which the factory proposed status changes automatically. In MVC terms, the board is a view, the ledger and journal are the model, and the coordinator is the controller. The controller enforces the update contract; moving a card cannot bypass admission or authorize a worker.

What review caught

The plan went through adversarial review before any code was written, and the review caught concrete failures:

  • Inherited lifecycle hooks could let a worker trigger coordinator behavior.
  • Separate tokens for one admin actor would retain the same broad account reach.
  • The proposed worker hook boundary did not exclude coordinator state.
  • An automated status update could overwrite a status a person had just changed.
  • During publication, closeout actually removed a held review checkout; the session restored it and recorded the retention defect.

The first four findings changed the design. The closeout incident is a recorded implementation defect. Runtime confinement, account access, restart handling and safe tracker updates still need direct proof.

Machines that build other machines

Everything so far describes an engineering factory, where roles plan, edit, test and review versioned software. A product factory runs domain tasks on the same skeleton.

I want the output of the engineering loop to become another working system. The engineering factory produces the code for a model-improvement system; that system produces a model candidate. Each output has a separate acceptance boundary.

I have one planned for a model-improvement product. The factory generates a dataset it is permitted to generate, trains a candidate model, and evaluates it against a development set while it iterates. Then the candidate is frozen. Only after the freeze does a final holdout run, on cases nothing in the loop has seen, with the base model and the candidate evaluated on the same cases. A scientific owner accepts or rejects the result. This pilot is not running. I'm not claiming the trained model is better, and nothing is being served.

Keeping the final exam independent

The product needs an independent examination path. Development scores guide iteration; the final holdout belongs outside the loop that selects the candidate. A poor final result can correctly end in rejection.

The engineering factory applies the same discipline to its own procedures. A candidate change to a skill runs on frozen delivery fixtures in three arms: the current skill, the candidate, and no skill at all. A person approves the candidate before it replaces the current version. That comparison is the job of dtm-skill-eval.

Both loops follow one rule I won't relax: neither can edit its own final grader or its own permissions. If a loop could rewrite the test that judges it, a passing result would tell me nothing.

How I'll know if it's worth it

The pilot is deliberately small: one repository, one named host, and ten comparable tasks like the duplicate-rows fix. For each task I'll record whether the outcome was accepted, how many human minutes it took, lead time from acceptance to merged change, rework, total cost including compute and operations, and any incidents. If the drop in human minutes and rework doesn't cover the cost of operations and review, I'll stop the pilot. Saving typing while adding attention cost would be a bad trade.

If you want to build one yourself, start smaller than I did. Put one procedure in a SKILL.md, give every agent its own worktree, keep a ledger of claims, and make sure the process that publishes is separate from the process that writes. Then send one real task through it and count the minutes you spent on it.