Headless Runs

Every lesson in this course builds one system: a stablecoin reserve attestation registry that discovers issuers, reads the PDFs they publish, and refuses to publish a field it could not read. What it is and where this lesson sits.

Tier
3 Pro
Build position
9
Throughlines
D Execution surface · F Determinism wrapping stochasticity · G Feedback loops · E Tool surface and trust boundary
Verdict
load-bearing
Commit
20f7d85
Demo
scripts/terms/demo-18.ts
Runs in
the course repository

Removing the human does not simply make a run unattended. It changes what the run is allowed to do, because every place the code would have asked is now a place it must decide.

The failure it fixes

The interactive loop has a person in it, and that person is load-bearing in ways nobody writes down. They approve tool calls. They notice a question. They stop a run that has clearly gone wrong. Take them out and the code does not become autonomous; it becomes a program whose specification has silent holes in it.

The dangerous hole is permissions. An interactive config marks a risky tool ask, which is a perfectly good answer when someone is there. Run that same config headless and ask has to mean something. Every implementation that treats it as allow has just granted the run the exact tool surface its author flagged as needing approval, and the config file still reads as cautious.

The second hole is ambiguity. An attestation whose composition table does not parse is, interactively, a question. Headless, if it is not explicitly a refusal, it becomes a guess. The run completes either way.

Break it first

Take a config that is fine interactively and start a headless run with it:

$ npx tsx scripts/terms/demo-18.ts
--- configs that must refuse to start
  ok   an 'ask' permission: permissions contain "ask" for Write, but a
       headless run has nobody to ask.
  ok   no budget: a headless run needs a positive budget declared before it starts

assertHeadlessSafe throws before anything executes. An ask in a headless config is not a nuance to interpret at the call site; it means the config was written for a different execution mode and has not been reviewed for this one.

The mechanism

src/headless.ts runs a step list against a declared permission map, putting term 15's hook in front of every step that passes the permission check. Four behaviours are the opposite of their interactive defaults.

An undeclared tool is refused, not defaulted. The set of tools a run may use has to be something a person decided, not a consequence of what the code happened to call.

A question becomes a refusal with a recorded reason. There is no one to ask, so the only honest alternatives are refuse or guess.

Unresolved work forces a non-zero exit. The run completed. It did not succeed, and nobody was watching to tell the difference.

The artifact is the entire output. Whatever is not in it did not happen, including the reason for every refusal.

Build it

ran      PublishRegistryRecord    permitted and passed every hook rule
REFUSED  PublishRegistryRecord    field(s) published while listed unreadable
REFUSED  Bash                     tool "Bash" is denied by config
REFUSED  SendEmail                tool "SendEmail" is not declared in permissions

human interventions : 0
refusals            : 4
unresolved          : 3
exit code           : 1

.claude/settings.json carries the permission map, every entry allow or deny, with a comment saying why none of them is ask.

The receipt

artifacts/18-headless.json. Twelve tests and three mutations, each failing exactly the tests that should catch it:

undeclared tool defaults to allow          -> 2 failed | 10 passed
"ask" accepted in a headless config        -> 2 failed | 10 passed
unresolved no longer forces non-zero exit  -> 2 failed | 10 passed

Where it overlaps

Line D is execution surface at its sharpest: this is the term where work stops happening in a place a person is looking at. Line E is the trust boundary, and this is the term that earned its mark in the Stage 1 recount, because headless removes the approver and the reachable tool surface widens as a direct consequence. Line F is determinism: with no human to vary the run, the config and the hook are the only things deciding, and both are deterministic. Line G is feedback, because the artifact is now the only channel the run has.

This term closes the Stage 3A slice. Term 19, next in build order, fans these runs out in parallel, and the pairing of term 15's hook with that fan-out is interstitial II.

Sharp edges

  • ask is invalid, not coerced. Any implementation that maps it to allow or deny at the call site has made a policy decision in the wrong file.
  • Zero refusals is a suspicious result, not a clean one. A run over real issuer PDFs that refused nothing has probably not checked anything; the demo's own run refuses four of eight on purpose.
  • humanInterventions: 0 is asserted, not assumed. It is in the artifact so that a run which secretly prompted somewhere fails the gate rather than passing quietly.
  • Determinism is tested. The same input twice must produce byte-identical artifacts, or the run cannot be bisected when it starts producing bad records.
  • The permission map is not a security boundary. It constrains a cooperating runner. A tool that writes by another route is outside it, which is the same limit term 15 has and an argument for a narrow tool surface.

A personal teaching project, in development. The registry it builds is a teaching artifact and is not an assurance opinion about any issuer. Not affiliated with or endorsed by any employer, or by any vendor whose tools it describes. Where a unit depends on a specific flag or path, it names the version it was verified against. All units.