The Autonomous Software Factory

Trusting Code No Human Wrote

Presented live around a running demo. The embedded panels showed a local harness that isn’t running here — each one is captioned with what it displayed on the night. If a diagram fails to draw, refresh the page.

Roger & Renate Rössing · Deutsche Fotothek · CC BY-SA 3.0 DE

Zechariah Judy · via Wikimedia Commons · CC BY 2.0

NASA Glenn Research Center · public domain

Evergreen Systems, 1987 · public domain

We’ve automated labour before.

For the first time, we’re automating the building of the software itself. 

How do we do it securely?

Background

How software has been built

One role decides what to build; another turns it into working software.

Product Manager

Owns what to build, and why: requirements & roadmap.

market · business · feedback · intuition

spec
requirements
implement
test
release

Software Engineer

Tracking the work

The unit of work is a card. It flows left to right through fixed stages, owned by a human at every step.

Backlog

Password reset flowPROJ-241
Export to CSVPROJ-238
Dark modePROJ-233

To Do

Rate-limit the APIPROJ-229
Audit log tablePROJ-226

In Progress

OAuth loginPROJ-217 · Mei
Search indexingPROJ-220 · Sam

Review

Billing webhookPROJ-212 · PR #84

Done

Avatar uploadPROJ-205
Email verifyPROJ-201

https://www.testaify.com/blog/a-comprehensive-history-of-cicd

Context

  • The working memory of the LLM
  • Append only
  • The ‘Dumb Zone’
  • Compaction is bad
Context usage terminal output

Context Window

sys user tool asst user tool tool asst ▣ summary user tool

Agent

def run_agent(user_goal):
    context = [{"role": "user", "content": user_goal}]

    while True:
        # 1. REASON: Ask the model what to do next
        response = llm.call(messages=context)
        context.append(response)

        # 2. ACT: Check if the model wants to use a tool
        if response.tool_calls:
            for tool in response.tool_calls:
                result = execute_tool(tool.name, tool.args)
                # Feed the result back into memory
                context.append({"role": "tool", "content": result})
        else:
            # 3. STOP: Goal achieved, or model is done
            return response.content
  • LLM with some tools
  • Dynamic Decision Making
  • Feedback Loop
  • Exit condition

Tools

A tool is just a function the model is allowed to call, described to it as a JSON schema.

{
  "name": "get_weather",
  "description": "Current weather for a city",
  "parameters": {
    "type": "object",
    "properties": {
      "city": { "type": "string" }
    },
    "required": ["city"]
  }
}
  • The schema is the whole interface: to the model, prompt.
  • It emits a JSON call; your code runs it.
  • Whether it runs is your call: where trust begins.

The model only ever asks. You act.

System prompt

The first, highest-priority message. Sets role, constraints, voice and output contract before the task is even seen.

soul: security-reviewer-go
role: qa
model: claude-opus-4-8
sandbox: go-toolchain@sha256:1c2…
tools: [read_file, search, submit]
system_prompt: |
  You are a senior Go security
  reviewer. Find vulnerabilities in
  the diff: injection, auth bypass,
  secrets committed in code.
  Cite file:line and the exploit;
  do not fix. Be terse. No praise.
  • Same model, same loop: the prompt is what makes one agent a planner and another a security reviewer.
  • It steers every turn that follows, but writes nothing back.

Persona

The stable identity that prompt encodes: its expertise, its tone, what it refuses to do.

soul: security-reviewer-go
role: qa
model: claude-opus-4-8
sandbox: go-toolchain@sha256:1c2…
tools: [read_file, search, submit]
system_prompt: |
  You are a senior Go security
  reviewer. Find vulnerabilities in
  the diff: injection, auth bypass,
  secrets committed in code.
  Cite file:line and the exploit;
  do not fix. Be terse. No praise.
  • Who it is (“senior Go security reviewer”) and how it behaves (“be terse, no praise”).
  • Change the persona, keep the loop: you have a different agent.

Soul

Persona + model + tools + sandbox, captured as declarative config. (We’ll come back to this.)

soul: security-reviewer-go
role: qa
model: claude-opus-4-8
sandbox: go-toolchain@sha256:1c2…
tools: [read_file, search, submit]
system_prompt: |
  You are a senior Go security
  reviewer. Find vulnerabilities in
  the diff: injection, auth bypass,
  secrets committed in code.
  Cite file:line and the exploit;
  do not fix. Be terse. No praise.
  • Stateless: no cross-task memory, reconstituted fresh on every invocation.
  • One loop, many souls: identity is configuration, not code.

Harness

The scaffolding that makes an agent safe to run.

  • Loop Control
  • State & Memory Management
  • Tool Execution & Sandboxing
  • Guardrails & Safety
Pi Harness Claude CLI Claude Code

https://steve-yegge.medium.com/welcome-to-gas-town-4f25ee16dd04

Prior Art

Beads refinery Welcome to Gas City OpenClaw Agent persona Operator

A factory is just a CI/CD pipeline where the build steps think.

Humans author specs.
Everything merged to main is autonomous.

… but the build steps are hostile-by-assumption LLM agents.
How do you trust what they merge?

▶ live demoVault — the deployed result

The app the factory built, opened live on stage. Every line of it was written by agents from a human-authored spec.

▶ live demoThe Create-Task wizard

Where a human authors the spec. The only place a person writes anything — everything downstream of it is autonomous.

Screenshot 2026 07 06 at 1.35.33 pmScreenshot 2026 07 06 at 1.35.33 pm

Architecture

Six choices that buy back the trust

The pipeline

A human writes the spec. From there every stage is an agent, and the graph only moves forward when the orchestrator says so.

flowchart LR H(["human: spec"]):::t --> P["plan"] --> AT["author-tests"] --> I["implement"] --> QA["qa / security"] --> M[["integrate → main"]]:::t QA -. "on_failure" .-> I I -. "on_failure · retry" .-> I classDef t fill:#2b6cb0,stroke:#2b6cb0,color:#fff;

Emergent breadth, declarative depth: an agent proposes how many children a stage spawns; config fixes what stage comes next.

Stages compose through contracts

What moves a card forward is not the agent saying so; it’s the orchestrator checking guards the stage declares. The agent does the work; the orchestrator decides whether it counts.

implement:
  precondition:  blockers-closed
  postcondition: [tests-red-then-green]
  on_failure:    implement   # no human, must route
  produces:      [qa]
qa:
  postcondition: [tests-pass, "mutation>=0.8", gosec]
  on_failure:    implement
  produces:      [integrate]
  • Precondition: must hold to enter (usually blockers-closed).
  • Postcondition: must hold to be accepted, checked in a fresh verification sandbox.
  • on_failure: mandatory route when a gate fails. No human in the loop, so every gate needs a destination.

The guards are abstract predicates: tests-red-then-green says what must be true, never how to check it.

The config behind this demo

The actual pipeline I’m running. Each stage names abstract guards; cards only advance when the orchestrator says they hold.

dag:
  plan          →  decompose the spec
  author-tests  →  tests fail (red)
  implement     →  tests red → green
  qa            →  tests · lint
                   · static analysis
  integrate     →  autonomous → main
  • The graph speaks in predicates: what must be true, never how to check it.
  • qa fails → on_failure routes back to implement. No human in the loop.

… bound to real tooling

One registry maps each abstract predicate to the command that decides it.

checks:       # abstract → real tooling
  tests            go test
  compiles         go build
  lint             golangci-lint
  static analysis  gosec
                   · govulncheck
                   · licenses
  • Swap the command, the DAG is unchanged.
  • The gates are just existing tools: nothing reinvented. Leverage what already works.

Design Principles

  1. Learn and have fun
  2. Customizable framework over opinionated workflow
  3. Distributed in nature
  4. Sandboxed
  5. Leverage existing techniques
  6. Strong provenance & decision-record keeping
  7. Resilient

Untrusted by default

Every agent runs in a zero-network sandbox; the runner brokers all I/O. The agent never holds a key or touches the wire.

Blocks code that exfiltrates secrets, phones home, or pulls malware: there’s nothing to phone with.

On the board: every card you see is executing inside its own isolated sandbox.

Producer ≠ verifier

Tests are authored by a different soul than the implementor, and gates run in a fresh sandbox the orchestrator controls, never the one that wrote the code.

Blocks an agent marking its own homework. Independence is what makes a green check mean something.

On the board: author-tests and qa are separate cards, owned by separate souls.

Single writer

Only the orchestrator writes the work graph. Agents propose mutations via their Result; each is validated as DAG-legal before it’s applied.

Blocks an agent rewriting the graph to delete its own gate or jump straight to merge. It can ask; it can’t write.

On the board: cards only move when the orchestrator says so. No agent moves itself.

Emergent within, declarative between

Agents decide how many work items a stage produces; config decides what stage comes next. An emergent issue-DAG inside a fixed role-flow.

Buys bounded creativity: work fans out as far as the problem needs, never off the rails the config lays down.

On the board: watch one issue fan into many during plan.

Bounded autonomy

Budgets and retry caps aren’t cost control; they’re the termination guarantee. Breach a budget and the work dead-letters for a human to triage.

Blocks an agent that loops forever, burning money and never halting. The budget is what promises it stops.

On the board: the budget meter, and stuck work landing in the DLQ.

Provenance by construction

Every change traces issue → soul → model → prompt → evidence, produced as a by-product of how the work is done, not logged after the fact.

Why you can’t trust a merge you can’t trace. Provenance is what lets a human re-enter and audit any decision the factory made.

On the board: every merged card links its full decision trail.

Under the hood

The mechanisms behind the six choices

The sandbox

One work item, one sandbox, destroyed after. The backend is config, not code:

BackendIsolationStartupUse
Docker shares the host kernel (weak) fast local dev only
gVisor user-space kernel medium medium-trust
Firecracker own kernel (KVM microVM) ~125 ms production target

Images are digest-pinned: a soul names a logical profile, infra resolves the digest, and the digest rides in provenance.

The trust boundary

One chokepoint: the sandbox can only reach its runner; the runner brokers everything else.

flowchart LR subgraph SB["producer sandbox · zero-network · untrusted"] A["agent"] end subgraph TR["trusted layer"] R["runner · holds key + adapter"]:::t O["orchestrator · sole beads writer"]:::t end subgraph VB["verification sandbox · fresh · deny-all"] V["gate · re-runs candidate"] end subgraph EG["allowlisted egress · logged · deny-by-default"] X1["model API"] X2["git · task branch only"] X3["Go module proxy"] end A <-->|"local socket · the only channel"| R R <-->|"NATS"| O R --> EG O -->|"provisions · producer ≠ verifier"| V classDef t fill:#2b6cb0,stroke:#2b6cb0,color:#fff;

Zero network, zero credentials, one allowlisted window — and that window doubles as the observability collector: every call, span, and transcript flows through it.

The whole record, live

▶ live demoOpenObserve — the whole record

Live traces, logs and metrics from the running pipeline: every agent action, tool call and gate decision, end to end.

One orchestrator, many runners

One orchestrator hands work to a queue; any number of runners, across any number of hosts, compete to pull it.

flowchart TB O["orchestrator · single scheduler · sole beads writer"]:::t JS[["NATS JetStream · work · results · dlq · approvals"]]:::q O <-->|"dispatch work · collect results"| JS subgraph HA["host A"] R1["runner · agent in sandbox"] R2["runner · agent in sandbox"] end subgraph HB["host B"] R3["runner · agent in sandbox"] RN["runner N · agent in sandbox"] end JS <-. "pull · compete · ack = lease" .-> R1 JS <-.-> R2 JS <-.-> R3 JS <-.-> RN classDef t fill:#2b6cb0,stroke:#2b6cb0,color:#fff; classDef q fill:#eef5fb,stroke:#2b6cb0;

Souls

Soul = identity

Declarative config: name, role, model, persona, tools, sandbox profile. Stateless: no cross-task memory, reconstituted fresh every invocation. All durable state lives in beads, git, and specs.

Agent = behaviour

Everyone runs the same loop. A role’s behaviour is its persona plus which tools are enabled: planner, test-author, implementor, and security agents all share one loop.

Souls fulfil roles, chosen per-issue by a selector; you scale by adding runners, not souls. This is what makes producer≠verifier concrete: implementor-go is a different soul from test-author-go.

No framework

No agent framework, no Claude Code: the loop, the tool dispatch, and every schema the model calls are custom.

Read

read_file · list_dir · search

Semantic · gopls

find_symbol · references · diagnostics …

Edit

write_file · edit_file · rename · code_action

Execute

run · run_tests · run_gate

Trace

trace_test → test↔spec map

Propose

submit_plan · submit · request_subtask · escalate

Explore — a sub-agent as a tool

explore: broad question → distilled {summary · file:line anchors · leads} — a nested loop on a cheap model; it burns its own context, not mine

When a gate fails

No human reads the log — a red gate has to be actionable.

Red gate

build first · tri-state · never a false green

Typed findings

file:line · rule · severity — raw output kept as evidence

Brief → retry

the next attempt sees the findings — the loop learns

Provenance, for real

Every merge to main is a harness-authored, SSH-signed provenance commit:

Soul: implementor-go | Model: claude-opus-4-8 | Tests-Soul: test-author-go
Issue: bd-1234 | Prompt-SHA: 9af… | Verified: build@sha256:1c2…,test@sha256:8be…,gosec@sha256:0a4… | Traceability: sha256:7c1… | Transcript: sha256:3d2…
  • Every hash → a content-addressed artifact store: auditable to the exact bytes.
  • Transcript = a replayable decision trail.
  • One signature authenticates every cited artifact.
▶ live demoDeployed result

The same app after its spec was merged to main and shipped — the far end of the provenance trail.

Spec in, merged to main, deployed, with a full provenance trail

What’s next?

You don’t trust the agent.
You trust the harness around it.

Untrusted by default · producer ≠ verifier · provenance by construction

lochieashcroft.com