Delivered
What aof provides today, stated per accepted item from that item’s own outcome record – 389 items, 1728 capabilities. Newest first; a milestone precedes its stories. Each line is product state, not intent.
- 190 · A tree is tested once per change, and every sign-off door reads that run’s record instead of running it again
- 190/00 · A repeat run is answered from its record
- 190/01 ·
impactednarrows on what it knows - 190/02 · A gate that did not finish resumes
- 190/03 · The doors stop prescribing repeat runs
- 190/04 · An existing project is told what its test declaration leaves out, and one skill declares it
- 186 · Core names no integration package outside the registry
- 185 · Notion’s effects, routing and store metadata reach core through the contribution
- 184 · Notion’s verbs reach core through one contribution
- 183 · Global issue log
- 182 · Renumber work items
- 181 · Unattended loops survive declines
- 180 · One memory node in the config
- 180/01 · The memory node holds backend, corpora, graph and per-engine settings, and one module reads it in either spelling
- 180/02 · aof work update rewrites an old-shape memory config to the new node, and aof work init writes only the new shape
- 180/03 · The config editor has a Memory section that shows the node in the new shape and saves only the new shape
- 179 · One knowledge engine behind both memory faces
- 179/01 · The knowledge-engine port answers four verbs and routes each by a backend’s declared corpora
- 179/02 · graphify serves the code corpus behind the port, and aof graph build and impact answer through it unchanged
- 179/03 · The record corpora sit behind the port, and aof memory answers byte for byte as it does today
- 179/04 · Test selection, contract derivation and seam liveness ask relations instead of parsing the graph
- 178 · Memory is its own command
- 178/01 · aof memory is the command and aof work memory its alias
- 178/02 · Every bundle prompt, skill and doc spells aof memory
- 178/03 · A recalled work record names the planning document its item came from
- 175 · aof on macOS
- 175/00 · Quit reaps the whole tree on macOS
- 175/01 ·
install-localbuilds the app on macOS - 175/02 · The desktop verbs install, run and stop the app on macOS
- 175/03 · The preflight checks the Mac
- 175/04 · The Mac runbook and acceptance
- 174 · A built graph is shared through the project’s own remote
- 172 · Excalidraw phase 3 – the architect conversation streams into the editor, through a designed UI
- 172/00 · The architect answers in a headless session
- 172/01 · Every conversation in one panel
- 172/02 · The answer streams into its thread
- 172/03 · The diagram surface has no terminal
- 172/04 · A right-click leaves the drawing true
- 171 · Excalidraw phase 2 – the editor follows the architect live and is easier to drive
- 171/00 · Ask the architect from the canvas
- 171/01 · Find a library item by name
- 171/02 · Review records are pushed
- 171/03 · Drafts in the house style
- 171/04 · A note reaches the open session
- 171/05 · A note is a thread beside its shape
- 170 · Excalidraw phase 1
- 170/00 · The native draft store and the review loop
- 170/01 · Diagramming is a package pair
- 170/02 · The opt-out seam
- 170/03 · The editor is a route
- 170/04 · A first draft from Mermaid
- 170/05 · Notes without a session
- 170/06 · One command examines every note
- 170/07 · The served-path smoke
- 170/08 · One format
- 170/09 · Project libraries
- 169 · Hyperagent plans the work
- 169/00 · Hyperagent plans the work
- 169/01 · Integration packages are self-contained
- 169/02 · The Hyperagent package lives in the aof home
- 169/03 · The Hyperagent package carries one zip
- 168 · A prototype builds something fast
- 168/00 · A prototype is a work item
- 168/01 · One verb builds a prototype
- 168/02 · A prototype draws its diagrams
- 168/03 · The board shows a prototype
- 167 · The daemon stays within a resource budget
- 167/00 · The daemon says what it costs
- 167/01 · An unchanged work tree costs a stat walk
- 167/02 · An idle view costs almost nothing
- 167/03 · A permanent fault is said once and backed off
- 167/04 · The budget is proven by a soak test
- 167/05 · The desktop shows a sustained overrun
- 167/06 · Local runs reach the store through the ledger
- 167/07 · An idle tick walks nothing
- 166 · A built graph travels to another machine
- 165 · Planning runs in Cowork
- 165/00 · The planning lock records each runtime
- 165/01 · The planning skills render as a Cowork plugin
- 165/02 · The pinned planner is packaged for Cowork
- 165/03 · Planning init delivers Cowork
- 164 · Every project change reaches the global store
- 164/00 · A local change is noticed and owed
- 164/01 · The desktop app syncs every workspace and catches up when it starts
- 164/02 · My own items read from my files
- 164/03 · Owed sync never gives up and shows how far behind it is
- 163 · The operator can read the loop’s open questions
- 162 · A running loop is visible in the UI
- 161 · The compiled wiki stays true
- 161/01 · Wiki source citations are checked by doctor
- 161/02 · The memory wiki describes the delivered procedure
- 160 · The whole-tree suite signs off in minutes
- 160/00 · The loop reads each git fact once per step
- 160/01 · Tests that are not about git run on a git model
- 160/02 · A focused run loads only what it names
- 160/03 · A child process compiles once and loads only its verb
- 160/04 · The gate launches clean and dies with its session
- 160/05 · The runner enforces the budget, and a timeout is not-finished
- 160/06 · Citation history is read only when a citation misses
- 160/07 · A test’s child aof runs from one bundle
- 160/08 · Tests spend git only on what they assert
- 160/09 · Small suite files share a unit
- 160/10 · The audits over this repository read once
- 160/11 · A test that needs a suite run starts the shard entry
- 160/12 · Tests keep their state their own
- 160/13 · The whole tree runs on every pull request
- 159 · The operator’s auto-memory is an opt-in memory source
- 159/01 · Recall the operator’s local auto-memory beside work-stream lessons
- 158 · Native permission approvals
- 157 · Episodic memory is recallable
- 157/01 · Episodic records resolve to their sources
- 157/02 · Every consumer accounts for the episodic pool
- 157/03 · A retry, a triage and a retrospective recall what happened before
- 156 · One loop selects an assistant per phase
- 155 · One agent-mode setting governs every session
- 154 · Codex drives the AOF loop alongside Claude
- 154/00 · A shared session boundary preserves Claude execution
- 154/01 · Runtime and model choices survive resume
- 154/02 · Codex executes a bounded phase through App Server
- 154/03 · Codex discovers correctly scoped native assets
- 154/04 · Codex upgrades preserve operator-owned files
- 154/05 · Shared workflows render assistant-specific instructions
- 154/06 · Codex drives and recovers the existing work loop
- 154/07 · Workers retain the selected runtime and assets
- 154/08 · Codex activity and usage are reported honestly
- 154/09 · Codex projects can choose an explicit memory backend
- 154/10 · The config editor explains effective assistant settings
- 154/11 · Live acceptance proves Codex and preserves Claude
- 153 · Memory closes the loop
- 153/01 · Recalls have complete verdict records
- 153/02 · Lessons name recurrence and promotion
- 153/03 · Memory reports effectiveness and health
- 153/04 · Workflow edges close their memory observations
- 153/05 · Tune proposes promoted prompt corrections
- 152 · Promote shows what to promote next
- 151 · aof:add-diagram draws the architecture diagrams a refine left undrawn
- 150 · aof:explain says what a work item is for, without writing anything
- 149 · Manual mode: continue guides the operator who builds the work themselves
- 148 · The memory corpus holds its vocabulary
- 148/01 · The ranking is held by an eval
- 148/02 · A lesson’s meta line is normalised on read
- 148/03 · Story retrospectives are indexed
- 148/04 · A live lesson’s meta line is held
- 148/05 · Memory status reports conformance and layers
- 147 · The loop hands a halt to a fresh session to fix, then resumes
- 146 · A capture can skip the backlog
- 145 · A milestone’s loop plan can be drawn
- 144 · The whole-tree test run signs off in minutes
- 143 · The loop starts from the backlog, refines a whole item in one pass, and runs each phase on the model the operator chose
- 143/00 · The loop promotes a backlog ref
- 143/01 · A whole-item refine in one drive
- 143/02 · One grammar for per-phase session choices
- 143/03 · The loop runs each phase on the chosen model
- 141 · Subagents and loops think at a chosen effort
- 140 · Refine runs solo unless told otherwise
- 139 · Shatter lands its drivers in the backlog
- 138 · The session driver sees claude’s screen
- 138/00 · The driver reads the screen
- 138/01 · Blocking screens are named
- 138/02 · A live session proves it
- 137 · AOF.md has a template like every other record doc
- 136 · Discovery questions in the loop
- 136/01 · A loop answer anchors the example
- 136/02 · A driven refine asks through the loop
- 136/03 · A pending ask is read from the hook
- 135 · Key examples in the contract
- 135/01 · The practice is its own package
- 135/02 · The parser reads
Rule:as a group - 135/03 · The board groups scenarios by rule
- 135/04 · An agreed example cannot fall out
- 135/05 · The contract is formulated from the map
- 134 · Discovery before formulation
- 134/01 · The baseline is counted
- 134/02 · The map is a document
- 134/03 · The answer is read from the harness
- 134/04 · The readiness gate
- 134/05 · The discovery beat
- 133 · Architecture diagrams
- 133/01 · The seam is named in config
- 133/02 · Export writes the SVG and the PNG
- 133/03 · The gates know about diagrams
- 133/04 · The console shows it
- 133/05 · The architect draws
- 133/06 · The live draw
- 132 · Run records carry the node id, not the node name
- 131 · The human in the loop
- 131/01 · The question is read and recorded
- 131/02 · The notifier and its channels
- 131/03 · The session waits and the loop keeps going
- 131/04 · The answer reaches the session
- 131/05 · The board shows the question and takes the answer
- 131/06 · The register
- 131/07 · The live run
- 131/08 · The messaging CLI
- 131/09 · The bot posts
- 131/10 · Answer by replying in Discord
- 131/11 · Slash commands
- 131/12 · A worker’s ask reaches Discord
- 131/13 · Test and allow from the CLI
- 131/14 · One channel, several projects
- 130 · Stop a running loop
- 130/01 · The stop request has one home
- 130/02 · The verb and the shell honour it
- 130/03 · The fleet sees and stops it
- 130/04 · The desktop stops what it supervises
- 130/05 · The register
- 130/06 · The live stop
- 129 · Loop concurrency
- 129/01 · The mode and the engine decide
- 129/02 · The drive is a child
- 129/03 · The lane commits and merges home
- 129/04 · The wave tick
- 129/05 · The account and the register
- 129/06 · The second live run
- 129/07 · The loop’s settings are self-contained
- 128 · work memory joins the route table
- 127 · Backlog and archive – the work tree holds what is live
- 127/01 · One enumerator, three roots
- 127/02 · Promote mints the number
- 127/03 · Archive is a move
- 127/04 · The fleet and the board see the shapes
- 127/05 · This tree holds what is live
- 126 · The declaration is the unit
- 126/00 · The loop says what it is doing and counts what it did
- 126/01 · run-status renders what the record holds
- 126/02 · The declaration predicate and its door
- 126/03 · The supervisor reconciles a supplied set
- 126/04 · The installer fixes the daemon environment
- 126/05 · The warning has one home
- 126/06 · The preflight names the missing heartbeat hook
- 125 · The loop graph gets a published face
- 124 · The edges aof does not draw
- 124/00 · The census reports its denominator
- 124/01 · Cap exhaustion returns to the plan
- 124/02 · The learning edge reaches every cut
- 123 · The review close mints no driver
- 121 · The Import Specifier Extractor Has Twenty Homes And 119 00 Added The Twentieth
- 120 · Ff 11902 Refuses A Narrower Species Than Its Register Row Claims
- 119 · The tree gets an interior
- 119/00 · Rule the guards that forbid the fix
- 119/01 ·
src/gets an interior - 119/02 ·
src/commands/gets an interior, and the registry stops explaining itself twice - 119/03 · The test tree gets an interior
- 119/04 · The mesh god-node is split
- 118 · Finding triage weighs what a driver costs
- 117 · The Marker S Ref Is A Placeholder So The Copied Line Still Does Not Run
- 116 · The command layer is imported upward by loop-record-render
- 115 · The Contract And Register Condensers Still Emit A Husk At A Tight Budget And Boundedfill Starves A Skeleton To Hold Room A First Entry Cannot Use
- 114 · Doctor’s lane module roster omits work-doctor-loop-record.mjs
- 113 · An Explicitly Named Config Path That Does Not Exist Is Silent
- 112 · The worktree prepare installs the ui workspace every dispatch
- 111 · FF-5308’s necessity leg is red, and it is the good news it says it is
- 110 · The Promotion Family Has Two Listitems Reads The Cache Read Surface Gate Classifies Neither Way
- 109 · Tech Debt Item 49 Still Records The Nextwork Fail Open As Open Though Story 86 Closed It
- 108 · The Rendered Bundle Is Stale For Verify And Assimilate Code So This Repo Runs The Pre Story Prompts
- 107 · Acd Cache Read Surface Boundary Is Red On A Clean Tree Its Pinned Reader Promote Gap To Chore Mjs No Longer Declares Defaultat
- 106 · Ff 5905 S Doctor Lane Module List Is Red On A Clean Tree And 66 S Fold The Family Ratchet Has Been Passed Twice Unfolded
- 105 · Outcome Md Is Not In The Streamed Requestable Artifact Set So The Mesh Cache Can Never Answer For It
- 104 · The Coherence Lane Is The Third Reader That Was Missed
- 103 · Doctor Reports Health Over A Stream It Cannot See
- 102 · The declaration names its loop
- 101 · Committed Generated Markdown Under Wiki Work Has No Eol Pin
- 100 · The Generated Marker Names A Command With No Ref
- 99 · aof’s own exemption ledger reddens a governed project’s audit – TECH_DEBT 72’s species, one lane over
- 98 · The Reference Corpus, Re-Fetched And Confirmed At Source
- 97 ·
validaterefuses a story’s honest forward reference - 96 · The declaration earns its keep
- 95 · The brief compiler sacrifices sections with budget unspent on a long story record, and condenses a long ARCHITECTURE to a husk
- 94 · A Malformed Aof Config Reads As An Empty One So Every Optional Declared Step Silently Does Not Run
- 93 · Ff 5809 Leaves Acd Prompt Bounds Name Their Home Unclassified In The Registry Reach Lane
- 92 · Ff 6109 S Harness Switch Lane Reds On The Shipped Declared Document
- 91 · The Cache Read Surface Pin Points At A Defaultat That Promote Gap To Chore Mjs No Longer Declares
- 90 · This Repo Cannot Declare Work Worktree Prepare Without A Prepare Script No Story Owns
- 89 · FF-7106 is declared in milestone 71’s fitness register but owned by no story
- 88 · Three arch tests are red from 71/00 and 71/01, both already done
- 87 · The test-isolation guard stops shipping
- 86 · The span vocabulary, completed
- 85 · Records follow the story
- 84 · A story span is a ref
- 83 · The agent layer’s four bounds
- 81 · The loop’s bounds survive a grader that takes real time
- 80 · Every item that delivers says what it delivered
- 79 · The committed loop graph
- 78 · The loop execution record
- 78/00 · The execution projection
- 78/01 · The item-scoped renderer
- 78/02 · The record command
- 78/03 · The signature and the doctor lane
- 77 · Harness audit – doctor for the machine, not the record
- 72 · The inner loop
- 72/00 · The declared toolchain
- 72/01 · The selection
- 72/02 · The test command’s face
- 72/03 · The cold boot
- 72/04 · The prepared worktree
- 71 · Loop discipline – one review round, and findings become work items
- 71/00 · The build’s terminator, and the free gate that runs before any reviewer is spawned
- 71/01 · A finding the cap stops chasing becomes a named work item, or a named question for a human
- 71/02 · One review pass – lanes spawned together, mode read off the wave, and re-work confined to the delta
- 71/03 · The render lane is gated on renderability – and this milestone supersedes 07’s
npx playwrightclause - 70 · Warm start
- 70/00 · The phase brief
- 70/01 · A launch whose prefix is shareable
- 70/02 · Cache economics per phase
- 70/03 · A story reads its slice
- 70/04 · The fix loop resumes the build
- 70/05 · A brief that carries what the phase must satisfy
- 70/06 · The saving is a number, not a claim
- 69 · Loop bounds
- 69/00 · The declared cap
- 69/01 · Heartbeat by consumption
- 69/02 · The four deadlines
- 69/03 · Progress, not liveness
- 69/04 · Slots before work
- 69/05 · A blocked run releases its slot
- 69/06 · The ledger binds the build loop
- 68 · Loop telemetry
- 68/00 · The spend-bearing run record – a sixteenth key, and a writer that refuses a lie
- 68/01 · Attribution at spawn
- 68/02 · Spend ingest at settle
- 68/03 · One run, one item
- 68/04 · Observe answers for a story and a phase
- 68/05 · Append-only snapshots
- 66 · Controls That Run
- 64 · Green the reds that arrived on main – six arch gates and two racy tests
- 63 · Event-driven triggers
- 63/00 · The trigger declaration
- 63/01 · The level is a ceiling, not an admission
- 63/02 · The launch envelope compiles
- 63/03 · A mesh assignment resolves to a loop call
- 63/04 · The signals that are not the mesh
- 63/05 · The trigger’s face
- 63/06 · The loop launch is watched as a loop
- 62 · The self-improvement loop
- 62/00 · The corpus and its floor
- 62/01 · The proposal, its lane and its patch
- 62/02 · Provenance that resolves
- 62/03 · The distance to a live proposal
- 62/04 · The tuner’s face
- 62/05 · Candidate formation
- 61 · The disciplined acceptor
- 61/00 · The clamp
- 61/01 · The epoch and the frozen criterion
- 61/02 · The observation census
- 61/03 · No executed consumer, no proposal
- 61/04 · The rule and the ledger
- 61/05 · The event a ruling raises
- 61/06 · The acceptor’s face
- 59 · The audit loop
- 59/00 · The auditor kind
- 59/01 · The instrument census
- 59/02 · Evidence re-run
- 59/03 · Staleness, silence and the prune
- 59/04 · The audit face
- 58 · Supervising loops
- 58/00 · The supervision vocabulary
- 58/01 · The reference hierarchy and the arbiter
- 58/02 · Layer separation, arbitration and the gate
- 58/03 · The supervision face
- 57 · Paired loops
- 57/00 · The watcher node
- 57/01 · Independence computed, and the gate
- 57/02 · Examples rows in the parser
- 57/03 · The contract-integrity ratchet
- 57/04 · Escape and intervention counters
- 57/05 · The pairing table
- 55 · Anchors & the frozen set
- 55/00 · The anchor taxonomy
- 55/01 · The groundedness report
- 55/02 · Provenance at write time
- 55/03 · Raw capture before classification
- 55/04 · The frozen set, compiled
- 55/05 · L3 unlocked
- 54 · Verification as a feedback loop
- 54/00 · The grade record – green is positive evidence
- 54/01 · The declared rubric
- 54/02 · Fitness in the gate
- 54/03 · Feedback rides the re-drive
- 54/04 · Scenario traceability
- 53 · The loop as a CLI artifact
- 52 · Loop registry & the loop graph
- 50 · Session launcher – start a session on a node, bound to a repo
- 49 · The terminals home
- 48 · Routable session identity
- 47 · /fleet with a repo filter
- 45 · UI app shell & path routing
- 43 · Mesh artifact authority
- 39 · Delivery Memory
- 38 · Cross-machine worker execution & session presence
190 · A tree is tested once per change, and every sign-off door reads that run’s record instead of running it again
A sign-off is evidenced by a recorded run, never a repeat of one
Every door that asks for tests gets an answer the command recorded against the tree’s content, and a repeat on unchanged content launches nothing: the build terminator, the review lanes, aof:review, aof:verify and a UAT sweep. The stories hold each capability whole: m190/00 (the record), m190/02 (the gate’s record and resume) and m190/03 (the doors’ prose).
A milestone closes on one whole-tree gate run, which may run on a hosted runner
190 itself closed on one aof work regression-gate 190 run: green, sharded, in 15.7 min on a clean ubuntu-latest checkout through gate.yml. Its row was committed home unchanged. A stopped gate resumes rather than restarting (m190/02).
Narrowing is positive and the project declares what it knows
--scope impacted narrows only on what it knows (m190/01). An existing project is told what its work.test declaration leaves out, and /aof:map-tests declares it and proves it by replay (m190/04). This repository declares its roots, suffixes, exclusions and resume templates.
190/00 · A repeat run is answered from its record
Every aof test run leaves one record keyed to its content
aof test takes a content key before it launches (the tree including uncommitted changes, wiki/work included) and files one record under <git common dir>/aof/test-records/ holding the key, scope, selection, result and what widened. packages/work/src/testing/record.mjs is its only writer, and the agent supplies no field of it (FF-19001, FF-19004).
A repeat on the same content is answered from its record
A run whose key and selection a stored record covers is answered with answered from the run of <instant> (tree <id>) and launches nothing. A red record answers only the identical selection, and --fresh forces a launch.
Callers asking together make one run
A caller whose selection an in-flight run covers waits on that run’s lock instead of launching its own. The lock and the record are placed by write-then-link, so a reader never sees half of one.
No door decides from a test record
No status, doctor, loop, audit or bundle door imports the record store (FF-19002). The record answers aof test, and the regression gate’s own row still decides the accept door.
190/01 · impacted narrows on what it knows
Package-owned suites are suites, and only runnable ones are selected
work.test.roots and suiteSuffixes admit the package and app test/ directories, and the selector takes a suite only when it is also a member of the whole suite the runner runs. Every registered suite is selectable (FF-19005).
Dependents are followed to the end
A changed module selects every suite that reaches it transitively through the code graph, not only its direct importers.
A new file narrows only when its place is known
A new suite selects itself (suite-itself), a new module imported by a changed file inherits its importer’s suites (imported-by-changed), and a declared path not yet written contributes nothing (not-yet-written). Any other file the graph does not know widens the run to every suite.
Documents narrow only through a declared coupling
A changed document or data file narrows only through work.test.couplings. This repo declares none, so every changed document still widens.
The narrowing reasons are a closed set
Every selected suite carries one of the five frozen reasons of ADR-003 §5, and a widened result carries none (FF-19003). A narrowed selection of nothing launches nothing.
190/02 · A gate that did not finish resumes
A stopped gate resumes on the same content
aof work regression-gate <NN> --resume re-runs only the units a not-finished run left: its killed units, its unstarted units and the whole of any killed packed unit. It skips finished lanes, and appends one row for the whole, reading resumed from <instant>: n of m units re-run.
A resume refuses what is not a resume
A resume is refused with regression-gate-resume-tree-changed when the content key differs from the stopped run’s, and with regression-gate-nothing-to-resume when the newest row is not not-finished. A gate that declares no reportArgs/filesArgs refuses with regression-gate-resume-undeclared.
The gate tells its program what to do through declared templates
work.test.gate carries reportArgs ({report}), filesArgs ({files}) and noLanesArgs beside jobsArgs and budgetArgs. scripts/test-sharded.mjs plans only the files a --files list names, and reports units, lanes, notFinished and notStarted.
Every gate run is also a test record
The gate writes an ADR-001 record through 190/00’s writer, keyed after its row is appended, with its sharded report attached. A later aof test --scope all on that content is answered from it (measured live: launched: false).
190/03 · The doors stop prescribing repeat runs
Each door asks aof test once and reads an answer from the record as evidence
The aof:continue build terminator and review lanes, aof:review’s test step and aof:verify now ask aof test at their scope, rather than telling each party to run the suite again. Each says that an answer from the record is the command’s own evidence. Reviewers judge the diff and do not re-run.
A UAT session sweeps once
The aof:verify UAT path and the UAT command and templates run one aof test --scope all for the whole session, answered from the newest gate run’s record when the content has not changed.
A stopped gate’s repair is the resume
verify.md names aof work regression-gate <NN> --resume as a not-finished row’s repair, and re-running or speeding up the gate as the repair when the content has changed. Never a code repair.
The prose names no tiers and one deliberate repeat
No command names a unit, integration or end-to-end tier (ADR-004). --fresh is named once, in <build_terminator>, for a suspected flake.
Every runtime render carries it
The Claude, Codex, OpenCode and .agents renders and .aof/templates/ match the bundle (aof work update: 0 drift).
190/04 · An existing project is told what its test declaration leaves out, and one skill declares it
aof project doctor names what a test declaration leaves out
A test-layout family checks a config’s fit to its tree: test-layout-suites, test-layout-couplings and test-layout-resume. Each is ok or a warning naming /aof:map-tests, and none is ever an error. An empty whole suite is never ok.
aof work update prints the same warnings
After its Updated ACD: line, work:update prints one line per test-layout-* warning, from the same function as the doctor, and prints nothing when the family is all ok.
A commit’s selection is replayed without running anything
aof test --scope impacted --commit <rev> --plan selects from the files that commit changed and stops. It launches nothing, writes no record and is answered from none. --commit without --plan is refused.
One answer to “what is a suite”
A suite is a file under a declared root that ends in a declared suffix and that work.test.exclude ([{ paths, why }]) does not name. select.mjs decides it, and the doctor, the selector and aof test’s whole suite all ask it. A declared suffix list reshapes the whole suite. With none declared, the whole suite keeps its pre-190 pair.
/aof:map-tests declares the layout and proves it by replay
The bundle command does the following, writing work.test.* and nothing else; --dry-run writes nothing:
- replays the last 30 non-merge commits;
- authors roots, suffixes, exclusions and couplings, each with its reason;
- authors the resume templates (
reportArgs,filesArgsand, when the gate takes one,noLanesArgs) only for a gate program that honours the contract; - replays again, and runs the doctor.
This repo’s declaration accounts for its native tests
work.test.exclude names the 64 native node:test files under packages/*/test and apps/*/test, with the reason, so test-layout-suites and test-layout-resume are ok here and the whole suite stays at 1,273.
186 · Core names no integration package outside the registry
The registry is the only core importer of any integration package
packages/core/src/integrations.mjs is the only module under packages/core/src that imports an @aof/integration-* package. FF-16901 (acd-integration-registry-one-home) reports any other importer, Notion included, with no named exception.
185 · Notion’s effects, routing and store metadata reach core through the contribution
Integration reactors come from the registry
application/bindings/effects/table.mjs builds every registered integration’s effects-journal reactors from its contribution’s effects member. Every integration’s local reactors run before mesh projection and every integration reactor after publication, so Notion’s sidecar remap and board sync keep their cascade positions.
Integration routing comes from the registry
application/bindings/integrations/routing.mjs merges each contribution’s routing({ readRouting }) members over the one .aof/integrations.json reader, and application.work.integrations.routing keeps its keys (resolveNotionRouting, isPageId, classifyParent, asBoardsRegistry, RoutingError).
Integration store classification comes from the registry
effects/stores.mjs merges each contribution’s storeMetadata tables and file stores into the one store classification, so notion-sidecar stays a fact written only by the Notion mapping module.
The contribution’s optional members are one recorded shape
169/ADR-002’s amendment records the contribution as { id, name, assets?, services?, commands, effects?, routing?, storeMetadata? }, read generically by core. Hyperagent carries none of the optional members added for Notion.
184 · Notion’s verbs reach core through one contribution
Notion registered in the one integration registry
packages/core/src/integrations.mjs registers @aof/integration-notion/contribution before Hyperagent, and aof work integrations notion sync-work / associate reach the command registry through integrationContributions(), in the same registration order as before.
The Notion package composes itself
createNotionServices in packages/integration-notion/src/services.mjs builds the sidecar mapping, the apply layer, the ntn CLI seam and the sync body from collaborators core injects. No core module under application/bindings/notion/ or bindings/commands/notion-* exists.
One core binding builds every integration’s services
application/bindings/integrations/services.mjs builds each registered contribution’s services once per application. It hands the same collaborators to every integration’s verbs, and application.integrations.notion exposes the built mapping, sync, CLI and sync-work services under its existing keys.
183 · Global issue log
Local issue capture
AOF provides issues log, issues list and issues remove from any directory, storing timestamped issue records with origin and optional details in the configured global AOF home.
182 · Renumber work items
Explicit renumbering
A live numbered item can move to a vacant number through aof work renumber, with a read-only preview, unchanged lifecycle status and no sibling shifts.
Reference and memory continuity
Current work-document references and linked stream stores receive the new identity; the configured memory corpus is rebuilt. Dated migration history lets recall accounting join original observations to moved lessons while allowing later reuse of a number.
Observable recovery
Held items and invalid destinations are refused before mutation. Ordinary document-write failures roll back, and a post-move memory failure returns the successful move with a nonzero exit and a recovery command.
181 · Unattended loops survive declines
Procedure completion independent of acceptance
The shared Codex and Claude phase contract treats a recorded negative verdict with routed findings as procedure completion, while retaining genuine procedure failures.
Durable developer handback
Both shipped verify procedures return a declined story from in-review to in-progress through the status command, and the loop selects continue while preserving the review-gate route for genuinely in-review stories.
Rubric command preflight
Doctor reports unusable declared rubric commands and loop launch rejects them before sessions start, using grade’s existing command predicate without changing runner-spawn-failed grading or shell behavior.
180 · One memory node in the config
Every memory setting has one home
Backend, corpora, the code graph’s sharing and each engine’s settings live under the one memory node in the schema, the reader, the update, init and the config editor alike (m180/01/the-one-memory-node-in-the-schema, m180/01/the-one-reader, m180/02/the-update-migrates-the-old-shape, m180/03/the-memory-section), and no surface writes the old spelling (m180/02/init-writes-only-the-new-shape-and-selects-local, m180/03/the-editor-never-writes-a-legacy-key).
A second engine is one block and one enum value
A later engine’s settings are one block under memory.backends, named by a value of the backend enum the schema admits, so adding one creates no root key and needs no second migration.
Egress is a reported classification, not a key
No key under memory configures egress. aof memory status and aof graph build report it, and a new project’s local default sends nothing to a hosted model.
180/01 · The memory node holds backend, corpora, graph and per-engine settings, and one module reads it in either spelling
The one memory node in the schema
$defs/memory in schemas/aof.schema.json declares backend, corpora (items enum equal to KNOWLEDGE_CORPORA, with work, imports and code required), graph (the code corpus’s share and remote, with the never-shared sentence), and backends.graphify.extractionBackend (enum equal to GRAPHIFY_BACKENDS), and no egress property at any depth (FF-18002).
The one reader
readMemoryConfig(config) in packages/knowledge/src/memory-config.mjs is the only module that reads corpora, sources, graphify, backends or graph off the memory node, or graph off the root config (FF-18001). It answers the new spelling or the old, lists each old key with its replacement under legacy, and refuses a setting spelled both ways with memory-config-conflict.
Corpora absent means today’s set
An absent memory.corpora serves work, imports and code, and operator is the only corpus a project can add. A list that omits an always-served corpus is refused with invalid-memory-corpora.
The faces print the new paths
aof memory status prints a corpora: line and one legacy: line per old key, config-inspect reports the memory node under its new paths, and aof project doctor shows one memory-config-legacy warning row whenever an old key is present.
180/02 · aof work update rewrites an old-shape memory config to the new node, and aof work init writes only the new shape
The update migrates the old shape
aof work update rewrites memory.sources, memory.graphify and a root graph into memory.corpora, memory.backends.graphify and memory.graph, reports one memory: line per move, and leaves every other key of the project config unchanged. A setting spelled both ways is left exactly as written, for the operator to choose.
Init writes only the new shape and selects local
aof work init-config writes a memory node in the new shape with memory.backend: "local", and the /aof:init command and its renders say that local is the default a new project gets and that it sends nothing to a hosted model.
180/03 · The config editor has a Memory section that shows the node in the new shape and saves only the new shape
The Memory section
The config editor’s project scope has a fifth section, Memory, that shows the memory node already rewritten to the new shape, lists each old key and its replacement in a notice ending Saving writes the new shape., and keeps its Saved Memory message after the save’s refresh.
The editor never writes a legacy key
A saved Memory node that carries an old spelling is refused with memory-config-legacy-write, and a save that is accepted drops the superseded root graph through replaceMemoryNode.
179 · One knowledge engine behind both memory faces
One engine seam behind both memory faces
aof memory (with its aof work memory alias), aof graph build, aof graph impact and the three code-graph readers reach the work records and the code graph only through the one knowledge-engine port (m179/01/the-knowledge-engine-port), so the code graph has one reader (m179/04/the-code-graph-has-one-reader) and every face answers as it did before the port (m179/02/graph-impact-is-relations-paths, m179/03/aof-memory-dispatches-through-the-port).
A second engine plugs in as one declaring module
A later engine joins aof by exporting corpora and the methods it declares (m179/01/the-capability-declaration), and a corpus it does not serve is reported as corpus-unserved rather than answered by whichever backend is present (m179/01/the-corpus-unserved-miss).
179/01 · The knowledge-engine port answers four verbs and routes each by a backend’s declared corpora
The knowledge-engine port
createKnowledgeEngine({ backends }) in packages/knowledge/src/engine.mjs answers exactly ingest, recall, relations and status, and routes each (corpus, verb) pair to the one backend that declared it, calling a record backend’s reindex as its ingest.
The capability declaration
Every backend composed into the port exports corpora, a map from a corpus in KNOWLEDGE_CORPORA (work, code, operator, imports) to verbs in ENGINE_VERBS, and construction throws a TypeError naming the backend and entry for a value outside the vocabulary, a declared verb with no method, or a pair two backends both claim.
The corpus-unserved miss
A verb asked of a corpus no backend declares answers { served: false, code: "corpus-unserved", corpus, verb, backends } and calls no backend.
179/02 · graphify serves the code corpus behind the port, and aof graph build and impact answer through it unchanged
graphify serves the code corpus
packages/knowledge/src/graphify-code-backend.mjs declares code: ["ingest", "relations", "status"] and is the one module that drives graphify for, and reads the edges of, graphify-out/graph.json.
graph build is ingest(“code”)
aof graph build answers the same BuildResult through the port’s ingest("code"), keeps 174’s share around it, and writes a self-ignoring graphify-out/.gitignore (*) on every build, so the built graph leaves nothing untracked.
graph impact is relations(paths)
aof graph impact answers byte for byte as before through the port’s relations, reading the edges once per call, and an artifact that does not parse refuses with a coded graph-unreadable 424 instead of escaping as a raw SyntaxError.
One degrade path for the code corpus
With the graphify binary absent graph build answers graphify-missing and creates no graphify-out/, and with the artifact absent graph impact answers no-graph, both from the code backend.
179/03 · The record corpora sit behind the port, and aof memory answers byte for byte as it does today
Every record backend declares the record corpora
Every backend in BACKEND_REGISTRY exports exactly { name, corpora, recall, reindex, status } and declares work, operator and imports from the one RECORD_CORPORA, never code or relations, and none declares them too, so memory off answers served-and-empty rather than corpus-unserved.
aof memory dispatches through the port
recall, recurrence, ingest, reindex and status reach the configured backend only through createKnowledgeEngine, and both aof memory and aof work memory print the same bytes as before the port for every argv row of the outline.
179/04 · Test selection, contract derivation and seam liveness ask relations instead of parsing the graph
The three readers ask relations
Impacted-test selection, story-contract derivation and the seam-liveness audit each take a relations function bound in core (bindings/code-relations.mjs) and report the same widenings, limits and refusals as before, with selectSuites and deriveStoryContract now async.
The code graph has one reader
Across packages/*/src, readGraph, normalizeGraph and computeImpact are imported only by the graphify code backend, their own modules, graphify.mjs and the graphify memory backend, and FF-17904 refuses any other importer.
A driver-free relations port
createCodeRelations composes relations over the code backend’s read half alone, so the audit family’s construction reaches no graphify driver and no process-starting tool store.
178 · Memory is its own command
Memory is a top-level concern end to end
Memory’s config (memory, 05/ADR-002), its command (aof memory, m178/01/the-aof-memory-route) and every bundle prompt’s recall step (m178/02/bundle-assets-spell-aof-memory) now name the same top-level concern, and aof work memory stays a byte-identical alias (m178/01/the-work-memory-alias).
A prompt-driven recall leads to the PRD behind a record
A recall made by any bundle prompt (refine, continue, verify, retrospective, shatter, in Claude Code, Codex, opencode or Cowork) returns work records that name the planning document their item was framed from (m178/03/records-carry-a-resolved-origin), while recall still searches only the work stream and wiki/planning is not indexed.
178/01 · aof memory is the command and aof work memory its alias
The aof memory route
aof memory <verb> is a registered command at route ["memory"], answering recall, record-recall, recurrence, ingest, reindex and status through the one core path runMemoryVerb.
The work memory alias
work:memory at route ["work", "memory"] is a second registered door whose input, run, argv, render and json are the canonical command’s own function objects, so both spellings print the same bytes for every verb and projection.
Help and hints spell aof memory
aof --help lists memory as a family of its own, and the usage text, the stale-store hint, import-milestone’s reindex warning and the work-command examples all name aof memory.
178/02 · Every bundle prompt, skill and doc spells aof memory
Bundle assets spell aof memory
No file under packages/*/assets/ spells aof work memory in any whitespace form, and FF-17802 refuses one that does.
Rendered runtimes carry the new spelling
.claude/, .agents/, .opencode/, .codex/ and .aof/loops/ are rendered from the migrated assets, and the work install renders all three runtimes, claude, codex and opencode.
Docs spell aof memory
The root README’s memory block and command reference, the schema’s memory.backend description and the knowledge README name aof memory, and milestone 05’s delivered-milestones row keeps its old spelling as history.
The learning-edge control reads the new spelling
acd-learning-edge-reaches-every-cut parses aof memory <verb> invocations in the prompts.
178/03 · A recalled work record names the planning document its item came from
Records carry a resolved origin
Every MemoryRecord carries origin, a list of project-root-relative, forward-slashed paths that each name an existing file. It is [] when there is none, and a story without its own origin: takes its top-level driver’s.
Origin resolution
A declared origin: value is tried relative to the project root, then to the record doc’s folder, then to that folder as it stood before archiving. A value naming no file is dropped and counted in aof memory status as origin.unresolved (6 on the live stream).
Recall shows origin
--block appends ` · origin
Index version 4
INDEX_VERSION and GRAPHIFY_INDEX_VERSION are 4 in lockstep, and a version-3 store reports stale until aof memory ingest rebuilds it.
175 · aof on macOS
A Mac is a supervised single-node control node
This MacBook goes from clone to a running fleet with the deploy rules’ commands alone (m175/04/the-deploy-rules-carry-a-macos-section). The menu-bar app runs both daemons in the login session and returns at login with no terminal (m175/02/the-desktop-verbs-on-darwin), Quit leaves no aof process (m175/00/quit-reaps-the-whole-tree), and a supervised claude authenticates in the daemons’ environment (m175/03/the-darwin-preflight).
A launch folder is not a workspace anywhere in the mesh layer
A folder with no aof config of its own, such as launchd’s cwd /, is not synced, not read for presence and not watched (hasOwnWorkspaceConfig, F-11, F-14). mesh serve --serve --workspace is honoured, the fleet UI resolves identity through resolveWorkspaceId, and repo availability reads the pinned checkout (TECH_DEBT 4 A–D, item 21).
175/00 · Quit reaps the whole tree on macOS
Quit reaps the whole tree
On macOS every supervised daemon leads its own process group, and Quit, SIGTERM and a quit Apple Event each TERM every group, wait 5 s and KILL what is left, so no aof process outlives the app (FF-17501).
The menu-bar menu
A left-click on the menu-bar icon opens the menu, which stays open: it is replaced only when its items change, and the icon only when its glyph does. Windows still shows the window on a left-click.
The window on a Mac
The window keeps the native traffic lights over its own title bar, and the red light hides it to the menu bar. The node row shows the hostname whole where it fits, with the role badge and this node chip in one centred group (DESIGN M3, M4).
175/01 · install-local builds the app on macOS
The app bundle is built and placed
node scripts/install-local.mjs --desktop on darwin builds the release app, assembles an ad-hoc-signed aof-mesh-desktop.app (executable AOF Desktop, LSUIElement) and places it in ~/.aof/bin beside the payload.
The install leaves the shell alone
No shell profile is edited and ensureOnPath is gone, so the npm-linked aof stays first on PATH, while ~/.aof/bin/aof --version answers payload <buildId>.
175/02 · The desktop verbs install, run and stop the app on macOS
The desktop verbs on darwin
install places the .app. run starts it through launchd in the login session, reloading a loaded-but-stopped job from its plist and refusing over SSH when no LaunchAgent exists. stop sends SIGTERM and waits up to 20 s, never force-killing.
Login autostart
install --autostart writes the LaunchAgent com.aof.mesh.desktop (RunAtLoad, Aqua session, KeepAlive false, the installing shell’s PATH), and Login Items lists it as “AOF Desktop”.
175/03 · The preflight checks the Mac
The darwin preflight
On macOS the preflight adds macos-command-line-tools, rust-toolchain, app-not-quarantined and daemon-claude-authenticated, and a check with nothing to probe reports --.
The daemons’ claude is probed in their environment
daemon-claude-authenticated runs claude auth status with the LaunchAgent’s PATH and launchd’s HOME, USER and LOGNAME alone, so it answers as a supervised run would (FF-12607).
175/04 · The Mac runbook and acceptance
The deploy rules carry a macOS section
.claude/rules/build-deploy-restart.md takes a Mac from clone to a supervised fleet: the build prerequisites, install-local --desktop, the control-node nomination with fabric direct on 127.0.0.1, aof mesh identity, autostart, deploy, restart, verification at the source, and the gotchas with the crash recovery.
The desktop README and ROADMAP
apps/desktop/README.md names the macOS prerequisites and the .app, and ROADMAP lists macOS crash containment as deferred.
174 · A built graph is shared through the project’s own remote
A commit’s codebase graph on the project’s own remote
A project’s remote can hold one graph bundle per commit, as a blob behind refs/aof/graph/v1/<full commit sha>. A plain git clone never downloads it.
- The remote accepts a ref that points at a blob outside
refs/heads/– measured on GitHub at spike 173 and again at verify (1.45 MB bundle,d78cca7).
aof graph build takes the published graph on a miss
With graph.share: "git-refs", a whole-root code build with no current local graph first fetches the graph published for HEAD from graph.remote (default origin). It keeps that graph, with no graphify run, only when its inputs match the tree. Otherwise it rebuilds as before, and an unusable remote is a warning, never a failed build.
- The clone’s config at HEAD matches the published commit’s –
.aof/aof.config.jsonis a graphify input, so a local, uncommitted edit (including the one that turns sharing on) makes every fetched graph a miss.
Clean default-branch builds are published once, and the remote keeps the newest 20
After a build, the graph is published only when it was built at HEAD from a clean tree and HEAD is on the remote default branch’s tip, which must be present locally. Any other build names its reason and pushes nothing. The first publish of a commit wins, nothing is force-pushed, and every successful publish prunes refs to the newest 20 first-parent default-branch commits.
- The node has push access – a node without it fetches only, and its publish is a warning.
aof graph fetch, aof graph publish and aof graph prune
Each half runs by hand, whether or not sharing is on. fetch lays out the published graph through graph:import’s own staged swap and refusals. publish and prune apply the build’s rules and report what they did or why not. Unlike the build, each verb exits non-zero on a remote it cannot reach.
Only the codebase graph rides the transport
graph-share.mjs is the one module that names refs/aof/graph/. No memory module reaches it, and it reaches no memory or mesh module, so the memory work graph and index are never fetched or published (FF-17401). No push to the namespace forces, and every remote call runs with GIT_TERMINAL_PROMPT=0 (FF-17402).
172 · Excalidraw phase 3 – the architect conversation streams into the editor, through a designed UI
The architect conversation is an editor surface with no terminal
A question or reply at a shape reaches a headless session on the board’s server (m172/00), its answer is written into that shape’s thread in the one conversation panel as it is produced (m172/01, m172/02), and the diagram surface renders no terminal and holds no code to type into one (m172/03); the operator approved it live on 2026-10-28, which closes F-171-04 (VERIFICATION ## User sign-off).
The conversation surfaces have a committed design baseline
mocks/conversation*.png (accepted 2026-10-27) are the conformance source of truth for the panel, the canvas ask, a proposal and the session state, with DESIGN.md stating the accepted fallback wherever the shipped surface cannot yet say what the mock says (F-172-09, F-172-10).
172/00 · The architect answers in a headless session
A headless conversation session in @aof/execution
conversation-session.mjs runs one stream-json claude per key, one turn at a time: a delivery during a turn is queued in order, a session with no subscriber and no turn in flight ends after ten minutes (stdin closed, then the tree), and one pure reader turns its stdout into session, turn, text, activity and turn-end events, each naming the request it answers.
The launch never bypasses permissions
The session launches with --permission-prompts none and an explicit --allowedTools list, with an argument vector and no shell, at the refine phase’s model and effort, and with the session-attachment keys scrubbed by the same predicate the loop’s driver uses (FF-17201).
The board delivers to the architect’s session and streams its turn
POST /api/diagram/conversation (admitted like every board write) starts, sends to, or queues on the diagram’s session; /api/diagram/events forwards each conversation event as an SSE conversation event, a snapshot first; with no claude to launch the route answers 503 architect-session-unavailable.
One home for the architect’s vocabulary
packages/diagrams/src/review-session.mjs is the only place the /aof:diagram-review invocation and the session’s tools (Read, Grep, Glob, Write, Bash(aof diagram scene:*)) are spelled; diagram:scene --action session and the editor’s copy-the-command fallback both read it (FF-17202).
172/01 · Every conversation in one panel
One panel holds every conversation on a diagram
ConversationPanel.tsx beside the canvas is the one place a thread, its replies and its proposal are read; the review list, the note bar and the thread popover are gone, and a note pin or the canvas ask opens its thread in the panel.
One pure model of threads
conversation-model.mjs (which absorbed note-threads.mjs) derives each thread, its subject, its state and the proposal it holds from the review records alone, with one clockOf time formatter.
172/02 · The answer streams into its thread
The answer is written into its thread as it is produced
conversation-stream.mjs reduces the stream’s conversation events into the session’s state and each request’s live turn, and the panel draws that turn inside its thread until the architect’s recorded turn for the same request replaces it; the thread follows the text while it streams, until the operator scrolls up.
Asking and replying reach the server’s session
Ask, a reply, Review with the architect, Ask again and a first draft POST to /api/diagram/conversation; Save as note posts nothing; no editor action opens or types into a terminal, and an unavailable session shows the command to copy.
Publish says it published
The publish answer carries whether the figure’s own ADR shows it, and the editor’s notice reads “Diagram published.”, adding only that the architect links the figure when the ADR does not show it yet; no link block is handed to the operator (F-172-16).
172/03 · The diagram surface has no terminal
The terminal control carries no diagram seam
apps/ui/src/diagram/dock-mount.mjs is gone, and the terminal control no longer takes laterInputs, onLaterInput or onSessionReady; the mount command’s own readiness typing (typeCommand, the outbox) stays for the board’s dock.
The surface table lists only surfaces that render a terminal
The fleet’s terminal-input census no longer carries a diagram dock row, and every control that counted the removed lanes and lines reads the smaller tree.
172/04 · A right-click leaves the drawing true
A right-click is checked to leave the drawing as it was
The diagram editor’s browser smoke drives six right-click gestures over 171’s ADR-003-push-stream scene at device pixel ratio 2, asserts each opened its own element’s menu, and compares the drawing and the saved scene after each; F-171-03 did not reproduce on the overhauled editor and is ruled out.
171 · Excalidraw phase 2 – the editor follows the architect live and is easier to drive
A conversation about a shape without leaving the editor
The architect is asked about a shape at the shape (m171/00), its answer arrives in the open editor as it is written (m171/02), the note it answers is a thread pinned beside that shape (m171/05), and a reply typed there reaches the same live session and is answered in the same thread – run live by the operator on 2026-10-25 (171 VERIFICATION, User sign-off).
The editor reads the architect’s records only through the scene command
Every editor surface this milestone added – the stream, the pins, the threads, the palette – reads diagram state through diagram:scene and the review records it owns; the board face names no diagram path and imports nothing of @aof/diagrams (FF-17101).
171/00 · Ask the architect from the canvas
Ask architect at the selection
A selected shape, or several, shows an “Ask architect” action on the canvas above the box enclosing them, and a question asked there is recorded as a request naming the selection’s ids and delivered exactly as the bar’s ask is (askArchitect in editor-state.mjs).
One placement home for on-canvas affordances
Every on-canvas affordance is a component of the one overlay layer (CanvasOverlay.tsx), placed only through canvas-anchor.mjs, the one module that converts scene coordinates to the screen (FF-17102).
171/01 · Find a library item by name
Find in libraries
The editor’s sidebar has a Find tab, beside the SDK’s own, that filters every item of the global and project library roots by item or library name (a library in both roots is searched once, from the project) and inserts a clicked or dragged result through the SDK’s own library drop, saved as an aof:lib node.
171/02 · Review records are pushed
The diagram events stream
GET /api/diagram/events is one “look now” stream per open diagram: it says only that the review records or the scene moved, with the stamp that moved, and carries no record; the board face learns what to watch from diagram:scene --action stamp, re-stamps on a change, every 30 s as a safety, and every 2 s when no watch starts.
The editor follows the stream
An answer or a proposal written by the architect appears in the open editor’s panel without a reload (~210 ms measured); a draft saved elsewhere is taken in place by a clean canvas and offered as Reload to a dirty one; the editor’s own writes never read as changed elsewhere, and the five-second poll runs only while the stream is down.
171/03 · Drafts in the house style
The palette from the style guide
house-style.mjs reads the semantic-roles table of the guide work.diagrams.style names (its light column, hex or rgba) into a palette, and there is none without paper and ink; diagram:scene read and context carry it.
Proposals and hand-drawn shapes start in the house style
propose fills fill:paper, stroke:ink, color:ink into every added node that has no style and is not a library item, before the diff, so the record and the preview say it; the editor seeds the SDK’s current-item defaults from the palette, so a shape drawn by hand starts in it and a stored shape keeps its own colours.
171/04 · A note reaches the open session
Delivery to the open session
A note or a question added while the architect’s session runs in the dock is typed into that session and run after its current answer, as its own command; with no session running, Ask architect starts a keyed new one that takes the question and then the waiting notes, and a note otherwise waits for a review session – the surface’s one deliver says which happened.
What a live terminal session is typed
The dock types its command only once the TUI’s own readiness holds (bracketed paste on, then something drawn), or at a 30 s cap, and every input – the command and each later input – goes through one serial outbox as one bracketed paste followed, after a 900 ms settle, by its Enter as a separate write.
One home for the TUI input contract
@aof/contracts/tui-ready is the single home of the readiness gate and the paste framing, read by the loop’s session driver and screen and by the browser’s dock (FF-13801 re-homed there).
171/05 · A note is a thread beside its shape
A note is a thread
A request record carries a thread of turns: reply appends the operator’s turn and reopens the note (a proposed one too, its proposal untouched), respond appends the architect’s and still sets answer, and an older record reads as the thread its request and answer imply; the architect’s context carries the whole thread, oldest first.
Threads beside their shapes
A note on a shape is a pin at that shape’s corner – one pin per shape, counting its notes, coloured by whose turn is last – and choosing it opens the thread beside the shape, where a reply is recorded and delivered to the running session through story 04’s one deliver.
170 · Excalidraw phase 1
An ADR’s diagram is a refinement loop between the operator and the architect
On a project with work.diagrams on, an ADR’s figure opens in an editor where the operator draws, uses project libraries and leaves notes, and the architect answers or proposes edits that apply only on the operator’s accept. Its parts are stated whole in the stories: the store and loop (m170/00), the package pair (m170/01), the opt-out (m170/02), the route (m170/03), the first draft (m170/04), notes (m170/05), the batch review (m170/06), one format (m170/08) and libraries (m170/09).
The loop runs on the Windows control node
The editor, store and review records work on the platform the operator uses. They were verified there on payload f43d5121, with a real model, a real AWS library and a published figure (ADR-010’s own diagram).
170/00 · The native draft store and the review loop
Scene store with optimistic revisions
diagram:scene saves an ADR’s draft as a canonical .excalidraw file under the diagram’s exclusive .lock, and a save against a stale revision is refused as a conflict.
Review loop
Requests, answers, proposals, accept and reject are aof diagram scene actions. Each is kept as a review record beside the diagram, and on every platform a record is created by rename() without replacing an existing id.
170/01 · Diagramming is a package pair
@aof/diagrams and @aof/diagrams-ui
Diagramming lives in two workspace packages, the node half (layout, generator registry, diagram:* verbs, doctor lane) and the browser half (editor, pure state, SDK, fonts), each reached through one contribution or one dynamic import.
One-way dependencies
@aof/work imports nothing of either package, which FF-17001 (acd-diagrams-package-one-way) enforces.
170/02 · The opt-out seam
One capability read
aof diagram capability and GET /api/diagram/capability answer { enabled, editor, generator } through the one reader of work.diagrams, and the editor follows the generator switch.
The opt-out holds at every door
With diagrams off, the board shows no Edit draft and fetches none of the editor. Every write action of diagram:scene refuses with diagram-disabled. A directly opened /diagram imports no editor and says the project’s diagrams are off, or, on a server that does not serve them (the fleet), points to the project’s board.
170/03 · The editor is a route
/diagram?ref=&file=
The editor is a routed surface, listed under no nav item. It is addressed by query, so it survives a reload and can be linked from the board’s Edit draft.
The architect’s dock beside the canvas
The surface binds its own dock (diagram/dock-mount.mjs, a fourth posture home beside fleet, board and home), so the architect’s session sits next to the drawing.
170/04 · A first draft from Mermaid
A first draft on request
An empty diagram offers “Request a first draft from the ADR”. It records a request and opens the architect’s dock session, and the architect proposes the whole diagram in the Mermaid profile.
Drawn by Excalidraw’s converter, in the drawing font
Accepting a first draft draws it with @excalidraw/mermaid-to-excalidraw, after loading the hand-drawn font for the proposal’s text, so labels are measured and wrapped in the font that paints them.
170/05 · Notes without a session
Notes that start nothing
Add note records a review request on the selection, or on the whole diagram, without launching a session. The review panel lists every open note, however old, and the operator can withdraw one.
170/06 · One command examines every note
Batch review
/aof:diagram-review <ref> <file> with no request id examines every open note in one session, recording an answer or a proposal per note. The single-id form still answers one.
Records leave the batch when acted on
A respond marks a note answered and a propose marks it proposed. A withdrawn or answered note refuses both, so a second batch never answers it again.
170/07 · The served-path smoke
A real-browser check of the served editor
test/smoke/diagram-editor.smoke.mjs serves two throwaway projects (diagrams on, diagrams off) with the real aof work ui. In installed Chrome it drives the board link, first draft, library insertion, freehand, notes, batch review, accept and publish, with the real CLI standing in for the architect.
170/08 · One format
The scene is the store, Mermaid the interface
The architect reads an ADR’s diagram only as the aof Mermaid profile projected from the stored scene (scene-view), and a scene whose view the profile could not read back is refused at save, naming the node.
Element-level accept
A proposal compiles in the editor into element operations. Accept refuses as stale, by Mermaid id, anything it touches that a person changed since, and refuses any update or addition outside what the proposal touches.
Publish from the canvas
Publish writes the editor’s SVG, and the PNG when work.diagrams owes one, where the ADR’s block links them, and answers the block to paste.
170/09 · Project libraries
Libraries used as they are, from two roots
Every .excalidrawlib in the global home’s diagrams/libraries/ (shared sets) and in the project’s .aof/diagrams/libraries/ (its own) loads into the editor’s Library panel. A project file shadows a global one of the same name, and each root has its own 8 MB cap. A real set (AWS Architecture Icons, 249 items) loads.
Insertions keep their provenance
An item a person drags in reads as %% aof:lib <id> <library>/"<item>". The architect places an item by name, and an edge proposed to a hand-placed item binds to an anchor that compile adds in the item’s own group.
169 · Hyperagent plans the work
Planning can be handed to Hyperagent end to end
A project turns on work.integrations.hyperagent, uploads the planner package once to the Hyperagent account, and from then on /aof:hyperplan takes a request to a draft pull request whose branch aof work validate passes. Measured on 2026-10-23 with draft PR #18. The parts: the verbs and skills (m169/00), the integration seam (m169/01), the aof-home package (m169/02) and its single zip upload (m169/03).
- The Hyperagent agent named by
work.integrations.hyperagent.agentexists, has the sevenaof-*skills and has GitHub write access – the doctor names the first two, and a read-only integration ends a runblocked.
Hyperagent is an integration package, and core names none outside one registry
Everything Hyperagent ships (verbs, /aof:hyperplan, planner files) lives in packages/integration-hyperagent and reaches core through one contribution read by packages/core/src/integrations.mjs. The model knows exactly claude, codex and opencode (FF-16901, FF-16902). A new integration is one package plus one registry line.
169/00 · Hyperagent plans the work
/aof:hyperplan delegates a planning step to Hyperagent
From Claude Code, OpenCode (/aof:hyperplan) or Codex ($aof-hyperplan), a planning request is composed into a deterministic brief, sent to the configured Hyperagent agent over its MCP endpoint, followed to a result block, and its plan branch checked. Questions are relayed and never answered for the operator.
- The project enables
work.integrations.hyperagentand has Hyperagent’s MCP server registered (the doctor names both).
The four deterministic verbs
aof work integrations hyperagent brief | result | check | doctor compose the brief (refusing an unknown mode or a missing request), read the AOF-PLAN-RESULT block back, validate a plan branch in a temporary checkout using the repository’s rename history (your index, HEAD and worktree list are left untouched), and report the setup. A long request’s plan branch is cut at a whole word.
Planning skills written for an agent with no checkout
aof-prd and full rewrites of shatter, refine, add-milestone, add-story, add-task and validate each carry the Hyperagent execution contract: reach the repository through GitHub, write only an aof/plan/<slug> branch, end in a draft pull request, never simulate aof.
A live plan run delivers a validating draft pull request
Measured on 2026-10-23: one plan request delivered draft PR #18 (a PRD, its research, three backlog milestones and two spikes, all inside the planning lane), and check reports aof work validate: PASS over its branch.
- The agent’s GitHub integration has write access to the repository. Read-only access ends the run with
status: blocked.
169/01 · Integration packages are self-contained
Integration contribution seam
An integration package exports one contribution (id, name, assets, commands) and reaches core only through packages/core/src/integrations.mjs, the one core module that may import an integration package (FF-16901; Notion is the one named exception).
Integration-owned bundle members
readDescriptor() appends each contribution’s assets/bundle.json members to core’s and reads their text from the owning package, both in dev and payload installs (the package’s assets/) and in a packaged binary (bundle/packages/<name>/ in the sidecar). A member id that core already declares is refused at load.
- The packaged-binary sidecar is filled by
install-local.mjs/build-sea.mjs– the reader is tested against a hand-laid sidecar; the writers are not yet exercised by a test.
The integration’s render follows work.integrations.<id>
An integration’s resources and assets render only when the project config has work.integrations.<id> (an empty object is enough) and are removed as aof-managed files on the next aof work update once it is gone; core’s members are never gated.
Hyperagent is an integration, not a runtime
The model knows exactly claude, codex and opencode; --runtime hyperagent is an unsupported runtime, and packages/core/assets holds no Hyperagent command, skill, planner instructions or variant (FF-16902).
The plan-branch check runs on Windows
aof work integrations hyperagent check extracts a plan branch into a temporary checkout with a forward-slash --prefix, validates it there and removes it.
169/02 · The Hyperagent package lives in the aof home
The Hyperagent planner package
aof work integrations hyperagent package [--dry-run] writes AGENT.md and the seven skills/aof-*/SKILL.md to <aof home>/hyperagent/ (~/.aof, or AOF_GLOBAL_HOME). The files are byte-identical to the package’s own. It owns that directory whole, writes nothing in the project, records no lock entry, needs no work.integrations block, and prints the upload steps without claiming an upload.
- The planner files are read through the package’s asset class – the reader core injects resolves them in dev and payload installs (the package’s
assets/) and in a packaged binary (the sidecar’sbundle/packages/integration-hyperagent/).
A project renders /aof:hyperplan alone
With work.integrations.hyperagent present, aof work init|update renders /aof:hyperplan (Claude and OpenCode command, Codex skill) and nothing under .hyperagent/. A lock that recorded the earlier .hyperagent/ render loses those files on the next update.
The doctor reads the package where the verb writes it
aof work integrations hyperagent doctor reports the planner package as missing, stale (it differs from the installed aof’s files) or current. When it is not current, a hint names the package verb. “ready” requires current.
The fragment declares what core never renders
Hyperagent’s assets/bundle.json carries packageFiles, the eight files its own verb ships, beside its one member hyperplan. The bundle-membership control counts them as declared.
169/03 · The Hyperagent package carries one zip
One upload for the seven skills
aof work integrations hyperagent package also writes <aof home>/hyperagent/aof-hyperagent-skills.zip. It holds <id>/SKILL.md for each of the seven planning skills and nothing else, uses the same bytes on every run, and the hand-over names it first, with each loose SKILL.md as the fallback.
- Hyperagent drafts one skill per
<id>/folder of an uploaded zip – measured once, with a same-layout zip, on 2026-10-23.
The doctor reads the zip
aof work integrations hyperagent doctor reports the package missing when the zip is absent and stale when its bytes differ from the zip the installed aof would write.
168 · A prototype builds something fast
A prototype is the lightest work item in the stream
prototype is a sixth top-level item type alongside the governed five. It is numbered in the same stream as milestones, spikes and chores, but it carries no stories, tasks, .feature, criteria, ARCHITECTURE or VERIFICATION. Nothing schedules it or waits on it, and its whole record is PROTOTYPE.md and STATE.md (m168/00).
One verb runs a prototype end to end, in the operator’s session
aof work prototype and /aof:prototype start, continue (taking a new ask), stop and complete a prototype on the current branch. No branch, worktree, subagent, review lane, commit, run record or accept door is involved (m168/01, FF-16804).
Every stop is a checkpoint on record
The wants keep their history (additions dated, drops struck through, never deleted). Each stop, done or abandon copies the hand-off into ## Log, so a prototype’s full state at each checkpoint can be recovered without a plan written in advance (168/ADR-004 §4).
A prototype is visible wherever governed work is
aof work find, status, validate, doctor, aof:explain, aof:add-diagram and the board all resolve a prototype. The board shows it in the existing lanes and counts it in no milestone tally (m168/00, m168/02, m168/03).
168/00 · A prototype is a work item
A prototype resolves like any work item
A NN_prototype_<slug> folder in wiki/work (or the backlog) is a top-level work item that aof work find, status, list and doctor resolve. It is neither a driver nor a depend target, so aof work next never offers one and no depends: may name one (FF-16802).
A prototype has its own four status words
A prototype’s status is active, paused, done or abandoned. Its edges are active ⇄ paused, either one → done | abandoned, and done and abandoned reopen to active. The governed five words and their order are unchanged, and aof work status refuses a governed word on a prototype as status-edge-not-applicable, naming the legal moves, and refuses a prototype word on a governed item.
Validate holds a prototype to its two files
aof work validate checks a prototype’s frontmatter, the ## Intent, ## Wants and ## Shortcuts sections of PROTOTYPE.md (Intent and Wants filled past the template), the ## Hand-off and ## Log sections of STATE.md, and that it carries no depends: or parent:. The doctor’s lifecycle lanes skip it, so a well-formed prototype is clean at every status word (FF-16803).
One type-to-record-doc map
RECORD_DOC_BY_TYPE in packages/work/src/identity.mjs is the only map from an item type to its record doc (prototype → PROTOTYPE.md among them), and records, doctor coherence, the doctor snapshot and the ratchet all read it (FF-16801).
The prototype templates ship in the bundle
.aof/templates/work/prototype/ holds PROTOTYPE.md and STATE.md, declared in bundle.json as template prototype and rendered by aof work update.
168/01 · One verb builds a prototype
aof work prototype carries the four moves
aof work prototype "<ask>" mints the next stream number through runInsertTopLevel’s append form and writes PROTOTYPE.md (Intent and first Want from the ask) and STATE.md at active. <ref> ["<new ask>"] appends a dated Want and logs it, and reopens a paused, done or abandoned prototype. --stop, --done and --abandon copy the hand-off into ## Log under a dated line and set paused, done or abandoned. --log appends one line.
The verb refuses what it cannot write faithfully
Two moves in one call, or a new ask beside a move flag, are refused as prototype-one-move. A stop or complete over the template’s unfilled hand-off is refused as prototype-handoff-missing, and a ref that is not a prototype as prototype-not-a-prototype. The verb never commits, mints no run record and imports no git, dispatch, worktree or lane module (FF-16804).
/aof:prototype builds in the operator’s own session
The bundle command (and its codex workflow.md section) calls the verb for every move. It spawns no agent, makes no branch, worktree, review lane or commit, keeps ## Shortcuts current, and when the checkout is on main says so once and then builds. It ships to every runtime add-chore ships to, through aof work update.
Explain names a prototype by its intent
aof:explain describes a prototype from its ## Intent.
168/02 · A prototype draws its diagrams
Diagram plan and export take a prototype
On a prototype, aof diagram plan and aof diagram export take a diagram name in place of an ADR id. The brief is the prose under ### <name> in PROTOTYPE.md ## Diagrams, and the files land in the prototype’s diagrams/ named by that name. An unknown name is refused as diagram-name-unknown, --slug as diagram-slug-not-applicable, and a heading with no prose as diagram-brief-missing. The prototype paths live in packages/work/src/diagrams/layout.mjs beside the ADR paths (FF-13302).
A prototype diagram can be redrawn
Exporting a name that is already drawn overwrites its files, with no dated copy; diagrams/ holds only the current picture.
/aof:add-diagram takes a prototype ref
aof:add-diagram <prototype> "<what to draw>" writes a new ### <name> brief, draws it and pastes its link under the brief. aof:add-diagram <prototype> draws every brief with no link, and aof:add-diagram <prototype> <name> redraws that one. The ADR path is unchanged.
168/03 · The board shows a prototype
A prototype card sits in an existing lane
The board places a prototype in an existing status lane: active in in-progress, paused in not-started, and done and abandoned in done. The mapping is laneStatus in apps/ui/src/board/model.ts, which the card’s status ring also reads. The card is tagged prototype · <status> and shows the prototype’s own status word.
Prototypes are counted nowhere
No milestone tally, story count, active-milestone count or blocked-gate count includes a prototype, and a milestone focus shows none.
Opening a prototype shows its two documents
A prototype card opens on PROTOTYPE.md with a STATE.md tab and no other tabs. PROTOTYPE is in the one artifact set (packages/work/src/artifacts.mjs), so work:doc serves it and a worker streams it.
167 · The daemon stays within a resource budget
The serving daemon holds its idle budget on a tree as large as aof’s
On the Windows control node and in Linux, mesh serve idles under the 2% budget over a 1,800-doc tree with the motivating faults planted, and both daemons hold the 10% viewer-open budget with a viewer polling. This is proven by the soak (m167/04), which is one unit of the whole-tree regression gate. What makes it hold at the milestone level is the four stories together: a tree that has not changed is not walked (m167/07), not parsed (m167/01), not re-walked for runs (m167/06), and a permanent fault is not retried (m167/03).
A daemon’s cost is visible, attributed and watched from outside
What a daemon spends its time on is named by cause and workspace in aof mesh status --json and aof work doctor (m167/00), and an idle view asks almost nothing of it (m167/02). The desktop shows a sustained overrun and never acts on it (m167/05). The motivating 17-hour burn would now show in all three.
167/00 · The daemon says what it costs
Each daemon meters its own cost by cause
packages/foundation/src/cost-meter.mjs is the one reader of process.cpuUsage and eventLoopUtilization in packages/ (FF-16701). It counts work against a closed cause set, labelled by workspace, route and X-AOF-Client client, and mesh serve and mesh ui each write a snapshot to meshDir/cost/ every 15 s and once more at a clean stop.
- A cause outside the closed set is refused, not counted – a strict meter throws; the production meter drops it and lists it on the snapshot’s
refused, with no degrade line, because the meter cannot import the reporter that imports it. - The board’s polls are counted under
ui–mesh uihosts the board server, so a board-poll storm shows in theuisnapshot, notserve’s.
Every degrade line names its workspace
A degrade raised while reading a workspace carries its workspace label, and every report is counted through the meter even when the line itself is throttled.
The daemon budget has one home
The idle budget (2% of a core) and the viewer-open budget (10%) are declared only in cost-meter.mjs (FF-16702); a fleet or board request makes a daemon viewer-open for 30 s.
aof mesh status and aof work doctor say what each daemon costs
mesh status --json carries a cost block per daemon (share, state, budget, top causes), its text report prints one cost line per daemon, and a snapshot older than 45 s reads not-reporting. The daemon-cost doctor lane warns on a daemon over its budget, naming its top cause, or on one that stopped reporting. Its warns never fail validate.
167/01 · An unchanged work tree costs a stat walk
A read of an unchanged tree is answered from a memo
packages/work/src/discovery.mjs remembers one tree read per resolved work dir, keyed by the stat digest and the tree’s directory shape. A second read of an unchanged tree opens no record doc, and an edited status or a new item (even one with no record doc yet) is answered on the next read. Both hot readers go through it: buildStreamView and the projection store’s readStreamSnapshot port. A memo answer counts as cache-hit labelled memo, and a parse as full-disk-read.
- A tree that cannot be fingerprinted bypasses the memo – an unlistable work dir, or a doc that vanishes mid-walk, is read from disk and never remembered.
Concurrent reads of one tree share one parse
Two callers at the same moment, such as the presence tick and a board poll, get one parse. A shared read that fails is never remembered, and reads of two different trees do not wait on each other.
The stat digest has one home
workTreeStatDigest lives in discovery.mjs. The sync pass and the read memo compute the same digest for a tree, and the sync pass’s stat gate is byte-identical to 164/ADR-002’s.
167/02 · An idle view costs almost nothing
The polled routes answer 304 when nothing changed
/api/work/list and /api/mesh/status send an ETag and answer 304 to a matching If-None-Match; an error answer carries no tag. The tag is two FNV-1a lanes plus the length, computed in packages/server/src/static-serve.mjs, and the fleet’s tag ignores diagnostics.generatedAt.
Every UI poll is scheduled through one cadence
apps/ui/src/lib/poll-cadence.mjs schedules every interval poll in apps/ui/src (FF-16703). It makes no request while the window is hidden, polls at once when the window is shown, doubles while answers are unchanged up to 15 s, and returns to the 5 s base on a change, a user action, or a pending assign. The clock ticks and the session launcher’s outcome wait are exempt. While a Resync watches, every board poll stays at the base.
167/03 · A permanent fault is said once and backed off
A permanent sync failure is said once and backed off
The launcher holds a closed permanent-code set (event-id-conflict, workdir-missing, not-a-directory, no-descriptor). A workspace failing on one gets one warning, then counted sync-pass/backoff skips, with the gap doubling from one tick to a 1-hour cap. The backoff ends when the workspace’s stat digest moves or a pass succeeds, and a restart says the failure once more. Any other code is retried on the next tick and warned about as before.
One registry resolve per tick
The sync pass and the presence aggregation share one resolveTickWorkspaces result in capturePropagation, so a gone folder is reported once a tick at most.
A gone registration is removed at once
A registration whose folder is missing (workdir-missing) or is not a directory is removed from this node’s rows on the first tick, with one line naming the workspace and its path, by global-node-registry.mjs. A no-descriptor registration stays registered and backed off. Another node’s rows are never touched, a registry that cannot be read removes nothing, and the next aof command in the folder registers it again.
167/04 · The budget is proven by a soak test
The real daemons are soaked against the budget
test/mesh/budget/daemon-budget-soak.test.mjs runs the real mesh serve --serve and mesh ui over a generated 1,800-doc project, under their own temp global home and on free ports, with the motivating faults planted: an empty cache, an event-id-conflict and five gone registrations. After a 15 s warm-up it measures a 30 s idle phase and a 30 s phase with a viewer polling. Each daemon’s CPU-seconds per phase, read off its own snapshots, is judged with the meter’s judge against DAEMON_BUDGET. A failure names the daemon, the phase, its share and its top cause.
mesh serveis held to the idle budget in both phases – it serves no fleet or board route (ADR-002), so onlymesh uireads viewer-open.
The planted faults are named, and each is said once
aof mesh status --json names each planted fault with its workspace in the daemons’ cost faults, and the serve log holds one line per sync fault, not one per tick.
The soak is one unit of the regression gate
It is registered through scripts/test.mjs, so the whole-tree gate runs it. It binds neither :4181 nor :4182, and leaves the global home it was handed and the process table as it found them.
167/05 · The desktop shows a sustained overrun
The desktop judges a sustained overrun from the OS’s own sample
The supervisor reads each child’s cumulative CPU time by its process handle on every 3 s poll tick (cpu_sample.rs). The core crate’s guard.rs judges that share against the budget in the cost block of mesh status --json, over the budget’s own 5-minute window: 60% from t0 enters overrun at exactly 5:00, a 40 s spike never does, and the overrun ends once the window has slid past the load. With no budget nothing is judged, a not-reporting daemon is judged against the viewer-open budget, and a daemon the OS cannot sample is never judged.
An overrun is shown and logged, never acted on
An overrun turns the tray degraded and fills the footer’s notice slot. It writes one JSON line on entering and one on leaving to the shared degrade log, with proc: "desktop". A standing exit notice outranks the overrun, and the overrun is dropped while the window renders error. The guard has no path to stop, restart or kill a child (FF-16704).
167/06 · Local runs reach the store through the ledger
Local runs are published through the ledger
run.started and run.completed each carry a publish-run reactor (locus local, retried until done) beside publish-projection. It upserts the run’s current record into work_item_runs with this node as reporter, only for a workspace the propagation predicate allows; a run recorded before this change reaches the store through the reconciler’s backfill.
- A store outage owes the row, it never refuses the transition – a transition whose publish finds the store unopenable leaves
publish-runowed in the journal, and the row lands when the step drains. Theaof work run-startverb itself still refuses in a mesh-enabled workspace when the store cannot be opened, because the item lock fails closed (m43/ADR-003 R1.4).
The presence tick reads what is running from the store
The tick takes this node’s running runs and their loops from work_item_runs in one query over its shared store handle (readStoreActivity), and reads no item’s runs/ folder. A loop’s lastActivityAt is the one per-running-run disk read (FF-16705).
- A workspace that does not propagate has no rows – its runs are not in this node’s presence, and a tick whose store cannot be opened reports no own runs.
167/07 · An idle tick walks nothing
The serving daemon watches each work tree it syncs
mesh serve holds one recursive watch per work dir that its sync pass covers, a backed-off one included. The tick arms before it walks, so a newly watched tree is walked once, by that tick. A lane or a checkout off the mesh is not watched. A registration that leaves the tick set loses its watch on that tick, a tick whose registry read failed changes no watch, and the daemon’s stop closes them all. Nothing else in aof arms a watch (FF-16706).
An unchanged watched tree costs no stat walk
statWorkTree (packages/work/src/discovery.mjs) answers a watched tree from its last complete walk until a watch event dirties it or that walk is 5 minutes old. So the stat gate, the backoff check and the presence read walk nothing on an idle tick. An event naming a record doc, a folder or nothing dirties the tree. One naming any other extension (a run record, a log) does not. An event during a walk makes the next ask walk again, and asks during a walk share it.
A tree that cannot be watched is walked as before, said once
A watch that cannot start, or errors later, is closed. Its tree is walked on every ask, and one work-tree-watch-unavailable degrade names the dir. It is not armed again in that process.
Every walk is a counted cause
stat-walk is in the cost meter’s closed cause set. In a process holding watches, every real walk is counted under it, labelled unwatched, first, changed or safety.
166 · A built graph travels to another machine
Graph export to one portable file
aof graph export <path> writes the codebase graph, graphify’s cache and the inputs stamp to one gzip bundle stamped with the source commit, build time, graphify version and project identity, and replaces an existing file only with --force.
Graph import into a clone of the same project
aof graph import <path> lays a bundle out as the clone’s graphify-out/, refuses a bundle from another project with no override, verifies every file before swapping it in whole, re-keys the carried cache to the clone’s line endings, and reports the graph’s age and how many commits it is behind HEAD.
A build that runs graphify only when its inputs changed
A whole-root code-only aof graph build . stamps what it read (graphify-out/aof-inputs.json, line-ending-neutral) and runs no graphify while no input, git-visible path, graphify version or graph.json has changed, on the building clone and on any clone that imported the bundle.
- graphify’s
manifest.jsonnames every file it read – the stamp covers inputs by those keys; a manifest with an absolute key is never stamped. - A CRLF and an LF checkout of one commit extract to the same graph – measured equal on this repo’s Windows and WSL clones (same nodes, edges and impact sets).
A code-only build’s own time budget
A whole-root code-only build defaults to a 10-minute graphify budget; every other graphify spawn keeps the 2-minute bound.
A WSL deploy that runs
scripts/deploy-wsl.sh writes the distro’s bin/aof.mjs as a forward to packages/core/bin/aof.mjs and completes, including the native reinstall.
165 · Planning runs in Cowork
Planning in Cowork on the same work tree
A repo’s planning can be done in Claude Cowork: the operator writes a PRD with the pinned pm-skills planner and runs /aof:shatter, which writes framed milestone SPECs and spikes, with their origin and slug depends edges, into the repo’s wiki/work/backlog/, and aof work validate and aof work promote from Claude Code or Codex then accept and schedule them (165/03 E5).
Three planning runtimes, one lock
aof planning init installs planning for claude, codex and cowork side by side, each recorded as its own entry with its own pinned sha, so adding one never disturbs another (m165/00, m165/03).
One planner commit everywhere
The planner Cowork runs is packaged from the same 40-hex pm-skills commit Claude Code installs, never from a floating ref (m165/02).
165/00 · The planning lock records each runtime
One planning entry per runtime
.aof/aof.lock.json holds planning: { runtimes: { <runtime>: <entry> } }, and a planning init for one runtime leaves every other runtime’s entry deep-equal and every non-planning section intact (FF-16501).
A second runtime needs no –force
A planning init for a runtime with no entry is added beside the existing ones, while a re-install of a recorded runtime is refused without --force, and --force re-pins only the runtime it names.
Legacy flat sections still read
A pre-165 flat planning section reads as its one runtime’s entry, and the next write converts it to the per-runtime shape.
165/01 · The planning skills render as a Cowork plugin
The aof Cowork plugin
renderPlanningPlugin() returns the aof plugin’s file map: .claude-plugin/plugin.json plus skills/shatter/SKILL.md and skills/explain/SKILL.md, each the one cowork preamble followed by its command’s body unchanged (FF-16502), and refine is not shipped.
One command-to-skill mapping for codex and cowork
commandAsSkill takes each runtime’s preamble from its mapping, and codex’s mapped skills render byte-identically to their pre-165 output.
Markdown-only without the CLI
A cowork skill probes for aof, skips each CLI step when it is absent and lists the skipped steps, and never commits.
A reproducible package
zipStored turns a file map into a stored (uncompressed) zip whose bytes depend only on the map’s contents, never its order, using only node: built-ins (FF-16503).
165/02 · The pinned planner is packaged for Cowork
Planner packages from the recorded commit only
packagePlanner({ sha }) packages pm-skills only from a tree whose HEAD equals the 40-hex sha, cloned over HTTPS at MARKETPLACE_REF with core.autocrlf=false and core.eol=lf, and refuses any other tree with a named reason (FF-16504).
A standalone package per recommended plugin
Each recommended plugin (with the optional pm-market-research on request) is its own file map carrying the repository’s root LICENSE, and pm-ai-shipping is never packaged.
No clone left behind
The live clone goes to a temporary directory that is always removed, and a caller-injected tree is never removed.
165/03 · Planning init delivers Cowork
aof planning init –runtime cowork
aof planning init --runtime cowork writes aof.plugin and one package per recommended planner plugin under the aof home’s cowork/ (~/.aof, or AOF_GLOBAL_HOME), prints each package’s full path with the upload steps, and spawns nothing.
The project carries only its lock entry
A cowork run writes nothing in the project except .aof/aof.lock.json, whose cowork entry names every package relative to the aof home and records installConfirmed: false (FF-16506, FF-16507).
Cowork beside claude and codex
A cowork entry is added beside the other runtimes’ entries without --force, a re-install needs --force and re-pins cowork alone, and an explicit --scope with cowork is refused, writing nothing.
All or nothing
A cowork run whose clone or packaging fails writes no package and no lock entry, and a --dry-run resolves no sha, clones nothing, spawns nothing and writes nothing (FF-16505).
A skill finishes unvalidated without the CLI
Each packaged skill’s preamble says that without aof a check the procedure requires is not a failed one, so a run marks its report unvalidated and names the checks to run from Claude Code or Codex.
164 · Every project change reaches the global store
Local files are canonical for a node’s own items
A project’s own records are the source of truth on reads and on writes: the global store is a projection updated only from files, and it adds only what this disk does not hold – other projects, and work another machine owns (stories 00 and 02; m164/00 and m164/02 outcomes state each half).
One sync pass, three drivers
Every change to a record file reaches the store through one pass – detect against the observed baseline, mark in the existing effects journal, drain through publish-projection – run by every CLI command for its own workspace, by the daemon at start and by every daemon tick over the registered set, whether or not a verb raised an event (m164/00, m164/01).
No sibling ledger
Sync state lives only in journal.sqlite and projection.sqlite; no package source opens any other database under the global mesh root (FF-16403), and no module writes a record doc from a stored row (FF-16404).
An owed sync lands and is visible
An owed publish never gives up and the operator can read its lag in doctor (m164/03); a publish over a partial node roster leaves membership intact and stays owed (m164/01).
164/00 · A local change is noticed and owed
Changed-since-observed baseline
work_item_observations (projection store schema 11) holds this node’s fingerprint of each row it last wrote, per workspace, plus a stat digest under ref *; only a successful upsert advances it, and packages/work/src/local-change.mjs is the one home of rowFingerprint/locallyChanged (FF-16401).
A local change takes its row
A record whose projected row changed since this node observed it takes authorship of its row in the next publish, exactly as an operator verb’s ref does; an untouched record never overwrites another node’s report, and a prose-only edit claims nothing.
- Another machine’s lane is honoured – a changed ref an active assignment holds is reported
held, never written, and lands over the lane’s report once the lock releases.
Workspace changes are owed in the effects journal
A noticed change appends one workspace.changed event (id from workspace and digest, collapse key ws:<id>) drained by the existing publish-projection reactor; newer marks supersede older owed ones, a verb’s event is never superseded, and the journal is schema 2.
Every aof command syncs its own workspace
The CLI face runs the sync pass for the command’s own propagating workspace before it sweeps owed effects, and a failing pass never changes the command’s own result.
A linked worktree never publishes
A linked git worktree of a mesh-published repository is refused by the propagation predicate (mesh-workspace-secondary-checkout), so only the repository’s own checkout publishes the workspace’s rows and root.
164/01 · The desktop app syncs every workspace and catches up when it starts
The daemon tick syncs every registered workspace
Every propagation tick runs the one sync pass over resolveTickWorkspaces – this node’s linked workspaces plus the launch cwd when it is a workspace – the same set presence reads; the cwd-only publish is gone (FF-16405), and each skip is a coded warning while the tick goes on.
The daemon catches up when it starts
startLauncher runs the same pass over the same set before its first tick, so a change made while the desktop app was off is in the store as soon as it starts (measured live on node-7297, 2026-10-19).
A torn node record never empties membership
A registry publish over a node roster with an unreadable record writes nothing and fails node-record-unreadable, so the last membership stands and the owed change retries until the record is readable.
164/02 · My own items read from my files
Own records answer from disk
aof work find, list and next answer a record this node reported from its local file; a cached row overlays a local record only when another node reported it and the file has not changed since this node observed it, decided by the one cacheAnswers predicate (FF-16402).
Doctor reads the way the stream does
aof work doctor’s overlay asks the same cacheAnswers, with this node identified by the id the publisher stamps rows with, so its coherence group and the stream view judge one identity; own rows no longer raise cache-status-divergence.
Store-only items are still listed
An item with no local folder (work another machine owns) is still listed from the store.
164/03 · Owed sync never gives up and shows how far behind it is
Retry until done, with backoff
A reactor may declare retry: "until-done" (anything else is refused when the reactor table is built); every publish-projection declares it, and a failed step waits min(15s × 2^attempts, 15 min) from its last attempt instead of stopping at the five-attempt ceiling, which every other reactor keeps.
Explicit retries skip the backoff
doctor --converge and the lane cleanup drain with backoff: false, so an operator retry pays a recovered store at once.
Doctor shows the sync lag
Where the workspace propagates, aof work doctor prints one sync: line per workspace (last success, owed steps with age, attempts and error, records changed since the last sync), raises sync-failing/sync-behind as warnings that never fail validate, and adds a sync array to --json only where it applies.
163 · The operator can read the loop’s open questions
aof work questions
aof work questions reads the ask files under loop-asks/ through readAsks, with no daemon round-trip. For each ref it prints the latest ask that is waiting or parked, oldest first. A row carries the ref, phase, runtime, asked instant and age, workspace name, full question, numbered choices and aof work answer <ref> "…". --json emits the rows. When nothing waits, it prints “No open questions.” and exits 0.
A stale ask offers no answer line
A row whose run is no longer running is marked stale, with no answer line, and names aof work loop <scope> --resume. The verb judges this through askRunStopped / runsBehindAsk in work-loop/ask.mjs, the same functions the loop’s sweepStaleAsks calls.
–all widens to every workspace on this node
--all lists the open local asks of every workspace on the node. It names each workspace from its descriptor, or by id when there is none, and reports liveness unknown when that workspace’s checkout cannot be loaded.
Worker questions in this workspace are listed
An assignment that a worker parked on a human in the current workspace is listed with its node. Where the question text never reached this machine, the row says so and points to the worker’s terminal. A local ask outranks a worker’s ask on the same ref.
One waiting-on-a-human rule
standingAsk in work-loop/ask.mjs is the only definition of “this run waits on a human”. aof work resume receives it injected (openLastAsk is gone), keeps its NEEDS YOUR ANSWER row, and points to aof work questions for the text.
The /aof:questions wrapper
packages/core/assets/commands/questions.md runs the verb and renders through aof work update into .claude/commands/aof/questions.md and its codex and opencode faces. work:questions is registered and routed as aof work questions.
162 · A running loop is visible in the UI
Loop sessions stream to their cards
A loop-driven Claude session streams its terminal live to its session card on the home and the fleet, from the control node and from a worker, each through its own node’s daemon over a loopback, token-gated ingress that stamps the daemon’s node id.
- A daemon runs on the node that runs the loop – the drive child relays through
<aof home>/mesh/terminal-ingress.json; with no daemon it degrades once and streams nothing. - The drive is a Claude PTY drive – a Codex native drive has no PTY to relay.
Loop panes are read-only and say why
A session row that names a driving loop mounts read-only on every surface; no keystroke is sent, and the read-only badge reads “read-only – a loop drives this session; answer it with aof work answer”.
Loop progress on the home and the fleet
The running loop’s line shows the current drive’s elapsed time and its last activity, turning to “quiet
Loop progress in the terminal
aof work loop prints <ref> – <elapsed>, <n> subagent(s) active, last activity <age> ago every five minutes while a drive runs, per lane in a wave, silenced by --quiet.
The home names a running loop
When a live node runs a loop whose session is not in the grid, the home’s empty state names the loop and says its session has not reported, or is not relaying, instead of the bundle sentence.
Node liveness follows the heartbeat
A node reads live while its presence heartbeat is inside the window, whether or not any workspace has republished since.
161 · The compiled wiki stays true
Every direct wiki page’s source citations resolve, and doctor keeps them resolving
All 12 direct wiki pages currently have zero wiki-source-unresolved or wiki-source-unreadable findings, scoped or unscoped. A later move or deletion that strands a citation in any of them becomes a doctor warning (m161/01 states the probe whole).
The memory page is held to the code it describes
wiki/memory.md now describes the delivered procedure (m161/02). Because every src/ path it cites is checked by the same probe, a module move that would stale the page now surfaces as a warning instead of drifting silently.
161/01 · Wiki source citations are checked by doctor
Wiki source citations are a doctor finding
aof work doctor, scoped or unscoped, reads every Markdown page directly inside <project root>/wiki. It reports each cited local src/ file that resolves to no current file through resolveCitedPath and the recorded rename history as one wiki-source-unresolved warning per page and file, naming the first occurrence’s line.
An incomplete scan says so
An unlistable wiki directory, an unreadable page, or a Markdown symlink pointing outside the project is a wiki-source-unreadable warning that claims nothing about that input’s citations. A project with no wiki/ directory reports nothing new.
Existence is a stat, never an import
A cited source file is checked for existence as a regular file inside the project. It is never imported or executed. A directory, an external URL, a glob family, a non-source path and a path that traverses outside the project never count as a resolving source citation.
161/02 · The memory wiki describes the delivered procedure
The memory page describes memory as delivered
wiki/memory.md describes the delivered memory procedure from milestones 148, 153 and 157, citing a resolving source or ADR for each part. It covers the three layers and eight record types, source locators and tags, episodic provenance, recall filters and --all, the six decision edges, recall observations, recurrence and promotion, accounting, and the order of retrospective evidence.
The page carries no live-store facts
The page names no active backend or record count for this repository. It sends readers to aof work memory status --json for both, and it lists no retired verb as supported.
160 · The whole-tree suite signs off in minutes
Every pull request signs off on the whole tree in under two minutes
Each pull request to main runs all 12,988 registered cases across 8 Linux runner shards, inside a 10-minute budget the workflow enforces; on PR #12 the suite wall is 1.8 minutes, and each file’s drift against main is reported beside it (m160/13 states the workflow whole).
A timeout is a not-finished unit, never a red
Wherever the suite runs – a sharded run on the control node, a gate, or a PR shard – a unit stopped by its own limit, the budget or a stop is reported not finished, and the gate’s door refuses a not-finished row as its own case with its own repair (m160/05).
The control node’s gate is a correctness gate
aof work regression-gate on the control node runs the whole tree with no budget, in a clean launch environment that dies with its session (m160/04), and its row is what the accept door reads; the budget lives with the pull request suite.
A test run is hermetic for the operator’s files
A test run writes neither the operator’s ~/.aof nor their ~/.claude.json (m160/12), and no test spawns the registry runner unless the runner is its subject (m160/11).
160/00 · The loop reads each git fact once per step
A loop step asks git each ref or topology question once
memoGitReads in packages/execution/src/worktrees.mjs answers a repeated rev-parse, symbolic-ref, show-ref, merge-base, rev-list or worktree list call from its first status-0 answer within one step. It forwards content and working-tree reads every time, clears itself on any other verb, and never keeps a failed read.
A step is one in-process run of git work
The wave gives a fresh memo to the ask-and-dispatch tick (and the dispatch calls it makes), to a lane’s prelude, to a lane’s commit, to a merge home, to a cleanup, and to the epoch re-mint. No memo outlives the step that opened it.
The loop’s commit names its own tip
recordCommitTip reads the sha from the commit’s own summary line, so committing through the loop’s port needs no second rev-parse HEAD. A lane already at its base is not advanced.
One lane cycle is held under a pinned call ceiling
FF-16001 pins one ready story’s lane cycle at 34 git calls through the port (37 unmemoised). The measured cases make 37% fewer git spawns (471 → 295 over three wave cases).
160/01 · Tests that are not about git run on a git model
A git model stands behind the loop’s port in tests
test/support/loop/git-fake.mjs models the 16 verbs the loop sends through its port. It refuses any verb or form it does not model, so a case can never pass on an answer the model invented.
The model is held to real git
FF-16002 runs one 49-step script against real git and against the model, and compares stdout, status and directory bytes after every step.
Loop suites run their non-git cases on the model
31 case sites in loop-command-wave, loop-command-reconcile and loop-command-stops run with git: "fake". Cases whose subject is git (a real merge home, a conflict repair, a test-side git log or merge-base) stay on real git. Alone, the three suites run 18–31% faster with about half the git calls.
The dispatch command takes the caller’s exec
packages/work-loop/src/commands/dispatch.mjs threads ctx.exec through admission, list, sweep and cleanup. Production leaves ctx.exec unset, so the CLI is unchanged.
160/02 · A focused run loads only what it names
A selection launches its own program
work.test.selectProgramArgs compiles beside args and replaces it only when a launch selects files. This repository declares ["scripts/test-shard.mjs"], so a focused aof test launches the light entry and never imports the registry. The gate’s toolchain drops the key.
The shard entry runs a named selection
node scripts/test-shard.mjs <suite>... [positions] runs every file it is named through the runner’s one loader (loadSelected in scripts/test-harness.mjs). It enters through the runner only for a selection holding a suite that imports the runner. A focused run of one small suite takes 3.4 s instead of 7.3 s.
The isolation guard knows the light entry
.claude/hooks/aof/guard-test-isolation.mjs treats scripts/test-shard.mjs as a test invocation, so an unisolated shard run is blocked like an unisolated runner.
160/03 · A child process compiles once and loads only its verb
The compile cache is on in every aof and test process
packages/core/bin/aof.mjs and scripts/test-harness.mjs enable Node’s compile cache before the application graph loads, and the bin reaches cli.mjs only by a dynamic import. On the control node the cache engages and hits, and it does not lower start-up (F-03).
aof --version loads 16 modules
The session hooks and an unused workspace binding left cli.mjs’s static imports, so a version check loads 16 file modules instead of 41.
A child’s module count is pinned, shrink-only
FF-16004 pins each measured route’s module count by equality: --version at 16, work find at 706. A story that adds a module to a route’s closure must move its row.
160/04 · The gate launches clean and dies with its session
The gate’s suite starts in a clean environment
cleanLaunchEnv drops every GIT_*, CLAUDE* and AOF_* variable (case-insensitively), keeping only AOF_GLOBAL_HOME, before the gate launches its suite. The gate’s own environment is untouched.
A stopped driven run stops its gate
The gate’s keep-alive reads its driven run’s record on each beat. When that run is no longer running, the gate aborts its launch and refuses with regression-gate-stopped, writing no row. Measured: 58 s from the settle to no process left.
The sharded runner stops when its stdin ends
The pool lives in scripts/test-sharded-pool.mjs. It watches stdin only when stdin is a pipe, and on its end or close it kills every in-flight unit and starts no more. A stopped run writes no timings.
160/05 · The runner enforces the budget, and a timeout is not-finished
The sharded gate is held to its budget
The gate passes work.test.gate.budgetMinutes through budgetArgs (this repository: 10, ["--budget-min", "{minutes}"]) and bounds its launch at the budget plus 2 minutes. At the budget the pool kills its in-flight units and starts no more. A serial run is held to neither.
A unit out of time is not finished
A unit the pool killed (by a stop, by the budget or at its own limit) names no failure, prints no not ok, and is never retried alone. The report prints # not finished - <unit> and # not started: <n> units.
not-finished is a row result the door refuses
GATE_RESULTS holds not-finished beside green, red and override, with gateResult as the one home of their precedence. aof work status <NN> done refuses a not-finished row as regression-gate-not-finished, and the verify prose routes such a row to a re-run or a faster suite, never to a code repair.
A killed unit cannot hold the pool open
Once a unit has exited, its output gets a bounded drain (10 s). After that the pool stops waiting, even if a process the unit started still holds the output.
160/06 · Citation history is read only when a citation misses
Citation history is a lazy source
readRenameMap answers a loader whose first load() reads git history once. Doctor’s snapshot, its wiki probe, validate’s reads check and tune all resolve each citation on disk first, and call load() only on a miss.
A stream whose citations resolve spawns no git for them
aof work validate 160 makes no git call (4 before). Doctor and validate report byte-identical --json findings to the eager build, and one wave case makes 82 git calls instead of 132.
160/07 · A test’s child aof runs from one bundle
A sharded run’s child aof starts from one bundle
scripts/test-sharded.mjs builds one ESM bundle of cli.mjs per run, into the run’s own directory, before the pool starts, and every unit’s child aof loads it through AOF_TEST_CLI_BUNDLE; aof work find 160 --json starts in a median 275 ms from it against 999 ms from source.
The bundle and the release share one recipe
CLI_BUNDLE_RECIPE in scripts/build-sea.mjs holds the platform, target and externals for both; the test bundle rewrites each module’s import.meta to its own source file’s URL, so assets, the manifest and git resolve exactly as from source.
The bundle is held to the source by a parity control
FF-16008 runs a fixed command set from the bundle and from the source and requires the same stdout, stderr and exit code; a control whose subject is the bin’s own loading strips the variable, and a focused run sets none.
160/08 · Tests spend git only on what they assert
The git model merges a lane home
test/support/loop/git-fake.mjs answers and writes merge --no-ff of a lane branch as real git does, refuses a merge in which both sides changed one path, and refuses any merge form it does not model; sixteen more loop cases run on it (12 wave, 4 reconcile).
A real-git fixture starts from a template
The lane, dispatch and mesh-worker fixtures copy a repository built once per process instead of running init and config per case; work-dispatch-lanes makes 1 init and 2 config calls where it made 88 and 176.
The loop suites spend fewer git calls
loop-command-wave makes 2,219 git calls (4,023 before) and loop-command-reconcile 1,160 (1,518), every case still green.
160/09 · Small suite files share a unit
Short suite files share a process
The sharded runner packs whole files estimated under --pack-seconds (15) into units of about --pack-target-seconds (60), largest first, never splitting a file; this repository’s plan is 339 units (68 packed, holding 964 files) where it was 1,235.
Every account stays per file
The shard entry prints # file <path> <seconds> <executed> for each file it runs, import time included, so timings, lost-case detection and a not-finished report still name files; a failed packed unit is retried alone as a unit.
160/10 · The audits over this repository read once
The source scanner is linear
stripComments and blankStringLiterals decide regex-or-division from the token before the slash, never by re-scanning the prefix; their output over every .mjs under test/, packages/ and scripts/ is byte-identical to the previous scanner’s, and blanking the test tree takes 977 ms (10,116 ms before).
The tune corpus lists the stream once
assembleCorpus lists the stream once per assembly and hands each item’s row to the snapshot read, so one assembly over this repository takes 0.7 s (12.6 s before) and produces a byte-identical corpus.
Memory ingest lists the stream as often for one item as for many
aof work memory ingest hands each milestone’s listing row to the snapshot read, so its episodic indexing no longer lists the stream once per milestone (a fixture of 12 milestones is listed as often as one of 1; before, 14 times against 3).
160/11 · A test that needs a suite run starts the shard entry
A test that needs one suite run starts the shard entry
core-workspace, notify-channels, discord-bot and site-build’s gate cases start scripts/test-shard.mjs, not scripts/test.mjs --only; FF-16013 holds every spawn of the registry runner under test/ to an allowlist of one entry, test-command-contract’s bounded runner() child.
The Pages gate runs its one suite through the shard entry
.github/workflows/pages.yml runs node scripts/test-shard.mjs test/arch/loop/acd-loop-document-current.test.mjs under a fresh AOF_GLOBAL_HOME.
160/12 · Tests keep their state their own
A test run trusts folders in a claude home of its own
The claude folder-trust writer resolves its home from options.homedir, then AOF_TEST_CLAUDE_HOME, then os.homedir(); scripts/test-harness.mjs gives every test process a temporary AOF_TEST_CLAUDE_HOME, removed at exit, and the sharded runner gives each unit its own home, so a test run neither reads nor writes the operator’s ~/.claude.json.
A store opens without rewriting a current schema
The global work projection store and the effects journal migrate only an absent or older schema; opening one at its current version executes no write statement, still applies its pragmas, and a newer version is still refused.
160/13 · The whole tree runs on every pull request
The whole tree runs on every pull request, inside its budget
.github/workflows/test.yml runs every registered case on each pull request to main and each push to main, as 8 shards of 4 workers on ubuntu-latest; the summary job fails the run for a red shard, a case no shard ran or two shards both ran, an unfinished unit, a missing report, or a suite wall over 10 minutes. On PR #12 the suite signs off in 1.8 minutes.
The runner takes one shard of the plan
scripts/test-sharded.mjs --shard <i>/<n> runs one balanced bin of the whole plan, counts only its own cases, and --report-json writes its verdict for the summary.
Each file’s drift against main is reported
scripts/test-ci-summary.mjs compares every file’s time with the timings main last saved to the Actions cache and lists the files past the threshold in the job summary; drift alone never fails a run.
The control-node gate keeps no budget
.aof/aof.config.json declares no work.test.gate.budgetMinutes, so aof work regression-gate on the control node is a correctness gate under its own deadline.
The whole tree runs on Linux
Every registered case is green on ubuntu-latest; install.ps1’s extraction, file-lock and registry rows are Windows-only and skip elsewhere, flagged.
159 · The operator’s auto-memory is an opt-in memory source
Operator procedure recallable beside the work stream
On a node whose config sets memory.sources: ["claude-auto-memory"], one recall answers from both the operator’s Claude auto-memory and the repository’s lessons, each record citing its own source; the capabilities are stated whole in m159/01’s outcome (opt-in source, operator lessons, rebuild-faithful recall).
Node-local by construction
Operator records exist only in the git-ignored local index of the node that holds the Claude home, never in graph extraction or mesh transport, and an unconfigured node’s index is exactly the work stream (FF-15901..03).
159/01 · Recall the operator’s local auto-memory beside work-stream lessons
Opt-in operator auto-memory source
memory.sources: ["claude-auto-memory"] makes the next whole-stream ingest add the selected project’s Claude auto-memory entries to the index; an absent or empty setting indexes exactly the work stream, with no external read.
Operator lessons from feedback and project entries
Each feedback or project entry becomes one read-only lesson (owner operator, near-miss, source type kept as a tag, whole body searchable, locator is the entry’s own path), while user, reference, MEMORY.md and malformed entries are skipped with a named diagnostic that never reveals contents.
Separate, rebuild-faithful recall
Operator and repository records of the same fact both appear, each citing its own source; edits, deletions and disabling are reflected on the next full rebuild, a scoped rebuild stays work-item-only, and neither graph extraction nor mesh transport ever sees the entries.
158 · Native permission approvals
Native automatic permission review
Codex-driven phases select and verify native automatic review, allowing approved task operations to continue without another operator permission answer.
Attributable review lifecycle
Run observations retain native review identity, status and interruption evidence without storing grants or treating approval as successful execution.
Fresh verification after historical permission failure
A fresh loop can verify completed work with the selected model while preserving its build and earlier non-retryable permission failure.
157 · Episodic memory is recallable
What happened on an item is recallable through the memory seam
After ingest, recall "" --item <ref> --area delivery --all returns the item’s findings, raw feedback and terminal runs, each resolvable to its source (m157/01). The m148 retrieval eval stays green with the episodic pool ranked beside lessons and ADRs.
Three workflow edges read that history before they act
Continue on attempt 2+, verify at finding triage and the retrospective’s evidence step each consume one retained recall, as described in m157/03. The two decision edges raise the declared decision-recall roster from m153’s four to six.
157/01 · Episodic records resolve to their sources
Three episodic record types
aof work memory ingest compiles finding, feedback and run records from existing sources, on both the local and graphify backends.
- A finding comes from a
VERIFICATION.mdorSESSION.mdregister row orF-heading; a row and a heading for one id are one record. - A feedback record is a
kind: "raw"FEEDBACK.ndjsonrow; classification and recall rows are not records. -
A run record is a terminal run (
done,failedorcancelled), carrying its loop stage, outcome,failureReasonand its matched agent’s stalls. - Index version – the
INDEX_VERSIONbump makes a store written before this change report stale until it is re-ingested.
Every episodic record resolves to its source
A record’s source is a line locator (<path>:<line>) or a JSON locator (<path>[:<line>]#<pointer>). readRecordSourceValue returns the line or the JSON value it names, and a locator that would leave the work tree is refused before any read. Over the live stream on 2026-10-12, all 1,226 episodic records and all 30 stall locators resolved.
- Ingest is read-only on its sources – rebuilding writes nothing to a register, a ledger, a run record or a snapshot.
157/02 · Every consumer accounts for the episodic pool
Status counts an episodic layer
aof work memory status puts finding, feedback and run under the episodic layer of the layer map. Both backends report the same partition, and every count sums to recordCount.
Recall returns an item’s whole history
aof work memory recall … --all returns every record in scope and overrides --limit; without it the limit still applies. A failed run’s block line shows its failureReason, and a lesson’s block line is unchanged.
Lesson-only consumers stay lesson-only
Tune’s corpus counts each lesson once over an ingested stream, and lesson recurrence never matches an episodic record.
157/03 · A retry, a triage and a retrospective recall what happened before
A retried build recalls its prior attempt
On attempt 2 or later, the continue procedure ingests, then recalls "" --item <ref> --area delivery --all --json. Before the first code change, it names the prior run (by retryOf, else the latest terminal earlier attempt), that run’s failureReason and the story’s findings in the milestone STATE.md. It submits one continue-episodic / developer observation.
Finding triage recalls the stream’s prior findings
The verify procedure ingests, then recalls "<finding text>" --kind finding --block across the stream before each triage decision. It cites a prior finding only on matching cause and evidence, never closes the new finding, and submits one verify-finding / verifier observation on the item being verified.
The retrospective’s evidence comes through recall
The retrospective procedure refreshes observability, ingests, and gathers findings, feedback and terminal runs through one --all recall of the item. When memory is off it reports the evidence as unavailable and does not fall back to reading the raw ledgers.
- Prompt-bound – all three edges are procedure text, held in the authored source and every render by FF-15702. Nothing in the runtime enforces that an agent follows them.
156 · One loop selects an assistant per phase
Model-only phase routing
aof work loop and aof work drive take models and efforts per phase (--model [PHASE=]MODEL[:EFFORT]) and infer each phase’s assistant from the model; no runtime flag exists on either command.
One pinned mixed plan per loop
A loop records one execution plan naming each phase’s runtime, model and effort; refine, continue and verify run on their own assistants in one command, and review and repair inherit the continue selection.
Recovery keeps the recorded selection
Resume, retry, lane reopen and worker handoff run each phase on its recorded assistant, model and effort, refusing conflicting flags and never rewriting a recorded failure.
Pre-launch refusals
An unknown, ambiguous or unavailable model, an unsupported effort, or a missing/incompatible Codex asset is refused before any phase run is minted, naming the cause and remedy, with no fallback to another assistant.
Neutral phase settings in the editor
The configuration editor saves per-phase models and efforts without assistant selectors and preserves unrelated settings.
155 · One agent-mode setting governs every session
The agent-mode chain and its default have one home
@aof/contracts/agent-mode (packages/contracts/src/agent-mode.mjs) exports AGENT_MODE_DEFAULT = "solo", agentModeFromConfig (work.agents.mode if valid, else solo) and sessionAgentMode (loop key, else work.agents.mode, else solo; null for verify).
The loop’s drive follows work.agents.mode when no loop key is set
aof work drive refine|continue composes --solo or --orchestrated from sessionAgentMode. loop-bounds.mjs no longer carries LOOP_AGENT_MODE_DEFAULTS, and loopAgentModeFromConfig answers the loop key alone (or null).
Every role-spawning prompt defaults to solo
Refine, continue, review, assimilate-code, migrate and autonomous state that an unset work.agents.mode resolves to solo. --solo and --orchestrated still override for one run. Verify reads no mode.
The schema and renders state the one default and the chain
schemas/aof.schema.json, the manifest, the lock and the Claude, Codex and OpenCode renders agree with the source prompts.
A per-role map is reported inert under the effective solo mode
aof project validate emits model-map-inert-under-solo / effort-map-inert-under-solo whenever the effective mode is solo, including an unset work.agents.mode.
154 · Codex drives the AOF loop alongside Claude
Two assistants share one governed loop
AOF’s existing work loop can execute and recover native Claude and Codex phases using one retained runtime choice, assistant-specific assets and common work-item gates; existing projects retain Claude as their default.
154/00 · A shared session boundary preserves Claude execution
Runtime session boundary
The existing Claude execution driver and the Codex adapter share one normalized session interface while the existing loop retains sequencing and retry policy.
154/01 · Runtime and model choices survive resume
Durable execution choice
Runs and assignments retain the resolved runtime, profile, phase models and effort; resume rejects a conflicting selection and legacy records retain Claude behavior.
154/02 · Codex executes a bounded phase through App Server
Bounded native protocol
The admitted Codex App Server profile records native threads, turns and normalized results, persists blocking questions, and bounds protocol queues, deadlines and owned-process shutdown.
154/03 · Codex discovers correctly scoped native assets
Native discovery outputs
AOF renders Codex skills, role configuration, scoped guidance and supported settings into their native discovery locations without changing the Claude render target.
154/04 · Codex upgrades preserve operator-owned files
Ownership-safe native upgrades
Recorded ownership and drift checks govern Codex asset updates and legacy cleanup while preserving operator-owned settings, guidance and neighboring files.
154/05 · Shared workflows render assistant-specific instructions
Runtime-specific workflow assets
Bundled runtime variants and project overrides supply native procedures, role settings and explicit delegation rules while preserving shared scope, ownership and verification obligations.
154/06 · Codex drives and recovers the existing work loop
Native governed phases
The existing loop drives Codex phases with driver-owned run identity, durable question delivery, pinned execution settings, bounded warm fixes and operator cancellation.
154/07 · Workers retain the selected runtime and assets
Durable worker handoff
Worker assignments retain their execution envelope across reconnect, prepare lock-owned native assets and start the lane view through the guarded status transition while retaining one primary-owned run.
154/08 · Codex activity and usage are reported honestly
Attributable native observations
Codex activity and token counters are joined to captured run, thread and turn identities; unavailable monetary cost and unsupported transcript measurements remain explicitly unavailable.
154/09 · Codex projects can choose an explicit memory backend
Explicit memory dependencies
Configuration and inspection expose the memory backend and its extraction dependency, including the local backend that does not launch Claude.
154/10 · The config editor explains effective assistant settings
Execution configuration editor
The configuration editor validates and persists runtime and scoped model settings, shows their effective selection, and retains keyboard and responsive behavior without executing the work loop.
154/11 · Live acceptance proves Codex and preserves Claude
Reproducible native acceptance envelope
The runtime verification fixture and recorded live evidence distinguish preparation, scripted transport, actual model execution, recovery and final acceptance for the admitted Codex profile and existing Claude driver.
153 · Memory closes the loop
Recall to recurrence is one traceable chain
At milestone level, the system can now answer whether a stored lesson was surfaced, whether the decision it informed honoured it, and whether the mistake came back. The chain has four links:
- a declared edge records the recall with per-record verdicts (m153/04, m153/01);
- the retrospective stamps the repeat as
Recurs(m153/02); memory statusanddoctorcount and flag it (m153/03);tuneturns a recurring prompt-destined lesson into a concrete advisory patch (m153/05).
An operator traced one lesson end to end, from ledger and source alone, at the @uat sign-off.
- Forward only – only items captured and retrospectives written after this milestone carry the chain. Archived ledgers and retrospectives are never back-filled, so historical lessons count surfaced 0 and recurred only through later stamps.
- Verdict is the recorder’s word – an
honouredverdict is the deciding role’s own judgement, not proof that the lesson prevented a mistake.effectiveness.basissays so.
153/01 · Recalls have complete verdict records
Recall observations in the item ledger
aof work memory record-recall <ref> --input <file> appends one kind: recall row to the item’s FEEDBACK.ndjson. The row carries the query, the scope, and an explicit honoured | departed | irrelevant verdict for each returned record. A resubmission of the same id is idempotent (replayed: true), and a conflicting one changes no byte.
- Composite identity – a returned record is identified by
item+id+recordType, never by its id alone.
Raw feedback stays raw
The finding-escape counter and every other ledger consumer select their partition explicitly. Recall rows never enter the raw-feedback denominator (FF-15301), and aof work feedback refuses recall fields.
brief is retired
aof work memory brief is an unknown verb, refused before any backend is resolved. Help no longer lists it.
153/02 · Lessons name recurrence and promotion
Forward lesson metadata
From each retrospective’s promotionFrom watermark onward, a lesson’s meta line carries **Promoted to:** control:<id> | rule:<path> | prompt:<path> | none and an optional **Recurs:** <item>/R<n>. Both are held by the memory-vocabulary validate rule and indexed into the lesson’s status and tags. Lessons below the watermark are never rewritten.
Calibrated recurrence matching
aof work memory recurrence "<text>" --exclude <item>/R<n> --json scores the text against every indexed lesson, across kinds. It names qualifying only when a score is strictly above threshold 0.23 (calibration 153.02-1). The result always carries the threshold and the calibration version. The matcher shares no code with retrieval, so base ranking is unchanged (FF-15302).
- Labelled basis – the threshold holds only as long as
test/arch/memory/fixtures/recurrence-pairs.jsonand its frozen split stand. A change to the weights or the rule bumps the calibration version and re-measures.
153/03 · Memory reports effectiveness and health
Effectiveness accounting
aof work memory status --json reports an effectiveness block. It gives surfaced, honoured, departed, irrelevant and recurred counts per record type, and per lesson with --lessons. It is built from the ledgers’ recall rows and the retrospectives’ Recurs stamps, and it flags corrupt or unresolved evidence in diagnostics and complete.
Recurring and aged-unpromoted doctor probes
aof work doctor reports memory-lesson-recurs for a lesson named in a later Recurs. It reports memory-lesson-unpromoted-aged once an unpromoted lesson has seen 5 or more later accepted milestones (the operator’s choice, configurable). An unknown age gets its own finding. All three are advisory and never fail a normal doctor run.
153/04 · Workflow edges close their memory observations
Captured decision-recall edges
Four edges submit one record-recall observation each, with their own edge, actor and scope: shatter (shatter-po, owned by the first driver, never replayed per driver), refine architect and PO (refine-architect, refine-po), and continue (continue-developer, near-miss scope). The Claude, Codex and OpenCode procedures do this, and an empty recall is still submitted with records: []. FF-15303 holds the roster at exactly these four edges, with render parity.
Retrospective records recurrence and promotion
The retrospective procedure writes the promotionFrom watermark first. For each new lesson it runs memory recurrence before writing, stamps Recurs only on a qualifying match, gives every new lesson an honest Promoted to, writes no document for a clean pass, and ingests only after authoring.
153/05 · Tune proposes promoted prompt corrections
Memory evidence in the tune corpus
aof work tune reads each selected item’s recall observations and the retrospectives’ Recurs stamps. Each candidate shows surfaced, honoured, departed and recurred counts, with citations to its ledger rows and source lessons. Observations from one document do not count as a second source document.
Computable prompt patches
When a recurring lesson has a prompt:<repo-path> destination, the prompt-or-brief-revision proposal carries a concrete from/to patch. It appends a deterministic `` block that quotes the lesson. The proposal stays advisory with no applier. A block already in force is reported as already-in-force. Targets outside the repository, symlinked out of it, or unreadable are reported as findings before any content is read, and the run writes no byte (FF-6207).
152 · Promote shows what to promote next
Promote candidates listing
aof work promote --show-candidates (and --json) lists every backlog item a named promote would accept now. The order is most backlog items unblocked (directly or transitively), then oldest created:, then group path and slug. Each waiting item is listed after them with the gate’s own offenders. The command writes nothing.
- The candidate rule is the depends gate’s – the listing calls
classifyDepends, so a dependency in the stream but not done never blocks, and any change to the gate changes the listing with it.
Promote the head
aof work promote --next-item [--at <P>] [--yes] promotes the head of that listing through the same promoteRow path a named promote takes. It refuses promote-no-candidates, with nothing changed, when the list is empty.
Promote argument conflicts refuse early
A slug, --next-item and --show-candidates are mutually exclusive, --at/--yes do not combine with --show-candidates, and any flag-shaped argument the verb does not know refuses as promote-flag-conflict before the work tree is read.
/aof:promote offers both modes
The command drives --show-candidates --json and --next-item on the verb’s machine face and never chooses an item by reading the backlog itself.
Claude commands carry their argument hint
Every rendered .claude/commands/aof/*.md carries its asset’s argument-hint as one JSON-quoted frontmatter value. Codex carries it in its usage line, and OpenCode copies carry none because their frontmatter has no such field.
151 · aof:add-diagram draws the architecture diagrams a refine left undrawn
/aof:add-diagram draws an item’s undrawn ADR diagrams after refine
/aof:add-diagram <ref> [ADR-NNN] ships for Claude, Codex and OpenCode. With no ADR named, it lists every ADR whose ### Diagram brief has no diagrams/ link and draws each in turn. With an ADR named, it draws that one only, first writing its brief from the ADR’s own text when it has none and the item is not done. An item with nothing undrawn gets “nothing to draw” and no file changes.
It reuses refine’s diagram step unchanged
Each ADR goes through aof diagram plan → the plan’s instructions → aof diagram export → the session pastes the returned block under the brief. enabled: false, available: false, diagram-item-delivered and a PNG miss keep the meanings they have in refine. There is no new CLI verb, and refine is unchanged.
150 · aof:explain says what a work item is for, without writing anything
/aof:explain answers what work items are for
/aof:explain <ref…> [--verbose] ships for Claude, Codex and OpenCode. For each ref, in the order given, it prints a three-to-five-sentence purpose read from the record (a story’s user story, a milestone’s objective, a spike’s question, a chore’s intent, a uat’s scope). It marks backlog and archived items, reports a ref that matches nothing, lists the matches of an ambiguous one without explaining them, and reports an unwritten purpose as not written down. A milestone’s short answer counts its stories. --verbose adds scope, stories or tasks, depends: edges and what is still open.
Asking writes nothing
The command runs only aof work find, doc, list and tasks plus Read. It mints no run, moves no status, stamps no updated: and writes no file, and a live run leaves git status unchanged.
aof work find resolves a folder path
findWork resolves a query holding / or \ (that is not a pair or a span) as a folder path from the caller’s directory, with a trailing record doc naming its folder. Only an exact item folder matches, so wiki/work/backlog answers []. Every reader that resolves through findWork (aof work doc, aof work tasks, …) takes a path, and every form that resolved before answers as before.
149 · Manual mode: continue guides the operator who builds the work themselves
Manual continue
aof:continue <story|task> --manual starts the story through its run, runs the story’s tests once, prints a terminal-only guide (what is still red, the user story, each task’s scenarios, reads:/files: with why each matters, the tests and the gate command, the plan, and an order), spawns no builder, and hands off to aof:review <ref>.
Manual is one story, at every door
--manual is refused when combined with --solo/--orchestrated, refused for a milestone or span, and refused by the CLI door for a non-story ref (continue-manual-not-a-story) or a remote decision (continue-manual-remote). It is a per-run flag only, so the loop never composes it.
aof:review
/aof:review <ref> reviews a story the operator built. It runs the story’s tests, continue’s gate ladder, a blast-radius ranking and continue’s architect and QA lenses, fixes nothing, reports every finding to the operator, and closes its run done. With no Blocker it moves the story to in-review and names aof:verify <ref> next.
aof:code-review removed
The bundle has no code-review command, aof:autonomous has no --ship, and work.codeReview.autoComplete is gone. aof work update deletes the three code-review renders from a repository whose lock records them, and assimilate-code hands off to aof:verify.
148 · The memory corpus holds its vocabulary
Every lesson is reachable by the kind it states
Across the whole corpus – milestone and story retrospectives, live and archived – a lesson that states a kind is indexed under its enum word (m148/02, m148/03), a live lesson cannot be written outside the vocabulary (m148/04), and memory status shows what is left blank or non-enum per field (m148/05).
Signing off an item reads no other item’s state
The whole-tree suite asserts nothing about which items are archived, where a gate sits, the citations inside done records, or an archived milestone’s ledger; a citation sweep reads only the documents of items that are not done (F-148-06).
148/01 · The ranking is held by an eval
Retrieval eval over the live corpus
FF-14801 (test/arch/memory/acd-memory-retrieval-eval.test.mjs) builds the records in memory from the real wiki/work and holds 25 cited (query, scope, <item>/<id>) pairs within the first five of rankRecords, the five lines an agent’s --block shows. A red names every lost pair with its query, scope and the rank it was found at, and a record that no longer exists reds as gone.
- The eval guards the base ranking only – the graph re-rank reads a git-ignored artifact that a clean checkout does not have (ADR-005), so a re-rank change does not show here.
148/02 · A lesson’s meta line is normalised on read
One home for the memory vocabulary
@aof/work/memory-vocabulary (packages/work/src/memory-vocabulary.mjs) is the only source that spells the meta-label grammar, the Kind, Area and Stage enums and the three gap statuses. FF-14802 holds that, and holds the retrospective prompt’s lists and the OUTCOME template’s status comment to the same words.
Meta values normalised on read
The one retrospective parser indexes a lesson’s Kind, Area and Stage, and a gap’s status, as the vocabulary word the written value starts with, and keeps the rest as a tag. A value outside the vocabulary is indexed as written, and a blank stays blank. Title, summary and text are unchanged. On the live corpus (489 lessons) non-enum Kind falls from 65 to 53, Stage from 88 to 15, and gap status from 12 spellings to 3.
- No synonym table – a value that does not start with a vocabulary word is never mapped onto one (148/02 Q1, 2026-10-04).
Every record carries tags
Every MemoryRecord has a tags array ([] when there is none) holding the qualifiers stripped from kind, area, stage or gap status, in that order. INDEX_VERSION and GRAPHIFY_INDEX_VERSION are both 2.
A store built before version 2 is reported stale
Each backend’s status carries index: { version, current, stale }, and a stale store names aof work memory ingest as the fix.
148/03 · Story retrospectives are indexed
Story lessons are recallable
Every item’s RETROSPECTIVE.md is indexed, whatever the item’s type. A story’s lessons carry its ref as item (134/01), so they are cited as (m134/01), and --item 134 reaches them. On the live corpus the lesson count rises from 489 to 788, and every one of the 299 new lessons comes from a story folder.
148/04 · A live lesson’s meta line is held
Validate holds a live lesson’s meta line
aof work validate errors on a live item’s lesson when its Kind, Area or Stage does not start with a vocabulary word, or its Owner is blank. The finding names the file, the R<n>, the field, the value and the legal values. A qualifier after the word is legal, and a lesson with no meta line fails on all four fields.
Doctor flags an archived lesson’s meta line
A doctor lane gives one lesson-meta-archived warning per archived retrospective that holds a non-conforming lesson, naming the count and the ids. It never gates, and no archived retrospective is rewritten. On the live tree it reports 94 files.
The live stream conforms
Every lesson in a live retrospective passes the hold. The 29 lessons in 14 retrospectives that failed it now keep each written word as the qualifier after a vocabulary word, and 145 and 146 carry meta lines authored from their lessons’ text.
The retrospective prompt states the hold
Step 4 of /aof:retrospective says that validate holds Kind, Area and Stage to the listed words with a qualifier after the word, and that Owner must be present. Its lists are the vocabulary module’s (FF-14802), in the asset and in the rendered .claude, .opencode and .codex copies.
148/05 · Memory status reports conformance and layers
Status names every record type’s layer
aof work memory status, on both backends, lists every record type with its count and the layer it serves from RECORD_TYPE_LAYERS, the one partition of record types: semantic holds adr, capability, gap and summary, and procedural holds lesson. A type missing from the map is counted as unmapped and is still listed. The type counts sum to recordCount (FF-14803).
- Status reads every record – the seam composes it from one unbounded recall, which on graphify also loads the graph for re-ranking (ADR-004).
Status reports vocabulary conformance
status reports blank and non-enum counts for Kind, Area and Stage over lessons, blank Owner, and non-enum gap status. Every count is nested, so status gains no top-level number.
The block line shows tags
A tagged record’s --block line carries [t1; t2] between its title and its source. An untagged line is byte-identical to the line before this story.
147 · The loop hands a halt to a fresh session to fix, then resumes
Lane halts are handed to a repair session
A foreground aof work loop handles its three lane stops (lane-open-failed, lane-merge-refused,
lane-merge-conflict) by writing a hand-over file under the aof home (mesh/loop-repairs/<runId>.json)
and driving a fresh Claude session typed /aof:repair <ref> <file>. Every other stop still ends the
loop. --no-repair or work.loop.repair: false turns the hand-over off.
One repair per halt, then the loop resumes or stops
The repair is a run on the halted ref, with brief.loop.phase: "repair" and the halt’s stop and
producer in its brief. It is settled from the session’s outcome. When it ends done, the account
prints Repaired <stop> at <ref> (run <id>) – resuming <scope>. and the loop re-enters its body in
process, on the recorded models and efforts. Otherwise the original halt is re-printed with the repair’s
facts appended to its Details, and the loop stops.
work:drive-repair and /aof:repair
Repair is the fourth drive phase, and its session resolves as continue. The /aof:repair bundle
command renders for Claude, Codex and OpenCode. It fixes only the loop’s own records, never discards a
commit, never touches the operator’s dirty paths or a delivered .feature, and never starts a loop.
The two known lane-halt causes are gone
runs/.heartbeats.ndjson queues are git-ignored, are never captured by the one commit verb (lane and
mesh worker alike), and are untracked where an earlier commit tracked them. STATE.md under the work
dir merges by union (wiki/work/.gitattributes), so two lanes’ build notes no longer conflict.
146 · A capture can skip the backlog
--in-stream on the five top-level add commands
aof:add-milestone, add-story (standalone), add-chore, add-spike and add-uat accept
--in-stream anywhere in their arguments. With it, the capture is scaffolded under backlog/ and then
aof work promote <slug> --json runs at once with no position, so the item lands numbered at the tail
whatever work.intake says. A promote refusal is reported as a stop and leaves the item in the
backlog.
The backlog intake is unchanged without the switch
Under work.intake: "backlog", a capture without --in-stream stays un-numbered in backlog/, and
aof:promote <slug> is still the next step.
145 · A milestone’s loop plan can be drawn
The loop’s wave plan, replayed
planLoopWaves in packages/work/src/ready-wave.mjs replays a milestone through nextWork
(throughReview) and partitionReadySetByDeclaredFiles. The result is waves with members, held
members and their reasons (files-overlap, write-set-unknown, after-unknown), depends: edges,
the lane bound, waitsForLane marks, built flags from each story’s real status, and one stated
assumption.
aof diagram plan <ref> loop
The command writes execution/loop-plan.json for a refined milestone under
work.loop.concurrency: refine_first and answers the diagram engine’s drawing instructions. It
refuses with loop-not-a-milestone, loop-not-refine-first or loop-not-refined, writing nothing.
aof diagram export <ref> loop
The command writes execution/loop.png from the drawn execution/loop.html through the generator’s
toSvg and the rasterizer, and keeps no SVG.
/aof:loop-diagram
A bundle command (claude, codex, opencode) that plans, stops on any refusal, draws loop.html in the
operator’s session through the engine’s skill, then exports. Built stories are drawn in the diagram
style guide’s green done role.
144 · The whole-tree test run signs off in minutes
The regression gate runs a declared whole-tree program
aof work regression-gate <ref> runs work.test.gate.args when it is declared, at scope all. This repo declares scripts/test-sharded.mjs. --jobs N expands work.test.gate.jobsArgs, and --serial runs the plain work.test program. Conflicting or invalid settings are refused before anything runs, and a malformed declaration is a red row naming work.test.gate.<key>.
A gate row says what was lost, what is not isolated, and how the run ran
The row’s detail is the failing cases, then not isolated: …, then <mode>[ --jobs N] · <min> min[ · over budget (<B> min)]. A green row carries a detail too, and still satisfies the door. After its verdict the gate prints the program’s # slowest files block and its Logs: path.
The sharded runner reports through one pure module
scripts/test-sharded-report.mjs owns the report and the exit decision. A case red twice, or a unit that ran fewer cases than it was assigned, is a column-0 not ok. A case red in the pool and green alone is # not isolated - <case> and does not fail the run unless --strict. The slowest-files block sums each file across its chunks into .tmp/test-timings.json.
143 · The loop starts from the backlog, refines a whole item in one pass, and runs each phase on the model the operator chose
One loop command from backlog idea to built work, on the operator’s models
aof work loop <backlog-slug> --refine whole-item --model refine=opus:xhigh --model verify=fable:high promotes the item, breaks it down and authors every contract in one refine session, then builds. Each phase’s session runs on its own model and effort, and each run’s declaration says which and from where. The parts are 143/00 (promotion), 143/01 (whole-item refine), 143/02 (the grammar) and 143/03 (the per-phase lend and record).
The loop’s run choices survive a resume together
The declaration a resume reads carries the scope’s origin (promotedFrom), the refine scope (refine) and the per-phase sessions (sessions). aof work loop <NN> --resume therefore continues a promoted item at its number, in the same refine mode, on the same flag choices, with no operator input.
143/00 · The loop promotes a backlog ref
A backlog slug is a loop scope
aof work loop <backlog-slug> resolves the scope with resolveItemExact. It promotes a backlog row through the registered work:promote alone (FF-14301), narrates Promoted <slug> → <NN>. and runs at the minted number. A promote refusal is the loop’s refusal, with nothing minted.
The declaration names where a promoted scope came from
Every run the loop mints carries promotedFrom: the slug, or null for a numeric scope. A declaration written without the key, by an older build or by a caller that does not pass it, reads back with promotedFrom: null.
The read-only doors never promote
--dry-run, an L1 report and a --json probe on a backlog slug answer wouldPromote and write nothing. --stop, --hand-off and --resume refuse a backlog slug with loop-backlog-ref-not-running.
143/01 · A whole-item refine in one drive
The refine scope is a loop setting with a per-run flag
work.loop.refine (per-story by default, or whole-item) lives in the loop’s bounds leaf as one frozen member list (FF-14302). aof work loop --refine overrides it per run. The resolved value is the declaration’s refine key, a resume inherits it, and every LoopState answer carries it.
Under whole-item the break-down drive is the whole cascade
For a milestone with no stories, the loop’s refine drive composes /aof:refine <ref> --solo --autonomous. --autonomous reaches the session through both drive seams (in-process and the child’s argv). A re-entered break-down refine keeps it, because the decision and the re-entry share the engine’s isWholeItemCascade. aof work drive refine <ref> --autonomous is the same drive from the CLI.
per-story is today’s loop
Under the default, every decision and every composed prompt is byte-identical to the pre-143 loop.
143/02 · One grammar for per-phase session choices
One grammar for --model and --thinking
parseSessionChoices in packages/execution/src/session-model.mjs reads --model [<phase>=][<model>][:<effort>] and --thinking [<phase>=]<effort>. It splits a model from its effort on the last : only when an effort spelling follows, so a Bedrock-style …-v1:0 id stays a model. An unknown phase, an unknown effort, an empty choice or two values for the same part of the same phase are refused (session-choice-unknown-phase, thinking-unknown-level, session-choice-empty, session-choice-conflict). A phased value beats an unphased one.
Each part of a phase’s session resolves on its own, and says where it came from
resolveSessionLaunch resolves the model and the effort separately: the flag, then work.agents.session, then the default (high, and no model). It reports modelSource beside effortSource. parseSessionChoices and resolveSessionLaunch are defined only in the session leaf (FF-14303).
A string flag can be given more than once
A CLI flag declared repeatable: true collects every occurrence into an array. An inline --flag=a=b value keeps everything after the first =, including in the two hand-rolled parsers in knowledge/src/memory.mjs and mesh/src/commands/session.mjs.
143/03 · The loop runs each phase on the chosen model
The loop takes a model and an effort per phase
aof work loop accepts a repeatable --model [<phase>=][<model>][:<effort>] and --thinking [<phase>=]<level>, parsed by 143/02’s grammar before anything is read or written. A refused choice stops the loop at its door with no declaration, no run record and no session.
The run record says what each phase ran on
The loop resolves all three phases once. Each run’s declaration carries sessions, one { model, modelSource, effort, effortSource } per phase, and every LoopState answer carries the table. thinking keeps 141’s meaning (the unphased level or null). Before the first drive the loop narrates Sessions: refine … ; continue … ; verify … ., and the dry-run probe carries the same table.
Each drive runs on its own phase’s choice
Each drive’s session gets its phase’s model and effort, read off the declaration by one engine function (sessionLendFor) at all three seams: in-process, the primary’s child, and a wave lane. A child gets them as --model / --thinking on its argv. aof work drive <phase> <ref> --model <choice> is the single-phase door.
A resume reruns on the recorded choices
aof work loop <ref> --resume re-applies the recorded flag choices and re-resolves the config and default entries. Any new session flag replaces the recorded flag set as a whole.
141 · Subagents and loops think at a chosen effort
One effort vocabulary
normalizeEffort() in src/session-model.mjs is the only reader of an effort level. It accepts low, medium, high, xhigh, max, reads extra-high as xhigh, and refuses anything else. Every door that takes a level calls it.
A driven session thinks at high unless told otherwise
resolveSessionLaunch answers effort + effortSource from --thinking, then work.agents.session.effort.<phase>, then high. Every spawned claude carries --effort <level> and none of the inherited effort variables. aof work drive <phase> --dry-run --json shows the effort it would launch at.
aof work loop –thinking reaches every drive
--thinking <level> on the loop overrides every phase for that run. It reaches sequential child drives, wave lanes and in-process drives, is narrated before the first drive, and is stored as the declaration’s tenth key, so --resume and a supervisor relaunch inherit it. An unknown level is refused as thinking-unknown-level before anything is written.
A role can pin its own effort
work.agents.effort (role → level) renders effort: after model: in .claude/agents/aof-*.md, for pinned roles only. An unpinned role carries no line and inherits its session’s effort. aof project validate checks the map the way it checks the model map.
An operator’s session in an aof project defaults to high
aof work update fills effortLevel: "high" into .claude/settings.json when the document has none. It never overwrites an existing value, and settings.claude.effortLevel in the project config still wins.
A phase command’s –thinking stops and names /effort
/aof:refine, /aof:continue and /aof:verify given --thinking <level> stop before any run or role. They print the /effort command to run and state that the session’s effort was not changed.
The run record shows the effort a session ran at
The spend ingest reads the record-level effort Claude Code writes on each assistant turn, so spend.effort names the level instead of "unknown".
140 · Refine runs solo unless told otherwise
A hand-run refine plays its roles inline unless configured otherwise
With work.agents.mode unset, /aof:refine resolves to solo and /aof:continue to orchestrated. A set key governs both commands, and --solo / --orchestrated override it for one run.
An orchestrated refine gives each story one QA agent
Under orchestrated mode, one aof-qa writes the Examples tables for all of a story’s tasks. The prompt forbids splitting that pass into one agent per task.
The loop composes a mode flag on every refine and continue it drives
LOOP_AGENT_MODE_DEFAULTS in src/loop-bounds.mjs is { refine: "solo", continue: "solo" }. aof work drive composes work.loop.agents.<phase>.mode when it is set and --solo when it is unset, and never reads work.agents.mode. verify still carries no flag.
The per-key loop resolvers still answer null for an unset key
The default is applied only at the phase level (loopAgentModeFromConfig), so the registry’s range probe and the tuner see an unchanged null.
This repository and the operator’s ten others follow the defaults
None of the 11 .aof/aof.config.json files pins a mode key. Each one’s rendered prompts state the new defaults, and a following aof work update --dry-run reads 0 updated and 0 drift-warning.
139 · Shatter lands its drivers in the backlog
Shatter frames into the backlog
aof:shatter writes every milestone and spike it frames un-numbered under backlog/[<group>/], each dependent carrying a backward slug depends: edge in PRD order. Under work.intake: "stream" (or no key), shatter then promotes each driver in PRD order through aof work promote, so every stream number is minted by the verb and none by the prompt.
Promotion enforces and resolves a backlog slug edge
aof work promote refuses promote-depends-backlog for an item whose depends: names a backlog slug. Promoting the target rewrites each entry naming its slug, in every other backlog row, to the minted ref: per entry, after the --at seam, and only for a slug unique in the backlog. The envelope reports the rewritten docs as rewired, a key present only when non-empty.
Validate checks backlog slug edges
aof work validate reports a slug entry on a backlog row that names no backlog item, adding the written-as number when the slug belongs to a stream item. It reports each backlog slug cycle once, at <work>/backlog, whatever the scope. Numeric entries on backlog rows stay unchecked.
One number/slug predicate
isDependNumber in src/work.mjs is the one all-digit test. The top-level shift, promote’s classifier, the shared per-entry rewriter rewriteRefEntry and validate’s numbered path all read entries through it, so a digit-led slug (10x-faster, 007-bond) is never shifted, resolved, graphed or zero-padded as a number.
138 · The session driver sees claude’s screen
A driven session’s screen is read, not inferred
When to type, what is blocking and what to record at a stop are all decided from one rendered screen per session (m138/00, m138/01). The three failures measured on 2026-09-24 were each observed closed on a real claude on two nodes (m138/02): a paste into a TUI that was not listening, a dialog idling to the 20-minute deadline, and a failure that left no screen.
Readiness holds on both of claude’s renderers
The input box is recognised on the fullscreen renderer’s alternate buffer and on the classic renderer’s normal buffer, so a drive launched after an unfinished fullscreen boot, or under /tui default, types on the box rather than timing out (ADR-002 §1 as amended at verify, m138/F-03).
An archived item’s citations resolve through the archive rule
aof work validate’s reads: check resolves a missing <work.dir>/<item>/… through <work.dir>/archive/<item>/…. An item created and archived inside one squash therefore no longer dangles on main (m138/F-16).
138/00 · The driver reads the screen
One screen model per driven session
Every interactive claude session the driver launches carries a @xterm/headless 6.0.0 model, fed from the session’s own PTY output and sized to it (80×24, scrollback: 0). src/terminal/screen.mjs is the only module that imports the package (FF-13801).
Typing waits for the input box
The directive is pasted on the first settled frame whose cursor row starts with ❯ at column 0 between two full-width ─ rules, on either buffer (ADR-002 §1 as amended at verify). If a model is live and no such frame arrives by the 60 s cap, nothing is typed, and the session stops failed / timeout with the screen recorded as screen-not-ready.
Every verdict the screen gives is acted on, by the door’s word alone
The driver acts on four verdicts from src/terminal/session-screen.mjs: type; consent (a menu walked one confirmed key at a time, then one Enter); blocked (failed / blocked_screen, with screen: { id } on the result); and wait (the heartbeat deadline suspended). The driver reads no screen itself.
Every stop but done leaves the screen
Every timeout, death, unaccepted directive and needs-input stop writes one degrade event carrying the rendered rows, the buffer and the cursor, taken before the kill. reportDegrade throttles per (code, key).
The byte gate is the fallback
With no model (the package absent, or failing to load), readiness is the pre-138 byte gate, verbatim: the floor, then bracketed paste ON with visible output since. screen-model-unavailable is logged once.
138/01 · Blocking screens are named
Six recorded screens, each with one action
src/terminal/claude-screens.mjs registers ready (type), trust (consent), mcp-approval, first-run and login (fail), and usage-limit (wait). Every entry is proved on every run against a recording committed under test/fixtures/claude-screens/, and ready claims no dialog (FF-13802).
Trust is answered by the operator’s standing consent, walked on the screen
claude 2.1.283 opens the trust dialog on ❯ No, exit. The door presses Down one confirmed step at a time until Yes, I trust this folder is highlighted, then sends one Enter. An arrow the screen does not show within 2 s gives the consent up by name.
A blocking screen is named in seconds, and the loop says which
A session on MCP approval, the first-run theme picker or the login menu stops failed / blocked_screen on the first settled frame that shows it, and is not retried. The loop’s lane narration reads settle: failed (blocked_screen: <id>), and its halt line carries screen=<id>.
138/02 · A live session proves it
The deployed driver is observed working on both nodes
On this node’s payload and the WSL node’s tree, a drive under an empty CLAUDE_CONFIG_DIR stops failed / blocked_screen with first-run within 2 s of launch. A loop over the same fixture halts run-not-retryable with screen=first-run. A real drive pastes its directive on the input box 4.7 s after launch, its session id is captured, and its cancel leaves the REPL frame in the degrade log, with no fallback code.
Every driven session declares the terminal the PTY is
Each interactive driven session launches with TERM=xterm-256color, the terminal the PTY and the screen model emulate, whatever the launching shell carries (m138/F-01).
The WSL deploy carries the lockfile, and a failed install is not stamped
scripts/deploy-wsl.sh syncs package-lock.json with package.json. A failed npm ci exits 1 and leaves .aof-wsl-deploy as it was (m138/F-02).
137 · AOF.md has a template like every other record doc
The AOF.md digest has a shipped template
src/bundle/templates/milestone/AOF.md declares the digest’s eleven frontmatter keys in order (source and importedAt marked # OMIT) and its four sections, Intent, Scope, Decisions and Lessons, and aof work update installs it at .aof/templates/work/milestone/AOF.md; no native scaffold writes an AOF.md.
Every imported digest is rendered from that template by one renderer
src/work/digest-template.mjs renders it for both the co-located write and the legacy-store path: keys in template order, a missing OMIT key dropped, sections in template order, a half that was not recovered left out, and schema/aofVersion stamped, so a fresh import never shows as stale.
Validate holds every digest to the template’s closed key and section sets
A digest record doc gets an error for a missing required key, an unknown key, an unknown section, a section out of order or a section that appears twice; the sets are parsed from the shipped template, not listed a second time.
136 · Discovery questions in the loop
A loop-driven refine settles its business rules with the operator, end to end
A refine that aof work loop drives asks each open business question as a tokened discovery question (m136/02), the loop posts it and waits in that lane while the session is recorded as waiting (m136/03), the operator’s answer through any channel 131 records is typed back and written into the map, and the example it settles reads stated or confirmed with the loop’s answer record as its anchor (m136/01) – measured live on the test-bed, where 08/00’s Q1 was asked, answered … inside max, anchored, and formulated into the contract.
Every driven ask reaches the operator on the current Claude Code
Not only discovery: any AskUserQuestion a driven session makes, at refine, build or verify, is detected and posted through the recorder hook (m136/03).
The whole-tree gate resolves 142’s moved and split modules
The rename ledger records PR #5’s 1,106 renames and 311 derived forwards, so a citation of a module 142 moved or split behind a forward resolves through history (FF-11903, FF-5204, reads:), and the unresolved-citation count holds at its ceiling of 55.
136/01 · A loop answer anchors the example
An answer given through the loop is a person’s answer to its map token
collectAnswers reads the asks entries of a story’s and its parent’s runs, in any state, beside the harness transcript: an answered entry whose question opens with exactly one map token yields a record with that token, the answer verbatim, the run’s session, answeredAt and the entry’s by, so the doctor lane and the build door accept a stated or confirmed example anchored by it.
An ask that cannot name its one token anchors none
An entry with no token at its head, a second token on a later line, or no answer yet yields no record.
136/02 · A driven refine asks through the loop
A driven refine asks each business question as its own tokened discovery question
In a session whose environment carries AOF_RUN_ID, refine.md’s discovery beat asks one question per AskUserQuestion call: its token first, the discovery marker, the rule and the example it settles, then 131’s four lines, with the options as the tool’s; a business question is never defaulted and never sent as the NEEDS_INPUT sentinel, and the answer is written into the map on resume.
A driven cascade asks one question after another
The --autonomous block asks a driven session’s open business questions one per call, each its own ask and wait; an interactive cascade still asks in batches of four.
136/03 · A pending ask is read from the hook
A driven session’s pending question is recorded before it is shown
The bundled PreToolUse hook claude-ask-pending, matched to AskUserQuestion alone, appends the call (runId, sessionId, toolUseId, input, at) to <item>/runs/.asks-pending.ndjson, prints nothing and exits 0 on every path.
The loop sees, posts and answers a question the transcript does not show
The driver settles needs-input on a record written during its drive whose call has no result in the transcript; the loop’s owner and the mesh worker read the question from it first; the re-drive types You asked:, the question, The answer: and the answer, while the run records the answer verbatim and a transcript question’s answer is still typed verbatim.
135 · Key examples in the contract
What a person agreed in discovery reaches the executable contract and stays there
With work.examples.enabled on and an applicable map, refine writes a story’s contract as one Rule: R<n> · … per map rule, with the key examples as headline Scenario: E<n> · … under it (m135/05). The parser reads the rules (m135/02), and the board shows them (m135/03). An agreed example that leaves the contract is an example-untraced error, and the build stays refused until the example is restored (m135/04). All of it ships from @aof/specification-by-example (m135/01).
135’s own contracts are the trace’s first real subjects
Stories 03, 04 and 05 are formulated in this form from their own maps, so the live work stream carries ruled contracts. 135/04’s stated E8 is traced at accept: delete its scenario and the doctor names it.
135/01 · The practice is its own package
Specification by example lives in @aof/specification-by-example
The example map grammar (map), the answer reader (answers), the examples doctor lane (doctor-lane), the continue door’s check (build-door) and the story probe (story-probe) are the package’s five exports. It depends on @aof/contracts and @aof/work alone, and core composes it into the work stream when work.examples.enabled is on.
@aof/work offers seams that never name the practice
createWorkDoctor takes a storyProbe, budgetRows and extensionGroups, and createPhaseDoorCommands takes a beforeBuild list of checks run at the continue door. No @aof/work source imports the package, lists it in its manifest or spells EXAMPLES.md (FF-13501).
135/02 · The parser reads Rule: as a group
The one Gherkin parser reports rules
parseFeature gives each scenario a rule (or none) and the feature a rules list. A rule’s tags apply to every scenario inside it and to no other, and a tag above a Rule: is listed once.
Example: is a scenario and an Examples row keeps its cells
Example: parses as a scenario, and each Examples table carries its columns and its rows’ cells. A feature with no Rule: parses to the same value under the old keys, across the whole corpus (FF-5704).
135/03 · The board groups scenarios by rule
The tasks projection carries each scenario’s rule title
aof work tasks and /api/work/tasks give each scenario the keys name, outline, lane and rule, where rule is the title of the Rule: it sits under or null, on the local and the cached path alike. Only the title crosses the wire.
The task card groups scenarios under their rule
The board’s TASKS tab (apps/ui/src/board/TasksTab.tsx) shows a task’s scenarios in file order, with the scenarios outside any rule first, as today’s list, then a heading over each rule’s indented list. A task with no rule, or a payload from an older node without the rule key, renders today’s markup.
135/04 · An agreed example cannot fall out
Every agreed example is traced to the contract by its id
The examples lane’s sixth code, example-untraced, is an error naming the story, the example, its line in EXAMPLES.md, its provenance and its rule. It fires for each confirmed or stated example that no scenario titled E<n> · … and no Examples row whose example cell is E<n> carries inside a group naming its rule. It says so when the carrier sits under a different rule. A proposed example is never required.
The continue door refuses a story missing an agreed example
aof work continue on such a story is refused with examples-question-open (409), its detail listing the same example-untraced findings the doctor reports, and nothing moves or dispatches until the example is restored.
135/05 · The contract is formulated from the map
Refine’s Contract stage formulates from an applicable map
refine.md’s story Formulation has the PO write one Rule: R<n> · … per map rule and one headline Scenario: E<n> · … per key example. QA’s outlines sit inside the rule they test, and a row restating an example carries its id in an example column. One feature per rule is the named fallback for a runner that cannot bind Rule:. With the gate off or the map not applicable, formulation reads as before.
The briefs and the guide carry the level above the matrix
The product-owner and QA briefs each carry their half of map-driven formulation. wiki/acceptance-criteria.md names the key examples as the headline above its three zoom levels, with one Rule: specimen, and every rendered copy matches a fresh render.
134 · Discovery before formulation
Discovery runs before formulation in this repository
work.examples.enabled is on here, so a story refined from now on gets an example map before any .feature, and its Contract and build stay blocked while a business question has no recorded answer; 144 is the first such story, held at its gate on one open question (m134/02, m134/04, m134/05).
A person’s answer is evidence the agent cannot write
Only an answer the harness recorded on the story’s token, stamped onto a run record at settle, can make an example confirmed or stated. The doctor refuses a label with no stamp behind it (m134/03, m134/04).
The before-number exists
At least 5 misunderstood-requirement findings and 1 amendment round over 21 stories on 124, 126, 127 and 133 is the baseline any later measurement compares against (m134/01).
134/01 · The baseline is counted
The before-number for discovery
134’s RESEARCH.md ## R7 counts misunderstood-requirement findings and amendment rounds per story on 124, 126, 127 and 133, by a stated rule with repeatable commands: at least 5 findings and 1 round across 21 stories (0.24 and 0.05 per story), read as floors.
134/02 · The map is a document
The example map’s closed grammar
A story’s EXAMPLES.md is parsed by one pure module, packages/work/src/examples/map.mjs, into rules, examples with one provenance each (proposed, confirmed, stated Q<n>), and questions with a class and a state. Any line outside the grammar is a coded malformed entry, never a silent skip.
One home for the map’s queries and its token
Open business questions, unanchored claims and the <ref> Q<n> / <ref> E<n> token are read through that module alone (FF-13402).
The work.examples.enabled gate
Discovery is off unless a project sets the boolean true. Absent, false or any other value is off, and an unknown work.examples key is examples-gate-unknown-key.
134/03 · The answer is read from the harness
One reader of a person’s answer
packages/work/src/examples/answers.mjs is the only module that reads the harness’s AskUserQuestion result (toolUseResult.answers) from a session transcript, and it keeps only answers whose question opens with a map token (FF-13401).
The answer is stamped onto the run record at settle
A settle writes each tokened answer to brief.answers as { token, question, answer, toolUseId, sessionId, at, entrypoint }; a refused or untokened question writes nothing, and collectAnswers(ref) reads every stamped record for a story.
Settle reads the real transcript store
A hand-run aof work run-complete resolves the transcript directory through claudeProjectsDir for a named workspace instead of the repository root, so it stamps spend and answers from any working directory (FF-13404).
134/04 · The readiness gate
The examples doctor lane
With work.examples.enabled on, aof work doctor reports a story’s map through five codes: example-question-open, example-provenance-unanchored and example-map-malformed (errors while the story is open, warnings once done), and example-rule-no-example and example-map-too-many-rules (warnings).
The map’s budget row
EXAMPLES.md has its own examples budget kind, 50 lines by default, governed by the doc-budget lane like PLAN.md.
The continue door
aof work continue on a story refuses with examples-question-open while any error-severity example finding stands, naming the findings, before the build starts.
Off is today
With the gate off, the doctor reports no example finding, no snapshot row carries a map, and the door refuses nothing (FF-13403).
134/05 · The discovery beat
The discovery beat at the head of a story’s Contract
With work.examples.enabled on, aof:refine drafts a story’s EXAMPLES.md from the installed template before any .feature. It strikes every question the record answers, asks each remaining business question through AskUserQuestion with its <ref> Q<n> token and its context, and stops the Contract stage on any error-severity example finding.
Briefs that propose and never label
The PO brief drafts the map and never writes confirmed or stated without a recorded answer on that token. The architect brief reviews every technical question and relabels one that is policy as business.
--autonomous asks at its one stop
A business question never takes a default in the cascade. Every open one is asked at the single end review, and a story whose question goes unanswered stays at its Contract gate.
The EXAMPLES.md template
.aof/templates/work/story/EXAMPLES.md is a legal map with no frontmatter (36 lines), carrying the Not applicable: line for a story with no rule a person owns.
133 · Architecture diagrams
An ADR’s diagram runs end to end, from config to the console
In one repository, an opt-in in work.diagrams is enough for an architect to plan, draw, export, gate and show a diagram under its ADR. Each stage is a separate surface: m133/01 (config, layout, plan), m133/02 (export), m133/03 (the doctor lane), m133/04 (the ARCHITECTURE tab, viewer and file route) and m133/05 (the prose). m133/06 proves the chain on ADR-002.
The generator is swappable at one seam
Replacing diagram-design means one new adapter file plus one config value. The architect prose, the file layout, the doctor lane and the console name no generator, and the adapter reserves a readBack direction for editing a diagram back into a superseding ADR.
133/01 · The seam is named in config
The diagram generator is a config choice
work.diagrams ({ generator, formats, style }) is read by one resolver, resolveWorkDiagrams. An absent key is off, a bad shape is one coded diagrams-* error that still resolves to off, and an unknown generator is refused by naming the registered ids.
One layout home for a diagram’s place and spelling
src/diagrams/layout.mjs alone owns the diagrams/ folder, the ADR-NNN-<slug> stem, the brief reader, the link block writer and the link parser. The parser skips fenced code and inline code spans.
One generator adapter behind one registry
generatorFor(id) resolves diagram-design, the only adapter, which carries locate, instructions, toSvg and a reserved readBack: null. The literal diagram-design appears in src/** only in that adapter (FF-13301).
aof diagram plan
aof diagram plan <ref> <ADR-NNN> --slug <slug> answers off, generator-missing, or the plan (stem, paths, brief, instructions) from config alone. It exits 0 on all three and writes nothing.
133/02 · Export writes the SVG and the PNG
aof diagram export
aof diagram export <ref> <ADR-NNN> writes the adapter’s SVG, then a PNG, beside the drawn source, and returns the link block ready to paste. A PNG miss keeps the SVG, exits non-zero and names what is missing.
A browser ladder that downloads nothing
The PNG comes from the first browser the ladder finds: the configured browser, then the cached ms-playwright headless shell or Chromium (both cache layouts), then an installed Chrome or Edge. The browser argv is formed in one function, and no Playwright, Python or npx is involved (FF-13303).
Export waits for the file
The rasterizer counts a render as done only when the process has exited AND a fresh, size-stable PNG exists. A launcher that returns before writing (Edge) is waited on, not trusted.
133/03 · The gates know about diagrams
A doctor lane for diagrams
aof work doctor runs a pure diagramsGroup lane that reports four things: a linked diagram file missing from the tree, an export owed by formats but absent, a link whose stem names a different ADR than the one it sits under, and an unlinked file in diagrams/ (an orphan). The link codes are errors while the item is open and warnings once it is done. An orphan is always a warning.
The lane is silent where there are no diagrams
An item with no diagrams/ folder and no diagram link reports nothing. This repository’s whole stream carries no diagram-* finding.
133/04 · The console shows it
Diagrams ride the artifact manifest
DIAGRAMS (diagrams/*.svg, one level) is the manifest’s last entry. aof work doc <ref> DIAGRAMS <member> answers a diagram, the board’s /api/work/doc forwards member, and a worker streams the SVG and never the source or the PNG (FF-13305).
A milestone ARCHITECTURE tab
The board’s milestone detail has an ARCHITECTURE tab, second after SPEC, that renders ARCHITECTURE.md. Each diagrams/<stem>.svg image appears inline where its ADR links it, as a loading, populated, missing or error figure. A diagram body reaches the page only as an encoded data:image/svg+xml image (FF-13304).
A full-size diagram viewer
Clicking a populated figure presents a full-size viewer over the same image, as the shell’s fullscreen occupant. It opens fit to the screen, toggles to twice the viewBox width with scrolling, and exits on Esc or the shell’s ✕ Exit fullscreen.
The block’s links open the committed files
The pasted block’s Source · PNG links, and the viewer’s Open in new tab, open the committed .html, .png or .svg from this node’s checkout through aof diagram file at /api/diagram/file. Every answer is served under Content-Security-Policy: sandbox.
133/05 · The architect draws
One diagram step in the architect’s ADR rule and refine’s Decide stage
aof-architect and aof:refine each carry one passage, inside the ADR authoring they already describe. The architect judges whether an ADR needs a diagram and writes its ### Diagram brief, then runs aof diagram plan and follows the answer (off, missing, or the instructions), then aof diagram export and pastes the block. In solo mode the main session runs the step itself.
The prose never names the generator
No bundle document names diagram-design. Every rendered copy (.claude, .codex, .opencode) matches a fresh render, and the architect’s tools are not widened to draw.
133/06 · The live draw
This repository draws diagrams, in the console’s style
.aof/aof.config.json sets work.diagrams = { generator: "diagram-design", formats: ["svg","png"], style: ".aof/diagrams/style.md" }. The style file maps ui/src/index.css’s tokens and fonts into the plugin’s style-guide structure, so a driven session never pauses for onboarding.
ADR-002 carries its own diagram
133’s ADR-002 links diagrams/ADR-002-generator-seam.{html,svg,png}, drawn and exported through the seam it describes. The operator accepted the drawing against its brief, its style and its PNG.
132 · Run records carry the node id, not the node name
Opaque derived node ids
A derived node id is node-<installHash(salt)> whatever the machine is called; the hostname is recorded as derivedFrom and never spelled into the id, and a pinned id still wins verbatim.
- Two installs never share a salt – the salt is a per-install
randomUUID(), and a collision widens the same hash rather than reaching for the hostname.
Legacy ids are recognised, and moved only on purpose
The load-time self-heal leaves a hostname-stem id untouched; aof mesh identity --reidentify is the explicit move to the opaque form and refuses a pinned id.
Re-identification says what it strands
Both --reidentify and a --name that moves an existing id answer { from, to, changed, invalidated, record } from one shared scan. The scan names the old id’s node record, its enrollment credential (config and registry roster) and its control-node nomination, and neither verb repairs any of them.
- The operator re-points what is reported – a control node whose
mesh.relay.controlNodestill names its old id readsisControlNode: falseat its next start, and the desktop supervisor then starts no:4182.
The fabric joins on the declared machine name
A node record carries hostname (the machine name, kept in the aof home) beside host (the dial address), and resolvePeers joins fabric peers on it rather than on the id’s spelling. The registry sync preserves it, and both fleet faces title a node with it (macOS .local dropped), with the opaque id beside it.
Tracked run records name no machine
test/arch/mesh/acd-run-records-name-no-machine.test.mjs passes only when every tracked runs/<node>/ segment and every record’s node key is opaque or baselined. The baseline holds umamis-mac-mini → 5 alone and may only shrink.
This control node runs as node-7297
The Windows control node’s id is node-7297, and the tree’s former placeholder segment (184 files) now sits under runs/node-7297/.
131 · The human in the loop
A question no longer stops the loop
A driven session that needs a human records its question, waits in place while every other lane keeps building, and resumes the same session with the answer; only an unanswered bound parks it, and a park is announced, not a halt (m131/03, m131/07).
One question, every face, every answer path
The same question reaches the terminal, the board card and aof’s own Discord bot with its phase and cost, and is answered from aof work answer, the board’s reply box or a Discord reply from an allowlisted user, each recorded verbatim with who and when (m131/01, m131/02, m131/04, m131/05, m131/09, m131/10).
Discord as an operator console for the whole mesh
One bot on the control node posts every notification with its project, carries a worker’s ask, answers /status, /asks, /loop stop and /loop resume for allowlisted users, and serves any number of projects from one channel (m131/11, m131/12, m131/14); aof messaging sets it up and tests it from the CLI (m131/08, m131/13).
131/01 · The question is read and recorded
One reader of a session’s question
readLastAssistantTurn, askQuestionFromTurn and readAskQuestion in src/work/observe.mjs are the only transcript scan that yields the question: the last assistant turn, or a pending AskUserQuestion’s questions and options. The driver’s private scan maps over them, and its seventeen exports are unchanged.
A four-line form of the ask
NEEDS_INPUT_INSTRUCTION asks a driven session to print Decision needed:, Options:, I would pick: and What the answer changes: before the sentinel. The threshold sentences for asking are byte-identical to before.
One ask file per run in the aof home
src/loop/ask-request.mjs owns <meshRoot>/loop-asks/<runId>.json: a fifteen-key record, ASK_STATES (waiting | parked | answered), the owner’s openAsk/parkAsk/clearAsk, the reads readAsk/readAsks, and createAskPoll. No other src/ module spells loop-asks (FF-13101).
An answer sanitised once
answerAsk is the only place an answer is checked. It refuses a blank answer, one over 8,000 code points, or one holding a C0 control other than TAB/LF/CR or DEL. Otherwise it stores the answer verbatim, and the first answer wins (ask-already-answered).
asks as the run record’s seventeenth key
Every run record carries asks last, defaulting to []. Each entry holds seven keys (question, phase, askedAt, parkedAt, answer, answeredAt, by). It is written only by openRunAsk, parkRunAsk and answerRunAsk in src/run-store.mjs, and a retry starts a fresh [].
A waiting run is neither reclaimed nor charged
The stale-reclaim scan skips a run whose last ask is unanswered (FF-13103), and attemptElapsedMs subtracts the clipped, merged ask intervals.
131/02 · The notifier and its channels
A work.notify config surface that never holds the secret
work.notify is a closed schema. Channels are named by type, and urlEnv defaults to AOF_DISCORD_WEBHOOK_URL. It also takes an events filter and a link. There is no url, webhook or token key, and resolveNotifyConfig is its one resolver. An absent block is a no-op with zero network calls.
One envelope, built in one place
buildNotifyEnvelope in src/notify/notify.mjs builds the eleven-key envelope (event, ref, at, node, phase, elapsedMs, question | stop | outcome, answerPath, link) for the closed EVENTS set. stop carries the halting item’s ref.
One headline formatter every face shares
src/notify/form.mjs (zero imports, typed by form.d.mts) is the only home of formatElapsed, oneLineAsk, eventPhrase, headline, cost and accountLine. The terminal, the board and Discord all read from it (FF-13108).
A Discord webhook renderer and a channel registry
CHANNELS maps a type to its renderer and sender. renderDiscord posts plain content with allowed_mentions: { parse: [] }, at most 2,000 characters. Only the body gives way under the cap, the headline, answer command and link are kept, and code fences are closed before the suffix.
Best-effort delivery that never fails a run
notify is awaited and sends to each channel in parallel, each bounded at 5 s and never retried. It never throws. A failure degrades by one of three named codes, and no message, log or degrade carries the URL (FF-13106, FF-13107).
An accepted milestone is announced
aof work status <milestone> done fires milestone-accepted once, after the move, with the title from the resolved row. A delivery failure never fails the accept.
131/03 · The session waits and the loop keeps going
One composer at every needs-input site
Every NEEDS_INPUT goes through awaitAnswer in src/loop/ask.mjs: the wave’s lanes, the shell’s primary drive, the retry ladder and the verify branch. It reads the question, records it on the run, writes the ask file, notifies session-needs-input and narrates NN/SS – waiting on you (<phase>, <elapsed>): <question>, in that order.
A waiting run stays alive on a ref’d wait
While a run waits, its record stays running with a live heartbeat, the process is held open by a ref’d setTimeout per check, and the wait is bounded by scheduleToCloseMs counted from the ask.
The answer resumes the same session
An answer re-drives the ask’s own session through work:drive --answer, and spawnLaneDrive({ answerFile }) does the same for a lane. The answer is typed verbatim as the session’s first input. A foreign or unreadable answer is refused before any mint or spawn (FF-13104).
A waiting lane does not halt the loop
Under refine_first a waiting lane keeps its slot while the other lanes build and merge. At the bound it is parked (committed, not merged), set aside and notified session-parked-unanswered. session-needs-input is minted only by parkedHalt, and only when nothing else can run (FF-13105).
--resume re-enters a standing ask
aof work loop <scope> --resume re-drives an answered ask and re-waits a waiting one with a fresh bound and no second notice. It also clears stale ask files. Ownership is read from the last ask’s parkedAt.
The loop reports its own halt and death
loop-halted fires once after the account, but never for session-needs-input, whose ask already went out. loop-died and loop-relaunched fire on --resume from the last loop-diag event (readLastLoopDiagEvent). A halt carrying parked lanes prints each ask block.
131/04 · The answer reaches the session
One verb answers a waiting ask
aof work answer <ref> "<text>" [--as <actor>] [--json] (work:answer, in src/commands/resume.mjs) answers the latest waiting or parked ask on the item. It writes through the ask file for a local lane or primary drive. It records the answer verbatim with by: { actor, via, node } and answeredAt, and fires session-answered, whose elapsedMs is the wait. --as is at most 80 code points, with no control characters.
A mesh worker’s ask is answerable through mesh:terminal-resume
For a worker’s needs-input assignment, the verb sends answer: { text, by, askedAt } on mesh:terminal-resume through the control router. The worker types it into the resumed session, and park-resume.mjs records it. A worker-refused resume answers terminal-resume-not-started (409).
The board answers through the same verb
POST /api/work/answer runs work:answer in-process. Every board write route passes one hoisted admitWriteRequest before its body is read (FF-13109). A non-object body answers invalid-body (400) on every write route, and a non-POST to a write path answers 405.
One loopback predicate guards both write faces
isLoopbackHost in src/static-serve.mjs refuses a write whose Host is not loopback with non-loopback-host (403), on the board and on the fleet alike.
The resume sweep names a run waiting on an answer
work:resume’s sweep renders a waiting run as a NEEDS YOUR ANSWER row carrying its answer command.
131/05 · The board shows the question and takes the answer
The ask fact on a list row
With mesh: true, work:list applies applyAskOverlay outermost and sets a thirteen-key ask fact (with scope last) on a row whose item has a waiting, parked or answered ask. A local lane’s fact is read from its ask file. A mesh worker’s comes from its needs-input assignment, through awaitsAnswer in src/board-mesh-execution.mjs, the same predicate the answer verb reads. The CLI’s --json is byte-identical.
An ask card in the detail panel
ui/src/board/AskCard.tsx shows WAITING ON YOU, the phase and elapsed wait, the question as plain text (never Markdown, clamped at six lines with a toggle), and a free-text reply box. It has one Send answer button, disabled while the box is empty, and no default or “continue” button. It also shows the answered, parked and refused states, and a worker’s ask as “question unreadable”. It is keyed per ask, and DetailPanel.tsx gains only its import and mount.
One pure decision and one client
askCardState in ui/src/board/action.mjs decides everything the card shows. The header relabels to Open terminal – <node> for a worker’s ask. workApi.answer in api.ts is the card’s only fetch, and it posts { ref, text, actor } to POST /api/work/answer.
A shown card tracks its answer
While any row carries an ask, the board’s silent 5 s list poll is armed. The card’s receipt holds until the ask leaves the wire.
The ui/ freeze moved with its reason
53/FF-5307’s ui/ digest is lifted into assertUiFrozen over (path, content) pairs. It is re-pinned with 05’s measured diff, and a one-character edit to any ui/src file turns it red.
131/06 · The register
Nine resolved controls over the human-in-the-loop seams
FF-13101…FF-13109 are arch-tests in three files under test/arch/loop/: acd-loop-ask-single-home, acd-loop-ask-waits-in-place and acd-loop-ask-reaches-every-face. They are registered by import and spread in test/arch/loop/index.mjs, and the test/arch/loop budget row is 65. ARCHITECTURE.md carries no pending – 131/06 marker, and aof work doctor 131 reports no control-unresolved.
Every control has been seen red
Each control’s named mutation reds exactly that control, and each is recorded in VERIFICATION.md’s register. The non-vacuity needles of FF-13101 and FF-13107 red when misspelled.
FF-13106 reds upstream of the redaction backstop
FF-13106 carries a structural degrade-message leg, so a URL placed in a degrade message reds even though degrade()’s redaction would strip it from the fixture’s output.
131/07 · The live run
The human in the loop, measured on a running system
A real refine_first loop on this machine asked the operator build-time questions that reached T1 and the Discord bot within the minute, kept its other lanes building while they stood, took answers from a Discord reply, the CLI and the board, each resuming the session that asked, resumed a parked session with its answer, and accepted its milestone; a supervised loop was stopped and handed back from Discord with /loop stop and /loop resume, the supervisor relaunching it.
131/08 · The messaging CLI
The aof messaging command family
aof messaging init|enable|disable|status <type> are four routed commands from one module,
src/commands/messaging/messaging.mjs, and aof --help lists them under Messaging:.
discord is the only channel type accepted. Any other type is refused messaging-unknown-channel.
A machine-wide, owner-only webhook store
A Discord webhook URL is kept in one file, <global home>/messaging/discord.secret, written
atomically. On POSIX the file is 0600 and its directory 0700. src/notify/secret.mjs is the
only module that knows its path. FF-13106’s store-path leg holds that.
A URL that never passes through argv or output
init discord reads the URL only from a hidden password prompt on a TTY, or from the first line
of stdin. A positional after the type is refused messaging-secret-in-argv. The URL is validated
against Discord’s four webhook hosts over https. No output, JSON answer or degrade message of any
verb contains it.
A per-project switch that writes only work.notify
enable discord adds work.notify.channels.discord = { type: "discord" }, and disable discord
removes every channel of that type. Neither changes any key outside work.notify, and neither ever
writes a url, webhook or token key. Both are idempotent, and both are refused
messaging-no-project outside a project.
A status that reports presence, never value
messaging status reports three facts per type: stored on this machine, env override set, and
enabled in this project. --json answers the same facts. It never reads the stored value.
A send that reads the store every time, with no restart
notify resolves a channel’s URL on every send. A non-blank env[urlEnv] wins. Otherwise it reads
the store. Otherwise it degrades notify-channel-unconfigured, naming both remedies. A running
daemon picks up an init without a restart.
131/09 · The bot posts
A Discord bot token as the one messaging credential
aof messaging init discord stores a bot token (hidden prompt or stdin, never argv) in 08’s owner-only discord.secret; isDiscordBotToken is the shape check, a webhook URL is refused naming the guide, and no webhook sender or URL shape remains in src/.
An offline invite URL
init discord prints https://discord.com/oauth2/authorize?client_id=<id>&scope=bot+applications.commands&permissions=2147552320, with <id> decoded from the token’s first segment and no network call.
A channel named by id in work.notify
A discord channel is { type, channelId, tokenEnv?, events?, allow? } in the closed schema (no url, webhook, token or urlEnv); enable discord --channel <id> writes it, refuses messaging-channel-id-required without the flag, and completes a channel 08 left with no id in place.
One authorised door to the Discord API
discordRequest in src/notify/discord.mjs is the only src/** code that builds Authorization: Bot … or names the API host (FF-13110); sendDiscord posts to /channels/{id}/messages after validating channelId as a snowflake, is bounded, never throws or retries, and answers the posted messageId.
Every notification posted by the bot
notify resolves the token per send (env[tokenEnv], then the store), posts through sendDiscord, and answers { delivered, failed, messages } with one { channel, channelId, messageId } per delivered channel; a 2xx whose body read outruns the bound counts as delivered with no id; a channel with no channelId degrades notify-channel-unconfigured naming the enable command.
A render that is the bot’s own message
renderDiscord answers { content, allowed_mentions: { parse: [] } } with no username, and DESIGN §3’s lines, cap and clip are unchanged.
A status that reports the bot, never its token
messaging status reports the stored token’s presence, the AOF_DISCORD_BOT_TOKEN override, and each project channel’s channelId (--json: project.channelIds, null for a channel with no id).
A bot setup guide
wiki/architecture/discord-notifications.md walks the Developer Portal, the Message Content intent, the invite, the permissions and the channel id, and every quoted CLI output in it matches the source CLI.
131/10 · Answer by replying in Discord
The bot runs on the control node
startLauncher starts src/discord/bot.mjs on the control node only, when a token resolves, and stops it with the daemon; with no token it logs discord-bot-off once and posting is unaffected; a worker never connects.
A gateway connection that resumes
src/discord/gateway.mjs is the only module that opens the gateway socket (FF-13111): it IDENTIFYs with intents 33280, heartbeats and closes a zombie, RESUMEs on every reconnect, re-identifies only on an invalid session or 4007/4009, guards the IDENTIFY budget, stops on 4004/4010–4014 with a named degrade, backs off 1 s → 60 s with jitter, and never throws into the daemon.
A machine-wide index from a posted ask to its ask
src/notify/ask-messages.mjs records { messageId, channelId, event, ref, workspaceId, projectRoot, postedAt } under <messaging store>/discord-asks/ when session-needs-input or session-parked-unanswered is delivered to a discord channel, atomically, pruning past 30 days, and a failure degrades notify-ask-index without failing the send.
An allowlisted reply answers the waiting session
src/discord/replies.mjs turns a reply to an indexed ask message from a user in that channel’s allow into invoke("work:answer", { ref, text, as: "@<username>", via: "discord" }), adds one ✅ reaction on success, replies with one coded line on a refusal, ignores bots and unindexed or foreign-channel replies silently, and never writes an ask file or run record (FF-13112).
An allowlist that defaults to nobody
work.notify.channels.<name>.allow is a unique list of user snowflakes, absent by default, so a reply is refused discord-answer-not-allowed naming the key until the operator lists someone; messaging status reports each channel’s allowlist size, never an id.
by.via: "discord" on the record
work:answer accepts via: "discord" from the in-process caller (no CLI flag), and the answer’s sanitation, first-answer-wins and session-answered are 04’s, unchanged.
An ask message that says it can be answered here
When the channel’s allow is non-empty, the needs-input action line reads Answer: reply to this message, or `aof work answer <ref> "…"`.
131/11 · Slash commands
Four guild commands registered by the bot
At every READY and hourly after it, the bot bulk-overwrites /status, /asks, /loop stop <scope> and /loop resume <scope> (each with an optional workspace) on the guild of every configured discord channelId, from one frozen table in src/discord/commands.mjs.
Every interaction deferred before its dispatch
Each interaction gets its type: 5 callback before any config read or dispatch (FF-13113) – ephemeral for /status and /asks, in-channel for /loop – and its final text is one @original PATCH capped at 2,000 characters.
Commands reach only allowlisted workspaces
An interaction counts only the served workspaces whose discord channel is the interaction’s channel and whose allow holds the user; with none it is refused discord-command-not-allowed; /loop with several and no matching workspace is refused discord-scope-ambiguous, listing them; a DM is refused.
/status and /asks read work:list
/status renders each in-progress row (ref, execution state, node, and waiting on you (<phase>, <elapsed>) for an ask) through form.mjs, and /asks lists waiting and parked asks as account lines with a jump link to the ask’s message when the index has one.
/loop stop is 130’s stop verb
/loop stop dispatches work:loop { scope, stop: true } in-process and reports the level reached (drain, then cancel) or the refusal as /loop stop <scope> was refused (<code>): <message>.
/loop resume hands the loop to the supervisor
work:loop --hand-off (handOff) writes a durable resume request, <meshRoot>/loop-resumes/<loopRunId>.json in src/loop/stop-request.mjs, which decideSupervisedDeclarations turns into a relaunch row even over an honoured stop mark, and the --resume launch clears; it refuses loop-hand-off-not-supervised, -running, -no-declaration, -not-local and -scope, is exclusive with --stop and --dry-run, and starts no process.
src/discord/** never spawns or reaches beyond two verbs
The discord family imports no node:child_process, and every invoke( in commands.mjs names work:list or work:loop (FF-13113).
131/12 · A worker’s ask reaches Discord
A worker’s park carries its question
A mesh worker’s running + needs-input park fact (assignment.reported) carries ask: { question, phase, askedAt }, read by readWorkerAsk over the worker’s own transcript through ADR-002’s one reader and clipped at 8,000 code points; every other report is byte-identical, and the journal-unavailable fallback carries no ask.
The ask kept on the control’s assignment row
global_assignments has a nullable ask column (store version 10, added by the idempotent ALTER); a park writes it, an absent ask never clears it, and the execution projection carries it only while the row awaits an answer.
A worker’s ask posted once, by the control
The settle-assignment reactor posts session-needs-input with the WORKER’s node only on the edge into needs-input (FF-13114), so a redelivered park posts nothing; with no control-side checkout it degrades worker-ask-unannounced.
The board and a reply read the worker’s question
applyAskOverlay shows a worker ask’s question, phase and askedAt from the row, and a Discord reply to the posted ask answers it through work:answer’s mesh leg (mesh:terminal-resume), which now also posts one session-answered carrying the worker’s phase.
131/13 · Test and allow from the CLI
aof messaging test <type>
The fifth aof messaging verb posts one test message, **aof – test message** · <project>, to each channel of the type in the project, through the notifier’s own pre-send checks (readyChannel, shared with deliver) and sender. It answers each message id, degrades nothing, and fails messaging-test-failed naming the fix for a 401, 403 or 404, or messaging-not-enabled with no channel of that type.
enable <type> --allow <user-id>[,…]
aof messaging enable discord --channel <id> --allow adds user ids to the channel’s allow, de-duplicated and in order. It never removes one, a repeat changes nothing, and any id that is not 17 to 20 digits is refused messaging-allow-invalid before any write. Without --allow, enable is byte-identical to 09’s.
The project on every Discord message
Line 1 of every Discord message reads **<headline>** <cost> · <project> · <node>, where the project is the config’s name, else the project folder, else absent. It is a render option, so the eleven envelope keys and the headline prefix the terminal and board share are unchanged.
131/14 · One channel, several projects
/loop finds its project by the loop the scope names
With several projects on one channel and no workspace:, /loop stop and /loop resume run in the one project whose scope carries a loop declaration (hasLoopOn, src/loop/stop.mjs); none is refused discord-loop-not-found, and a real tie is refused discord-scope-ambiguous naming only the tied projects.
A dispatch lane is never served as a project
The bot’s served list folds every member through foldDispatchWorktree, so a workspace whose presence recorded a lane’s worktree is served as its primary checkout.
130 · Stop a running loop
A running loop can be stopped from outside its own console
A live aof work loop on this machine is stopped from any other terminal, from the fleet node card, or from the desktop window, and every one of those routes writes the same one request that the loop reads. This holds on Windows, where no process outside the loop’s console can signal it. Observed end to end on the installed payload (m130/06).
One request, three faces, one verb core
The verb (m130/02), the fleet route (m130/03) and the desktop row (m130/04) each resolve a stop through stopLoop, and write the one request file m130/01 owns. No face holds a second copy of the resolution, the ladder or the vocabulary. FF-13001 and FF-13003 were each observed red when a second home was planted.
A stop never leaves the run it stopped in flight
The interrupt path that dropped 129/04’s driver observation now settles every stopped drive before the loop halts. A cancel settles cancelled with failureReason: null and a drain settles as the drive ended, so a stopped loop leaves no leaked running row to wall the next mint (m130/02, FF-13002).
A stopped loop stays stopped until the operator resumes it
The desktop’s reconcile does not relaunch a loop whose request was honoured, whether the desktop started it or attached to it. aof work loop <scope> --resume is the one door back, and it clears the request (m130/04, 130/ADR-007).
The frozen surfaces did not move for the stop
The board shows no loop, and src/board-ui.mjs and ui/src/board/ are unchanged. LoopState keeps ten keys, brief.loop nine, and LOOP_STOPS its fifteen members, with operator-interrupt the stop. src/run-store.mjs moved once, and only for 130/06’s retry carry, re-pinned under ADR-007 §5.
130/01 · The stop request has one home
A durable stop request per loop, in the aof home
A loop’s stop request is one ten-key JSON file at <meshRoot>/loop-stops/<loopRunId>.json (honouring AOF_GLOBAL_HOME, never under a checkout), written through writeText’s temp-and-rename, and read absence-tolerantly – a corrupt file reads null after a degrade report.
The two-rung ladder and its lifecycle
requestLoopStop creates a request at level 1 (drain), escalates it once to level 2 (cancel) stamping escalatedAt, is idempotent at 2 and leaves an honoured request unchanged; markStopHonoured and clearStopRequest close requested → honoured → cleared, with STOP_LEVELS and STOP_STATES the one map from level and state to word.
One interrupt source for the running loop
createStopSource composes the process’s own SIGINT/SIGTERM with the file on an unref’d poll: either raises the level 1 → 2, producer() names whichever raised it first, one AbortSignal aborts at level 2, and a third signal reaches node’s default because the source’s listeners are then removed.
src/loop/stop-request.mjs is the only speller
The literal loop-stops and the state words requested/honoured are spelled in this module and nowhere else under src/ (FF-13001, red-probed).
130/02 · The verb and the shell honour it
aof work loop <scope> --stop
--stop is a flag of work:loop that runs no foreground body: it resolves the scope’s latest declaration through src/loop/stop.mjs’s stopLoop, writes or escalates the request, and answers <scope> – stop requested (drain|cancel) for loop <loopRunId>, live|not live. <path>; --stop --resume is refused loop-stop-exclusive, a scope with no declaration loop-stop-no-declaration, and a declaration whose latest run names another node loop-stop-not-local.
One verb core for every face
stopLoop is the one resolution of a stop, imported by the command and by the fleet route alone (FF-13003); a loop found not live has its request marked honoured at once, so the next tick halts without a drive.
The running loop reads the source, not a flag
runLoopBody takes ctx.stopSource ?? createStopSource(...), halts operator-interrupt at the tick head on level 1, and composes the source’s signal onto every drive’s driver options so level 2 cancels the in-flight session through the driver’s own stop bracket (tree terminated, pty released, exit confirmed).
Every interrupted drive settles
A drive that returns after a stop is settled before the halt – cancelled with failureReason: null when the source cancelled it, as it ended otherwise – so an interrupt leaves no running row; a needs-input drive stays open for --resume by design (FF-13002).
The halt names the request, and --resume clears it
The halt line’s Details carry signal, level, request, by and cancelled; the halt marks the request honoured with the cancelled runId, and --resume clears a standing request and narrates Cleared stop request for <id> (<state>, level <n>) – resumed. exactly once.
130/03 · The fleet sees and stops it
Presence carries the node’s live loops
readActiveLoops beside readActiveRuns yields one eleven-key entry per live loopRunId (scope, level, cap, phase, cycle, ref, runId, supervised, stop); both presence producers (the heartbeat verb and the launcher tick) and the registry reshape carry it as loops, emitted LAST and only when non-empty, so a node with no loop publishes its record byte-identical to before (FF-13005).
The status body names the serving node
/api/mesh/status carries localNodeId – the control node’s id, null on an unconfigured machine (TECH_DEBT item 18 (b) discharged).
One guarded loop-stop route in assign’s shape
POST /api/mesh/loop-stop is admitted by the same same-origin + application/json guard as /api/mesh/assign (the hoisted admitWriteRequest / resolveLocalWorkspaceRow, TECH_DEBT item 44 discharged), lifts exactly { scope, workspaceId }, resolves the local workspace and answers stopLoop’s document or its coded refusal.
One line and one button on this node’s card
The fleet node card’s current-work region appends one line per live loop (loop <scope> [· stopping | · cancelling] · <phase> <ref> · cycle <n> of <cap>), drops idle when loops exist, and renders Stop then Stop now (destructive) only on the card whose node is localNodeId; a remote node’s loop line renders with no button (FF-13006).
The rung is remembered per drive
The card’s rung memory is keyed to the drive – rememberStopRung(memory, loopRunId, rung, runId) – so a cancelled-then-resumed loop shows its button again on the new drive without a reload.
The board and the Rust drift pin are untouched
fleetCurrentWorkLines and nodeCurrentWork are byte-identical, nothing under ui/src/board/ moves, and git diff -- ui/ names eight fleet files (FF-5307 re-pinned with that measurement).
130/04 · The desktop stops what it supervises
The desktop lists the loops it supervises
The desktop’s view model carries loops: [{ id, label, signal }], one per supervised declaration from the signals map and the last answered declarations tick, and the window renders them as a second .controlbar (loop <scope>, the daemons’ ramp pill, one stop control, no Start), absent when there are none.
A desktop Stop is the request first
stop_loop(id) on a declaration places the hold and spawns aof work loop <scope> --stop (the argv formed in core by stop_argv, never in the shell – FF-13007); a second press escalates to cancel, the pill reads stopping, and a failed spawn is a footer notice.
The ladder is a pure, tested decision
stop_step(presses, since_cancel_ms, grace_ms, exited) in core/supervision.rs decides Request / Cancel / Wait / Kill / Done, with STOP_GRACE_MS = 30_000 counted from the cancel; the shell applies taskkill /PID <child> /T /F then waits on a Kill, and never start_kills a declaration.
The desktop attaches to a loop it did not start
A declaration whose relaunch is refused duplicate-run and which has not been pressed attaches instead of reading dead: it shows running, spawns nothing more, and each press runs the --stop verb with no grace and no kill rung (130/ADR-007).
A stopped loop is not relaunched
The declarations producer reads each candidate’s stop request and hands decideSupervisedDeclarations a stopped set of honoured loopRunIds, so a stopped loop yields no row until --resume clears the mark; src/work/loop.mjs gains no import (FF-13004).
130/05 · The register
Seven stop controls run on every suite run
FF-13001–FF-13007 live in three files under test/arch/loop/ (acd-loop-stop-request-single-home, acd-loop-stop-settles-the-run, acd-loop-stop-reaches-every-face), registered by import and spread in that directory’s index.mjs, 20 cases green, with FF-13007’s cargo half in supervision.rs.
Every control has been seen red
Each control’s named mutation was applied in place, observed failing with the message recorded in VERIFICATION.md’s fitness register, and reverted byte-for-byte; the three with a sweep also carry a non-vacuity probe (a misspelt needle reds the sweep as reading the empty set).
The budget table moved by exactly the register
The test/arch/loop row rose 59 → 62 by exactly the three files and the src/loop exemption’s why names its new members, in this one story’s hands.
130/06 · The live stop
The stop is observed working on a real loop
On this machine’s payload, a live --supervised loop on the test-bed was stopped from the verb (drain, then cancel), from a gap between drives, from the fleet card’s two rungs, and from the desktop’s row, each read at the source: the driver’s bracket in the diag log, the run record cancelled with failureReason: null, the halt line naming the request, no relaunch across the declarations ticks, and --resume clearing the request.
An unbriefed retry carries no declaration
retryRun carries the prior run’s brief minus loop, so an operator’s aof work resume of a dead loop’s run no longer becomes the newest declaration in scope and captures the verb; every loop retry passes its own brief, so a loop’s lineage is unchanged (51cfa6a, m130/F-01).
The desktop stops a loop started on a console
The desktop attaches to a --supervised loop it did not start and stops it through the same request (130/ADR-007, 874eef6, m130/F-02) – observed on its build: press 1 wrote level 1 and press 2 level 2 through the desktop’s own --stop spawn, and the loop’s bracket cancelled the session with no fallback kill.
129 · Loop concurrency
aof work loop drives a milestone in concurrent worktree lanes, and the unit of concurrency is a worktree
Set work.loop.concurrency: "refine_first" and a loop over a milestone refines every story serially in the primary, then fans each write-disjoint wave into one dispatch worktree per member, then verifies serially – the three-phase shape of m129/01, executed by the wave tick of m129/04, over the lane verbs of m129/03 and the child drive of m129/02. Unset, the shell is byte-identical to the sequential loop that preceded it. The milestone’s own framing defect is closed by construction: no two sessions share a checkout, so a story’s grade, its run record and its progress sample are its lane’s alone.
The loop ALWAYS owns the fan-out; work.agents.mode governs only what a lane’s session spawns
The SPEC proposed that orchestrated drive a milestone-level /aof:continue and solo drive lanes; ADR-001 §5 departed and the PO ratified it (STATE.md, 2026-09-12). There is therefore ONE home for fan-out and merge-back – code, not prompt prose – and this repository, which is orchestrated, reached the SPEC’s verifiable outcome without flipping a single story session to solo. An orchestrated-milestone drive stays addable later as an additive mode value; nothing shipped forbids it.
The tree that commits the change owns the record, and the primary learns of it at the merge
A lane mints its story’s run against a ref resolved IN the lane, settles it there, grades there, commits there and merges home; the primary’s story runs/ is empty until that merge and then answers the lane’s own runId. One wave run per epoch lives on the milestone ref in the primary and carries the loop’s liveness, so the supervisor sees exactly one declaration however many lanes are open (m129/04, held by FF-12903 and FF-12907).
Merge-back is the mesh’s one verb, pointed the other way
advanceBranchToBase merges a lane home – fast-forward when it can, a real --no-ff merge otherwise, a coded refusal and a named halt on a conflict or a dirty tree, and never a rebase, a force-push or a reset (m129/03, held by FF-12904). Lanes merge serially in completion order, and the next wave is cut from the merged HEAD.
The configuration surface is self-contained under work.loop, and the operator signed it off
work.loop.concurrency, work.loop.dispatch.concurrency and work.loop.agents.<phase>.mode live in the one bounds home, each falling back to its workspace twin, the lane bound able only to NARROW the pool’s (m129/07). This surface exists because the operator held the milestone door at the 05 accept to specify it, and 07 was scoped, built and accepted against that specification.
The concurrency was driven for real on this control node, three times
m129/06 is the measurement, not a claim: two lanes open and two sessions driving at once, each lane’s record carrying the wave’s loopRunId, one baseline per wave excluding the base’s inherited reds, serial merges, every lane cleaned up, a named exit, one loop process across a 5.5-hour run. Four live-measurement gaps remain open there with discharge conditions.
Seven structural controls hold the shape, each observed red on the leg it names
FF-12901 through FF-12907 (m129/05) are the milestone’s standing guarantee: one home for the mode, no second concurrency number, no session driver or node-pty inside the loop family, the wave read from work:next and never recomputed, the lane’s records and grade taken in the lane, lanes as children of one declaration, and merge-home never discarding. Every one was made red on its declared leg and restored byte-identical; the probes are in VERIFICATION.md § Fitness functions.
129/01 · The mode and the engine decide
work.loop.concurrency resolves in the bounds home as a mode
src/loop-bounds.mjs exports LOOP_CONCURRENCY_MODES (frozen ["sequential", "refine_first"]), DEFAULT_LOOP_CONCURRENCY ("sequential"), resolveLoopConcurrency (a member verbatim, anything else the default, never a throw) and loopConcurrencyFromConfig; the key is the ninth member of BOTH LOOP_BOUND_CONFIG_RESOLVERS and LOOP_BOUND_VALUE_RESOLVERS, rangeProbe admits exactly the two modes, stepProbe refuses every step on it as outside-declared-range, and loopBoundsFromConfig is unchanged at eight keys.
A loop record citing the mode as a ceiling carries a bound with no number
configBound in src/loop-record.mjs passes every resolver answer through positiveIntegerOrNull, so ceiling: [config:work.loop.concurrency] loads without a loop-ceiling* finding and projects state: "bounded", bound: null, comparison: null.
The engine routes an in-review story to the gate on status
decideLoopPhase reads next.status verbatim; an in-review story with tasks answers { act: "gate", ref, command: "work:validate" } whatever lastPhase, cycle or the UAT count say, a story with no tasks still refines, a supplied gate still routes on its findings, and every other status decides exactly as before.
Under refine_first the engine names the refine phase from two additive inputs
concurrency and unrefined ride decideLoopPhase and decideLoop unchanged in key set; under "refine_first" a non-empty unrefined answers drive refine <unrefined[0]> at cycle 1 ahead of every head state and type, any other concurrency ignores unrefined, a malformed unrefined is empty, and a through-review { state: "done" } stays { act: "done" }.
The wave decision is pure and bound-free
decideWave({ wave, heldSet, live, setAside }) in src/work/loop.mjs answers a frozen { dispatch, hold } – the wave in its own order minus live minus set-aside, the held set minus live – reading members as refs or { ref } objects and memories as arrays or Sets, with no bound input, no configuration read, no mutation, and null for any malformed wave, held set or memory.
Three lane stops are the closed stop set’s last three members
LOOP_STOPS is fifteen members, ending lane-open-failed, lane-merge-refused, lane-merge-conflict; LOOP_REFUSALS is unchanged at six; acd-loop-probe-contract’s literal and every other LOOP_STOPS pin (FF-6306, FF-12404 leg 5, 63/03) name the same fifteen. src/work/loop.mjs still imports nothing.
129/02 · The drive is a child
aof work drive <phase> <ref> takes a lent run across the process boundary
--run <id> joins the closed input schema, CLI flag spec and argv mapper of all three phase drivers; managedRunId reads it before ctx.loopDrive.runId ("" is absent), and under it the command mints no record, settles nothing, carries the id as AOF_RUN_ID / AOF_RUN_ITEM_DIR into the session env, captures the session id on the parent’s record when one exists, and returns settlementContext ({ projectsDir, transcriptBaseline, spendBaselineAvailable }) on the result of every real drive, lent or owned. A bare drive is byte-identical; --dry-run answers { ref, phase, command } and starts nothing, lent or not.
--fix <file> is the fix transport across the process boundary
The file is read as one JSON object in fixTransport’s shape before any record is minted and before any spawn; continue alone honours it (refine and verify ignore it as they ignore ctx.loopDrive.fix); the flag wins over ctx.loopDrive.fix; every way the file fails to yield an object is the one coded refusal drive-fix-unreadable.
Under --run, the child’s stdin is the cancel channel
ctx.stdin ?? process.stdin is resumed and its end listened for inside the same try…finally as the driver call; an end after the session is live aborts a signal the driver takes, one before it is ignored; the stream is paused once the session settles. Without --run stdin is never touched.
The session driver takes a caller’s AbortSignal
driveInteractiveClaudeSession(brief, { signal }) – an abort while the session is live requests { outcome: "failed", failureReason: "cancelled" } through the driver’s own stopForOutcome bracket (stop-requested → tree terminate on win32 → pty-released → exit-confirmed); an abort after settle or behind an unconfirmed sentinel stop changes nothing; a signal already aborted at the spawn point never spawns and answers processStarted: false. The export set and direct-import set are unchanged.
runBounded aborts, additively
signal, graceMs and stdin: "pipe" are three additive options on src/work-audit/spawn.mjs’s one bounded spawn; an abort ends a piped stdin, waits the grace (DEFAULT_GRACE_MS 20 s, or a positive safe integer), then kills; SPAWN_OUTCOMES is ["exited", "deadline-expired", "not-started", "aborted"]; the first of the deadline and the abort to fire decides and exactly one kill is sent, the message naming which bound sent it; an already-aborted signal never spawns; an unknown stdin is a returned not-started. Callers passing none of the three are byte-identical, stdio stays ["ignore", "pipe", "pipe"], and no shell exists on any path.
src/loop/child-drive.mjs spawns a lane’s drive and reads exactly one document
spawnLaneDrive({ ref, phase, runId, lane, fixFile, env, deadlineMs, signal, graceMs, spawnChild }) runs process.execPath through runBounded – under a Node runtime with [<this tree's src/cli.mjs>, "work", "drive", …], under the packaged launcher (isPackaged()) with the verb words alone – cwd the lane, env the parent’s plus the caller’s, stdin: "pipe", never a shell; the whole trimmed stdout is parsed as one JSON object and the exit is classified from it (document / refused / died / timeout / aborted), with the last twenty stderr lines on every answer and whatever document parsed before a kill. It mints no failure reason, touches no run record, prints nothing and imports no session driver.
src/loop/ exists as a declared budget exemption
SOURCE_DIRECTORY_EXEMPTIONS carries src/loop with a why naming the row it owes at the ninth file or with the loop-* root-leaf move; the family holds one member.
129/03 · The lane commits and merges home
commitWorktreeChanges lives in src/mesh/worktree.mjs and is scoped by paths
The verb is defined once, beside its sibling git verbs, and re-exported from src/mesh/worker-execution.mjs (absent definition, present re-export); its runner is options.exec, else options.pushExec, else the module’s default with a five-minute budget; a non-empty paths makes the stage, the staged check and the commit all pathspec-scoped (git add -- <paths>, diff --cached -- <paths>, commit --no-verify -m … -- <paths>), with the .aof reset owed only when the scope can reach .aof and then :(exclude).aof on the commit; absent, -A, the reset and the unscoped commit exactly as before. The worker’s two call sites are unchanged lines.
resolveRefInWorktree and worktreeWorkDir live in src/work/dispatch.mjs
Both are defined in the lane’s home and resolveRefInWorktree is re-exported from worker-execution.mjs; the resolver enumerates-then-filters through findWork and answers null for any traversal-shaped ref without constructing a path. src/mesh/worktree.mjs still carries only its pre-existing loadWorkspace import from work.mjs; the assignment sink’s static closure is pinned at 73 and the driver’s reach at 24.
advanceBranchToBase answers a dirtyPolicy
"strict" (the default) is byte-identical to the mesh’s door 2 – a non-empty status --porcelain refuses with no files key. "touched-paths" refuses only when the union of every staged index entry (a rename contributing both paths) and the worktree-side/untracked paths intersected with git diff --name-only --no-renames HEAD...<commit> is non-empty, returning assignment-gate-propagation-dirty-worktree with files sorted and deduplicated, read from --untracked-files=all under core.quotePath=false; the tree is untouched on every refusal. Door 1 (already-current) is decided before any dirt check; a conflict is still aborted and refused; an unknown policy is a thrown gate-propagation-bad-option before any git verb runs. The literal set is exactly ["strict", "touched-paths"].
src/work/dispatch.mjs composes dispatchLaneBase, commitDispatchLane and mergeDispatchLaneHome
dispatchLaneBase(lanePath, { primaryRoot, exec }) answers the merge-base of the lane’s HEAD and the primary’s HEAD (the main checkout’s line only when no primaryRoot is given); commitDispatchLane(lanePath, { message, node, exec }) answers { committed, tip }; mergeDispatchLaneHome(primaryRoot, ref, { milestoneDir, message, node, exec }) refuses a detached primary before any write, commits the loop’s own writes scoped to milestoneDir under the mesh identity, then runs the one verb from the primary at the lane’s tip under touched-paths – every answer carries ref, branch, base, tip and commit, with outcomes already-current / fast-forwarded / merged / refused (lane-merge-refused, reason: "detached-head" | "branch-missing" or files) / conflict (lane-merge-conflict, aborted, the lane and its branch intact). The module spells no git merge, rebase, reset or --force of its own and imports no child_process.
A lane opened on an existing line is advanced to the primary’s HEAD first
resolveDispatchLane(projectRoot, ref, { advanceTo }) runs advanceBranchToBase(lane, advanceTo) (strict) on every door whenever a sha is given and carries the outcome as advanced; any refusal is { outcome: "refused", code: "lane-open-failed", cause: <the verb's code> }; an advanceTo that is not a hex object name is a thrown dispatch-lane-advance-not-a-sha before any door opens; without advanceTo the answer carries no advanced key.
STATE.md merges by union, and only STATE.md
.gitattributes carries exactly one merge= line, wiki/work/**/STATE.md merge=union, matched by git’s own matcher for every STATE.md under the work directory and for nothing beside it; the line names neither text nor eol.
One porcelain parser, one exec seam, one mesh identity
parsePorcelainStatus, resolveExec / defaultGitExec (which forwards env) and meshIdentityArgs are exported from src/mesh/worktree.mjs and are the only spellings; laneChanges reads through the parser.
129/04 · The wave tick
The per-story post-drive ladder is settleStoryCycle in src/loop/cycle.mjs
settleStoryCycle(phaseRun, bookkeeping, ctx, { crossToVerify }) is the one home of the grade delta, the sampler, the review gate, the four bookkeeping maps and the verify cross, parameterised by the workspace it grades in; src/commands/loop.mjs reaches work:grade and the sampler only through it, is 1,686 lines (2,311 before), and under sequential produces byte-identical LoopState (ten keys) and driven rows (six keys). The gate ladder, DOCTOR_GATE_CODES, haltDecision and the git helpers stay in the shell and reach the ladder through its options bag; narrate and report are parameters – neither family module owns a printer.
refine_first is three phases, and BUILD is the wave tick in src/loop/wave.mjs
REFINE drives every in-scope story with no tasks, in stream order, then commits the loop’s own writes scoped to the in-scope drivers’ folders under the mesh identity (skipped when nothing was driven; a git refusal is a lane-merge-refused halt with the index restored). BUILD asks work:next with throughReview: true, hands the answer to decideWave minus the lanes it holds and the units set aside, asks work:dispatch { refs } to admit the wave, and runs every admitted member as a lane; done is the boundary. VERIFY honours a fresh gate act on an in-review story – validate, doctor, the recorded grade (a successor run’s, else the attempt run’s ledger) without a re-run – and routes to verify or a continue re-drive.
A lane runs its story in its own tree
Each admitted member is opened at the primary’s HEAD (a reused lane advanced there through advanceTo; a still-fresh running record in it refuses the open by name, a stale one is reclaimed and the lane re-driven from its own tree), its item resolved in the lane, its run minted there with the loop’s declaration and brief.lane, driven by a child aof work drive with the lent id, an isolated home and a deadline of startToCloseMs + startupGraceMs, settled against the lane item, graded and gated in the lane workspace, committed on the lane branch, merged home in the primary serially through mergeDispatchLaneHome, and cleaned up through work:dispatch --cleanup. The primary’s story docs hold no record until the merge. Every step is narrated; a cleanup refusal is narrated, never a halt; a halt in one lane drains the others (each committed and merged where it merges) before the loop returns.
Every refusal of the merge home is one named halt, thrown or returned
mergeHome reads mergeDispatchLaneHome’s thrown commit-failed / gate-propagation-failed / gate-propagation-base-unresolved as the refused shape (reason = the code, merge-home-error when it has none, the message on error), so mergeHalt names lane-merge-refused / dispatch:merge-home:refused for a returned and a thrown refusal alike, and lane-merge-conflict for an aborted conflict; the lane stays committed and kept for the resume’s reconcile, and the wave run settles failed.
The grade baseline is a property of the base commit
One baseline per wave, measured once in the first lane and keyed by the base sha; readGradeBaseline({ baseCommit }) answers by sha and by ref, never across the two; every lane’s delta is its own; a baseline that cannot be taken degrades once and grades raw; a held member on a new base measures a new one; a resumed loop reads a lane-held baseline back without a rubric run; VERIFY never re-grades; sequential keeps the per-story baseline on the primary.
The wave run carries the loop’s liveness
A milestone-level run with brief.wave (members, bound, base) is minted in the primary before the first dispatch, heartbeated on the wave interval through the hook’s own queue only while a lane is open, settled done with the wave and failed on a halt, and re-minted after every merge and every mid-epoch admission so each epoch’s brief names exactly its lanes and decideSupervisedDeclarations lists exactly one row per loop; every lane run’s brief.loop deep-equals the wave run’s except cycle; the driven rows carry lane, baseCommit, merge and wave.
Admission is dispatch’s answer, re-asked after every merge
A held member is dispatched only once the colliding lane merged, from the merged HEAD; refused members are re-asked as lanes close; the bound is narrated from work:dispatch’s answer and wave.mjs holds no concurrency literal; at-capacity with own lanes in flight waits, with none in flight halts lane-open-failed / work:dispatch:at-capacity naming the foreign holders and the remedy; an empty dispatch with held members is dependency-blocked with nothing in flight and waits with a lane in flight.
Signals, the parent deadline and the resume
The first SIGINT/SIGTERM drains and halts operator-interrupt; the second ends every child’s stdin, settles each lane run cancelled and halts the same way; a child past the parent deadline is settled timeout and retried in its lane under the lineage budget; --resume reconciles every live lane under the dispatch root in order before the first ask – a stale running run reclaimed and re-driven from its tree, a committed unmerged tip merged, an ancestor tip cleaned up, dirt committed first, an unclassifiable or out-of-scope lane narrated and left – and re-mints its wave run without retrying it as the milestone’s act.
The loop is a declared supplier of the lane opener, behind the door
69/FF-6907 admits src/loop/wave.mjs by name and reason: its resolveDispatchLane( call sits inside openLane, the function it binds onto ctx.runDispatchLane for work:dispatch to call inside the admission lock and pool; a call outside that function, a binding with no work:dispatch ask, or an undeclared supplier is each a named finding, and the door count stays the door’s alone.
129/05 · The account and the register
The four controls land under test/arch/loop/, registered once, non-vacuous
acd-loop-concurrency-single-home (FF-12901), acd-loop-family-boundary (FF-12902, FF-12906), acd-lane-records-and-the-declaration (FF-12903, FF-12907) and acd-lane-grade-is-lane-scoped (FF-12905) each export archTests ({ name, run }, every name arch/129/05 FF-129…), each one import and one spread in test/arch/loop/index.mjs under a 129/05 block, nothing named in scripts/test.mjs; every sweeping leg asserts what it FOUND before what it claims (the engine’s "refine_first" branch, the one dispatch.concurrency read, the seam’s runBounded( call, the wave’s work:next asks, the lane mint’s resolveRefInWorktree( binding, the brief.loop pass-through into the lane mint, the ladder’s work:grade call, the gradeBaselines map keyed by baseCommit), and an emptied sweep is a red naming the file. The three fixture legs (FF-12903, FF-12907, FF-12905) drive a real two-member wave through runLoopBody over test/support/loop/lane-fixture.mjs with the injected spawnLaneDrive and git seams – two child calls, zero processes, the found-count guarded before the claim. Every src module is imported lazily inside run(). The test/arch/loop budget row is 59 (55 → 59, its why naming 129 and the four files).
Every declared control of 129 resolves and has been observed red
All seven FF-129xx rows of ARCHITECTURE.md’s register resolve to a file on disk, carry no pending marker, and each has been made red on exactly the leg its row names, with the message recorded – the register in VERIFICATION.md ## Fitness functions is the record; aof work doctor 129 reports no control-unresolved at either severity.
The sweeps read structure, not windows
classifySites(source, { siteRe, guardRe, declarationRe }) and topLevelArguments(body) live in test/support/source-slice.mjs – the enclosing-function rule as ONE generic (FF-12906’s wave-read leg passes a nested-aware declaration pattern, since wave.mjs declares tick / runLane two spaces in) and a depth-aware argument splitter; acd-number-null-safe’s classifyNumberSites is a five-line call onto it. Direct imports are read through importSpecifiers, comments stripped through the one scanner, no positional slice (FF-11902 / F-47-04-ARCH-2 green on the four).
FF-12904 is an extension of the never-discards control, never a twin
BRANCH_PATH_MODULES is the six (posix, repo-relative: the mesh’s three plus src/work/dispatch.mjs, src/loop/wave.mjs, src/loop/cycle.mjs); the sweeps are pure over an injected loader (forbiddenFormOffenders, armedMergeProblems, dirtyPolicyLiteralSet) so every plant is driven in-process with no checkout touched, and the default loader is readFile, so an absent member is ENOENT naming its path. The ARMED leg judges each module by its own argvs; the dirtyPolicy literal set is exactly strict, touched-paths and an empty set is NOT FOUND. The four shipped cases keep their names; four FF-12904 cases join them.
The autonomous prompt names the key beside its home
One paragraph in <config> of src/bundle/commands/autonomous.md names work.loop.concurrency, sequential, refine_first (refine → lanes → verify, in that order), .aof/aof.config.json and “flag” – no numeral, no aof work / aof: token; FF-7101 leg (a) binds it (LOOP_BOUND_VALUE_RESOLVERS["work.loop.concurrency"] is resolveLoopConcurrency by identity). The three renders (.claude, .codex, .opencode), src/bundle/manifest.json and .aof/aof.lock.json agree with the source (aof work update --dry-run → 0 updates); autonomous-shell-out-prompt’s survivors config-key set is four.
The lane suites share one fixture spelling
writeRel, mergeHeadAbsent and conflictMarkers are exported from test/support/dispatch-lane-fixture.mjs; gate-propagation-refusals-leave-branch, mesh-worker-commit-diff and work-dispatch-lanes import them and spell none of their own.
129/06 · The second live run
refine_first is switched on in this repository and drives real waves into lanes
.aof/aof.config.json carries work.loop.concurrency: "refine_first", work.loop.agents.refine.mode: "orchestrated" and work.loop.agents.continue.mode: "solo", with work.agents.mode untouched at orchestrated; three live aof work loop runs (127 on 2026-09-15 and 2026-09-15/16, 130 on 2026-09-21) drove REFINE → BUILD waves → VERIFY on this control node against real Claude sessions, and the readings of each are in VERIFICATION.md 129/06 with their run ids, lane paths, base shas and halt details.
Two lanes run at once, each with its own record, heartbeat and grade
Measured at the source on 2026-09-21 (loop-diag.130.2026-09-21T16-42-17-910Z.log, 615 lines, one loop process pid 58920): Wave 3 – dispatching 130/03, 130/04 (bound 3). opened dispatch-130-03 and dispatch-130-04 at a49725a 156 ms apart, both lanes were on disk 19:21:50 → 20:15:04Z and two sessions drove concurrently 19:29:08 → 20:03:36Z; each lane minted its own run records carrying brief.lane {worktree, branch, baseCommit} and the wave run’s brief.loop.loopRunId, each advanced its own heartbeatAt, and one baseline measured once in lane 03 at a49725a (5 inherited over 1987) served both lanes’ grades.
A lane’s grade is the lane’s, and the inherited reds are excluded by the baseline
Gate work:grade 130/04 – pass, 0 of 1987 case(s) failing (5 inherited, excluded by the baseline). at 20:14:58Z and the same for 130/03 at 21:07:04Z, each after its own fix cycles in its own worktree; no lane’s grade carried another lane’s reds, which is the whole-checkout grading defect 129 exists to end.
Lanes merge home serially through the one verb, and the next member is cut from the merged HEAD
04 merged first (own-writes 8fcd0c4, then 2f728fa = merge(8fcd0c4, 8088cd9)), then 03 over the moved primary (c107b6f, then d1e6477 = merge(c107b6f, c55b2f1)), both --no-ff through advanceBranchToBase, no conflict, write-disjoint by construction; the next member’s lane (130/05) opened at a56d7da, whose parent is 03’s merge. Every finished lane cleaned up (removed, branch … removed ×5).
The wave run is a record per epoch, and the supervisor sees one loop
Wave run 130 -0003 carried brief.wave {members: [130/03, 130/04], baseCommit 46cb366, bound 3} with a heartbeat to 20:11:49Z, settled done at 04’s close and re-minted at once as -0004 {members: [130/03], baseCommit 2f728fa} – 129/04’s epoch ruling as a record rather than a narrated line. One loop process ran the whole 5.5 hours with no supervisor relaunch.
The lane child owns its console, so a console-scoped kill never reaches the loop
runBounded takes ownConsole, and on win32 spawnLaneDrive spawns the lane child detached with its own hidden console; the 2026-09-21 run survived five lane kills whose stderr shows node-pty’s console-list agent failing AttachConsole failed against the detached child, where the same mechanism had killed the loop unbracketed twice before (deaths #4 and #5).
The driver honours the stop it requested, and a provider wait is not silence
A stop the driver itself requested settles with the requested outcome even when the liveness probe sees the dead pid first (a death nobody asked for is still agent_died), and a Usage limit reached · continuing automatically at <time> / You've hit your session limit · resets <time> line read off the output suspends the heartbeat deadline until progress resumes, with startToCloseMs still bounding the attempt and one provider-wait breadcrumb in the diag log.
The loop-diag log names the tree that ran
installLoopDiagnostics reads aof --version’s own string through an injected seam and the start line carries build=<string>, degrading to unknown when the reader throws – the payload was re-stamped mid-run on 2026-09-21 and nothing in the log could say which tree the loop had loaded.
129/07 · The loop’s settings are self-contained
The loop’s settings live under work.loop, each falling back to its workspace twin
.aof/aof.config.json → work.loop carries the mode (concurrency), the loop’s own lane bound (dispatch.concurrency) and the role mode of each driven phase (agents.refine.mode, agents.continue.mode); every one is read from the file and never passed as a flag, and a repository that sets none of the three new keys runs byte for byte as before – no flag composed, no bound passed, no prompt text changed.
The bounds home holds twelve keys, the three new ones answering null when unset
src/loop-bounds.mjs exports resolveLoopDispatchConcurrency (a positive integer verbatim, else null), resolveLoopAgentMode (solo | orchestrated verbatim, else null), LOOP_AGENT_MODES, the config-shaped loopDispatchConcurrencyFromConfig / loopAgentRefineModeFromConfig / loopAgentContinueModeFromConfig, and loopAgentModeFromConfig(workspace, phase) over LOOP_AGENT_MODE_RESOLVERS (refine, continue; any other phase answers null). Both resolver maps carry work.loop.dispatch.concurrency, work.loop.agents.refine.mode, work.loop.agents.continue.mode after work.loop.concurrency, in that order; the range probe admits exactly the members; loopBoundsFromConfig (the deadline policy) is the eight of HEAD. The home reads no non-work.loop.* key.
The lane bound narrows the pool’s, never widens it
work:dispatch takes bound (schema { type: "number" }); narrowDispatchBound(pool, requested) in src/work/dispatch.mjs answers min(requested, pool) for a positive integer and the pool’s bound for anything else, and every face of the answer (list, refs, ref, sweep, cleanup) carries the EFFECTIVE bound. work.dispatch.concurrency stays the one number for the machine, read by src/work/dispatch.mjs alone. The shell (src/commands/loop.mjs) resolves work.loop.dispatch.concurrency once through the home and hands it to the wave as bounds.laneBound; the wave passes it as bound on its --list read and its { refs } ask when it is a number, and nothing when it is null; the narration Wave N – dispatching … (bound B) and the wave run’s brief.wave.bound carry the effective bound. Under a lane bound of 1 a two-member wave runs its lanes one at a time.
The phase drive carries the phase’s role mode; --orchestrated is --solo’s twin
aof work drive refine|continue <ref> composes /aof:<phase> <ref> --solo or … --orchestrated from work.loop.agents.<phase>.mode (phaseCommand(phase, ref, mode), PHASE_MODE_FLAGS), and /aof:<phase> <ref> when the key is unset; verify never carries a flag; --dry-run shows the composed command, and the real CLI composes the same one. refine.md and continue.md parse --orchestrated as the override in the other direction (a solo work.agents.mode to orchestrated for one run), refuse the two flags together before any role runs, and name work.loop.agents.<command>.mode with its fallback to work.agents.mode; autonomous.md names the three keys and their workspace twins beside work.loop.concurrency. The nine renders, src/bundle/manifest.json and .aof/aof.lock.json agree with the source.
The standing controls admit the shape and tell the two dispatch keys apart
FF-6901’s annexation leg reds a work?.dispatch?.concurrency read in the home and admits loopConfig(workspace)?.dispatch?.concurrency; FF-12901 leg 1 pins twelve keys, leg 2 finds exactly one pool read (src/work/dispatch.mjs) and exactly one loop-key read (src/loop-bounds.mjs) and names a second reader of either, and its family sweep reads a dotted key whole; FF-7101’s key regex reads work.loop.agents.refine.mode whole. Each carries a self-check plant.
128 · work memory joins the route table
The work memory route
work:memory (src/commands/work/memory.mjs) is a registered command whose cli.route is
["work", "memory"]; deriveRouteTable() carries the key work memory, and
resolveRoute(["work", "memory", …]) lands on it with the verb and its arguments as the rest.
One door
src/cli.mjs carries no subcommand === "memory" branch and no workMemoryCommandCli;
workMemoryCommand is exported by nothing and imported by nothing under src/; aof --help lists
aof work memory … under Work and the static Also: tail names aof session alone.
The seam’s parse, applied by its one home
The command’s cli.argv re-serialises the face’s parse into argv and hands it to
parseMemoryArgv, so the six scope flags, --item as both recall filter and rebuild scope,
--all, and the non-positive-or-non-numeric --limit fallback are applied by the seam and not
copied; SCOPE_FLAG_SPECS is derived from the seam’s SCOPE_FLAGS.
One core path for both doors
runMemoryVerb answers help before resolving a backend, gates the verb as a coded unknown-verb
refusal (status 400), then resolves and dispatches through executeMemoryVerb; the routed
command’s run calls it with the face’s input, and runMemory – the in-process entry the twenty
test harnesses and the retrospective-memory-ingest loop record actuate – composes over it with
parseMemoryArgv’s.
The help guard at the parse
parseMemoryArgv answers help: true for --help or -h anywhere in argv, checked before any
other token; through either door a help request prints the usage on stdout at exit 0 and reaches no
backend; help typed as a verb is an unknown verb.
The two projections
renderMemory and memoryJson are the seam’s one human and one --json projection per verb; the
command’s render/json and the collector-based defaultRender call the same two. --json recall
is the records array; brief the digest without text; reindex/ingest the summary without
records; status the { backend, recordCount, … } object; help { usage }.
A null render prints nothing
runCommandFace prints a rendered result only when it is not null; renderMemory answers null
for an empty injection block, so recall <miss> --block on a backend that answers no record is zero
bytes on stdout at exit 0.
The spine’s policies, inherited
An undeclared flag – --bogus, and --help alike – is Unknown flag "…" for work:memory. at exit
1; under --json every refusal is the face’s one envelope carrying code.
The one usage line
MEMORY_USAGE is the seam’s exported usage line; cli.spec.usage and memoryUsage()’s first line
read it, and it spells --all and --block.
The four frozen lists
WORK_IDS carries work:memory; BOARD_DEFERRED carries memory with its stated reason;
argsFor("memory") is status --json; PRINTERS has no work/memory.mjs row and
PRINTER_CEILING is 11; src/work/memory.mjs calls console.log nowhere.
src/commands/work/
The directory exists with one member, and SOURCE_DIRECTORY_BUDGETS carries a row for it at
ceiling 1 / allowance 0 whose why names the fold of the other work:* commands as a separate
item; test/integration/features is a budget row (9) rather than an exemption, and
test/integration/steps is at 10.
The integration scenario
test/integration/features/work-memory.feature runs through the real CLI with steps resolved by
convention from the shared grammar: status renders memory: backend=none records=0 on a bare
fixture, status --json answers backend: "none", and bogus fails naming the verb.
125’s README control, green
acd-readme-names-what-ships’s every command the README spells resolves row passes unedited; the
five aof work memory lines the README spells resolve through deriveRouteTable.
124/02’s leg 4, flipped
acd-learning-edge-reaches-every-cut leg 4 asserts that a registered command’s route IS
work memory, and still reads the memory flag surface from src/work/memory.mjs’s own branches.
The loop record
retrospective-memory-ingest.md (bundle member, installed copy and manifest entry in agreement)
states that the memory surface is the registered command id work:memory and that
module:src/work/memory.mjs#runMemory remains the narrowest real export for the ingest act; its
four seam line citations are the shipped seam’s.
127 · Backlog and archive – the work tree holds what is live
The work tree has three roots and one rule for each
<work.dir>/ holds the live items, <work.dir>/backlog/[<group>/…]/<type>_<slug>/ holds
un-numbered drivers, <work.dir>/archive/<NN_type_slug>/ holds accepted ones name verbatim – one
enumerator walks all three (m127/01/Three roots, one enumerator), one verb mints a number on the
way in (m127/02/One mint), one verb moves a folder on the way out (m127/03/The verb), and this
repository lives that way (m127/05).
Identity is the ref, never the location
A citation by number (52, m52/ADR-003, depends: [121]) resolves wherever the folder sits;
find, doc, validate, doctor, depends and memory ingest see the archive as part of the
stream, and next, loop, the default list / recent and the board’s default fetch see only
the live root, each with an --all / includeArchived door. No verb in this milestone changes a
number an item already has.
The order of the stream is the order of work
An idea is born un-numbered and gets its number at aof work promote – at the tail, or at --at P
through the one reindex engine – so the stream’s numbers are minted in the order the operator
schedules; the insert-* family is that verb under its old names.
Every node sees the same shapes
A remote node’s cache-first find / list / next answer for a backlog slug or an archived ref
as the owning node does (schema 9, m127/04); the board paints the backlog as rows and hides the
archive behind one toggle with one mark; the fleet never offers to assign an un-numbered item.
The history the public root cannot carry is recorded
.aof/rename-ledger.tsv holds the 1,128 renames the public-root cut removed, derived once with
the cited-path resolver’s own argv; every resolver edge reads it after git’s own records, so an
archived document’s citation of a moved module still resolves on a fresh clone.
127/01 · One enumerator, three roots
Three roots, one enumerator
listItems walks <work.dir> (the live stream, rows byte-identical to before), then
<work.dir>/backlog/** (groups at any depth; a leaf is a <type>_<slug> folder with no number,
never descended, never a task), then <work.dir>/archive/ (ITEM_RE folders, name verbatim,
stories walked as at the root) – each root only when it is a directory, and a project with neither
root yields exactly the rows it yielded before.
The row shapes
A backlog row carries number: null, ref: <slug>, backlog: "<group path>" (forward-slashed,
"" at the backlog root); an archived row carries archived: true; a live row carries neither key
and the frozen seven-key listStream/findWork shape is widened only on the two new roots.
One home for the grammar
ITEM_RE, BACKLOG_ITEM_RE, BACKLOG_ROOT and ARCHIVE_ROOT are each defined once, in
src/work.mjs; src/work/doctor.mjs, doctor-freshness.mjs and src/work/reindex.mjs import
them and define none. Six work-root scanners are allow-listed by path and reason in FF-12701
(doctor’s orphan lane, routing’s foreign form, recovery’s foreign tree, migrate-folder’s foreign
stories scan, provenance’s synchronous resolver, ratchet); observe.mjs’s three scanners and
doctor-freshness.mjs’s roadmap lane now take rows from the enumerator, and migrate-folder.mjs
mints through appendPosition.
One live-row predicate
isLiveStreamRow(row) ⇔ row.number != null && row.archived !== true, exported from
src/work.mjs. nextWork’s driver and story walks, listStream’s default path, aof work list,
aof:recent (through aof work list --json) and insert-story --under filter through it;
findWork, validateWork, src/work/doctor.mjs, aof work read, depends resolution and
memory ingest do not – an archived item resolves by its ref and satisfies the edge it is named
in, and insert-story refuses insert-parent-archived rather than scaffolding under archive/.
--all
aof work list --all (and listStream(workDir, { all: true })) appends the archived rows after
the live and backlog rows; the default listing is live rows by number, then backlog rows by group
path then slug (plain string compare), and no archived row.
validate and doctor over three roots
A backlog record doc must carry no number: and is otherwise checked as at the root; a backlog
row is neither a source nor a target in the depends graph; two backlog leaves sharing a slug are
one backlog-slug-duplicate; the numbering lanes run over every numbered row (archived included)
and no backlog row; roadmap-folder-mismatch counts an archived milestone as a folder and a
backlog milestone as nothing; the orphan lane knows both root names and walks the archive; the
scope grammar of doctor and validate reaches all three roots through the branches it already had.
Every .number consumer is null-safe
The ten files holding a .number parse (asserted as the exact set by FF-12702) guard each site
within its enclosing function by isLiveStreamRow, a != null test or a story narrowing;
appendPosition answers the highest number ever minted plus one over live AND archived rows (a
number is never retired), and selectAffected shifts live rows only.
The three controls
FF-12701 (acd-work-root-one-enumerator), FF-12702 (acd-number-null-safe) and FF-12706
(acd-next-walkers-exclude-archived) are registered in test/arch/work/index.mjs, green, and each
observed red under the probe its register row names.
127/02 · Promote mints the number
One mint
aof work promote <slug> [--at P] [--yes] [--json] (src/commands/promote.mjs, /aof:promote)
is the only place a top-level number is minted: it resolves exactly one backlog row by slug
(promote-not-found / promote-ambiguous / promote-numeric-ref otherwise), takes the next
number from appendPosition over every NUMBERED row – live and archived – or opens the slot at
P through the existing reindex seam (transitionStreamReindexed, space: "top-level", the
count gate unchanged), renames the backlog leaf into the stream and stamps number: and the
# NN · heading prefix surgically; status: and updated: are untouched.
An archived number is never re-minted
--at P refuses (promote-number-archived) when an archived driver holds a number in the shift
range, and the append counts archived rows, so no promotion writes a number an archived folder
already carries.
depends: is validated at promotion
A backlog item’s depends: is a planning note until promote reads it: a numeric entry must
resolve live or archived (promote-depends-unresolved), an entry naming another backlog slug is
refused (promote-depends-backlog) with both ways out named.
insert-* are what promote --at is
insert-milestone|chore|uat scaffold into backlog/ and promote at P through promote.mjs’s
runInsertTopLevel; insert-story keeps the nested engine (a milestone’s local axis is not a
stream number); src/commands/insert-shared.mjs shrank 638 → 628 lines and neither it nor any
insert face computes a number.
work.intake is write-side only
initConfig writes work.intake: "backlog" only into a config it creates; an absent key reads as
"stream"; the token appears in src/** only in init.mjs, init-update.mjs, promote.mjs
(one refusal text) and the bundle prompts; no reader branches on it. A phase door
(work:continue|refine|verify) refuses a backlog ref (phase-backlog-ref).
The prompts have one door
aof:add-milestone|chore|spike|uat|story scaffold un-numbered under <work.dir>/backlog/[group/]
under either setting and compute no number – under "stream" they call aof work promote;
aof:refine and aof:continue promote a backlog ref as step 0 and continue with created.ref;
aof:add-story refuses a backlog parent (“promote first”) and an archived one; all eleven prompts
are rendered into the three runtime trees with their manifest hashes.
127/03 · Archive is a move
The verb
aof work archive <NN> | --done [--yes] [--json] (src/commands/archive.mjs, /aof:archive)
moves an accepted driver’s folder, name verbatim, from <work.dir>/ to <work.dir>/archive/; it
refuses a ref that is missing, not a driver, in the backlog, already archived, not done, or
whose destination exists, and never touches a number. A done milestone moves with its stories;
a done story under a live milestone stays put.
--done behind one gate
--done selects every done driver at the root and refuses without --yes
(archive-confirm-required, the candidate list in detail); with it, every candidate moves in
number order in one run and one envelope (archived, rewritten).
The engine is the seam’s fact-writer
archiveItems and rewriteCrossingLinks live in src/work/archive.mjs (reindex.mjs’s twin: fs
only, no effects, no number); the command reaches them only through transitionStreamArchived in
src/effects/stream-transitions.mjs – lock → move → stream.archived appended and drained →
publish – so the fleet cache follows through the existing publish-on-mutate row.
Every crossing link resolves to what it did before
One rewrite pass over every .md under the work dir, syntactic (](target) only, fenced blocks
included, frontmatter never), rewrites exactly the relative inline links that cross the archive line
– a root-sibling ../NN_… gains archive/, a link out of the moved folder gains ../ – preserving
each file’s EOL and BOM; every link resolves to the same path after the move as before.
Readers resolve by ref, path-readers by findWork
find, doc, validate, doctor, depends and memory ingest answer for an archived item at its
new dir; the two runtime readers of a live item path outside wiki/
(acd-tune-carries-no-second-rule, acd-declared-program-single-speller) resolve through
findWork, and 03’s census suite names every later reader that reads a real item folder.
Never automatic
aof:verify never archives: verify.md names aof work archive <NN> once, as the operator’s next
act after the accept, and no other prompt names the verb.
127/04 · The fleet and the board see the shapes
The cache row carries the two shapes at every hop
The global work projection is schema 9: work_items gains backlog TEXT and
archived INTEGER by the v8 in-place ALTER idiom, archived mapped true → 1 at the bind; the
disk projection, both frame doors, the read-back and the fleet payload widen ONLY a backlog row
(number: null beside backlog) and an archived row (archived: true) – a live row keeps its
frozen seven keys. The row screen lives in src/work/item-row.mjs, re-exported by the store.
A remote node answers as the owning node does
cacheOnlyItem (src/work/read.mjs) rebuilds a cache-only backlog or archived row in the
enumerator’s shape – number derived from backlog’s presence, never from the ref – so
findWorkCacheFirst, listStreamCacheFirst (default and --all) and nextWorkCacheFirst, and
aof work find|list|next on that node, answer for a backlog slug or an archived ref exactly as the
owner does; a checkout that still holds the folder keeps its own location.
One list parameter
GET /api/work/list?includeArchived=1 threads work:list’s own all; the face filters nothing;
the WorkItem type carries backlog and archived.
The overview shows the backlog as rows
deriveBoard partitions number === null rows out FIRST; the overview paints them as DESIGN
§Surface 1’s region after the gates – a heading with the count and aof work promote <slug> in
its subline, root rows first, groups as verbatim mono paths, rows as TYPE · slug · title ·
[stale] with no ring, chip, number, progress, dots or button; absent when the wire carries none.
One toggle reveals the archive with one mark
Show archived – one aria-pressed button in the top bar left of the legend – flips the request
in place (busy + disabled in flight, reverting with the dispatch toast on error); archived cards
render after the live ones with the archived pill and a bg-muted/40 surface, the ▤ N archived
chip appears (even at 0) only when ON, and the mark is painted in the lane board’s switcher button,
switcher rows, lane cards under all and the detail header – never on a story.
The fleet partitions the backlog out
milestoneListItems drops number: null rows before the fleet paints or offers AssignAffordance
on a milestone; archived milestones fall under the fleet’s existing status filter, unmarked.
127/05 · This tree holds what is live
This repository’s intake is the backlog
.aof/aof.config.json sets work.intake: "backlog"; an aof:add-* on this tree lands
un-numbered under wiki/work/backlog/, and the add → promote round trip holds on a byte-faithful
shape copy of the real stream (every record doc and feature, no runs/).
The done drivers are archived
aof work archive --done --yes moved 125 done drivers (67 milestones, 19 stories, 4 spikes, 35
chores) under wiki/work/archive/, name verbatim, in one run (ed9c00c): 2,289 renames,
1,710 crossing links rewritten in 161 files, no number:, status: or updated: line changed,
the four wiki/memory.md links fixed by hand, and the memory index regenerated. The root of
wiki/work reads as the live items and the three roots.
The outsider’s check passes on the real stream
test/work/stream/work-this-tree-holds-what-is-live.test.mjs runs over THIS tree: every root
folder is live or a sub-root, every ITEM_RE entry under archive/ is a done driver, find 52 /
doc 52 / doctor 52 / validate answer for the archived milestone, next and the default
listings never propose one, next 32 is ready through its archived dependencies, the board face
excludes the archive by default and includes it on the parameter, no relative link resolves worse
than before the move, and the 27 test files that read a real item folder read it where it lives.
126 · The declaration is the unit
A declaration survives the machine it was declared on
The four seams the SPEC measured as knowing-but-not-acting are now one chain that closes. A loop
declared --supervised and killed with its runtime is listed by the predicate (m126/02), reaches
the desktop supervisor through mesh status --json --declarations (m126/02), is reconciled into a
running child within one 30 s tick (m126/03), and resumes against a compute budget that charges
attempt time rather than calendar (m126/00). Observed end to end on 2026-09-10: a declaration
minted 2026-08-23 – eighteen days before – resumed rather than refusing deadline-exhausted, and its
next attempt was started by the supervisor with no operator command.
The Declaration is the supervised unit, and the supervisor knows nothing else
The noun the loop already persisted is now what a supervisor keeps alive. app/desktop holds no
loop, scope, phase or run vocabulary; it receives {id, argv, cwd, label} and reconciles desired
against actual. A completed loop stops appearing in the answer, so nothing relaunches it –
reconciliation replaced restart-on-exit, and no completion semantics crossed the boundary
(m126/03). The next thing that declares – a graph build, a soak – is supervised by the same
mechanism without the supervisor learning what it is.
A supervisor-spawned loop can drive a Claude session with no console
Measured rather than assumed, on the real supervisor: an aof.exe work loop … --resume child spawned
under CREATE_NO_WINDOW with piped stdio allocates a headless ConPTY (conhost.exe --headless
--width 80 --height 24) and runs claude.EXE under it. Every aof.exe child of the supervisor
reports MainWindowHandle = 0. This was 126/03’s one named risk and no fixture could have
answered it.
An operator can see what a loop is doing while it does it
aof work loop narrates in flight through its one printer (m126/00) and aof work run-status
renders the record’s own facts rather than two of its sixteen keys (m126/01). The eleven-hour
silence the milestone was framed from is not reachable from this tree: the first line of a
supervised run is now Driving <ref> – <phase>, cycle N of M, <level>.
The control node’s daemon environment is stated, not guessed
aof mesh desktop install --autostart registers login autostart idempotently through one injected
runner, and both install and run report a three-check preflight that repairs nothing
(m126/04). A Windows service was refused on the measured basis rather than deferred: session 0 has
no login session, so claude there is unauthenticated and every supervised loop would start and die
on auth.
One warning stopped being an eleven-hour session’s only output
m126/05.
126/00 · The loop says what it is doing and counts what it did
The attempt-series clock
scheduleToClose is measured as accumulated ATTEMPT milliseconds summed over the retryOf lineage:
a settled attempt ends at updatedAt, a reclaimed one at heartbeatAt ?? updatedAt (never at the
reclaim stamp), a stale running one at that same last liveness, and only a demonstrably-alive
running attempt ends at now.
The millisecond decider
decideScheduleToClose takes {elapsedMs, ceilingMs} and refuses anything that is not two numbers –
the {startedAt, now} form is gone rather than surviving on a branch – and both shell call sites
obtain elapsedMs from the summer, so downtime between attempts is charged to nobody.
One retryOf walk, in the engine
retryLineage, attemptElapsedMs and lineageElapsedMs live in src/work/loop.mjs;
src/commands/loop.mjs holds no retryOf traversal of its own, and retryLineageStartedAt points
at the engine rather than duplicating beside it.
One staleness threshold per invocation
heartbeatFromConfig is resolved once per loop invocation and shared by the reclaim sweep and both
deadline sites, so the sweep and the clock cannot disagree about whether a run is alive; the engine
holds no staleness definition of its own and takes stalenessMs and isStale as data.
In-flight narration through the one printer
src/commands/loop.mjs announces Driving <ref> – <phase>, cycle N of C, <level> before every
drivePhase call, and says Resumed, Retrying and Reclaimed once each at the sites that
produce them; the four in-flight lines that already existed reach the same seam. The module still
holds exactly one console.log – the injected launcher printer – and PRINTERS gains no row.
--quiet
work:loop accepts quiet in its closed input schema, its CLI spec and its argv shaper; under it
zero in-flight lines are printed and the terminal account is byte-identical to the loud run,
including a halt’s stop, ref, producer and resume command. The default is loud, and --json still
never launches.
Nothing new on disk
The run record’s sixteen keys and the loop declaration’s eight are unchanged – every instant the clock reads was already recorded.
126/01 · run-status renders what the record holds
aof work run-status names every fact its record carries
The human render prints the run’s state, failure reason, reclaim marker and resume instant, the loop envelope’s phase, cycle-against-cap and level, the attempt, the elapsed time, the heartbeat age, the session and the node – each shown only when the record holds it, and never invented when it does not.
Elapsed and heartbeat age have one arithmetic, shared with the deadline that enforces it
The render imports attemptElapsedMs from src/work/loop.mjs rather than deriving a duration of its
own, so a reclaimed run reads its last heartbeat and not its reclaim stamp: the measured 124/00
attempt renders 1,785,756 ms where a createdAt-to-updatedAt span would read 41,380,713 ms.
A human render has an injected instant, and src/spine/face.mjs is where it comes from
The one faceCtx the face constructs carries now (ISO-8601 Z) read from the wall clock in that
module and no other, handed to every cli.render; src/commands/run-status.mjs contains no
Date.now(), no new Date( and no readFile, and a render called without a now omits its two
time figures rather than printing NaN or throwing.
A worker-mirrored run history says that it is one
A fromWorker result’s heading names the mirror and the node that reported it, so a projection-held
row is not read as a live local fact; answeredFrom is deliberately not that marker, because a
disk-resolved item whose runs were streamed answers cache.
The --json document is frozen across the change
All six producing return sites keep their present key sets and order – three distinct shapes, with
exactly one four-key path that gains no reportedBy – the 16-key record and its 8-key brief.loop
round-trip unreshaped, json: (result) => result is still identity, ref-not-found/404 is unchanged,
and the render writes nothing back to the result it is handed.
53/ADR-004’s freeze is narrowed in the open: the document is frozen, the render is not
53/FF-5307 leg 2’s byte-pin of src/commands/run-status.mjs is re-pinned at a new digest with the
reason written beside it rather than deleted, and src/board-ui.mjs and the ui/ tree hash are
unmoved.
126/02 · The declaration predicate and its door
The supervised-declaration predicate
decideSupervisedDeclarations in src/work/loop.mjs is a pure decider that takes workspaces of run
records and returns rows, spelling no failure-reason literal and no run-state literal, opening no
store, touching no filesystem and reading no clock – its verdicts come from isRunning, isStale,
retryReadiness and readLoopDeclaration, with deadline-exhausted recomputed from the attempt
clock so a lineage whose compute budget is spent is left off the list even when the store answers
ready.
isRunning on the run store
src/run-store.mjs exports isRunning(record) beside isStale – a pure predicate that tells a
record in flight from a settled one, so the run-state vocabulary has one home and a caller can ask
the question without spelling a state.
Supervision as the declaration’s ninth key
A loop declaration envelope is nine keys with supervised last, set by aof work loop --supervised,
inherited on --resume, and projected as a sixth recoverable key defaulting to false; the
usable-declaration requirement is still five keys, so every declaration already written to disk reads
back as unsupervised and yields no row.
The loop’s argv in one home
src/loop-argv.mjs is a zero-import leaf holding LOOP_INPUT_KEYS, LEVEL_FLAG, RESUME_FLAG,
loopInputOf and argvFor – the only module in src/** that composes a ["work","loop",…] array.
src/commands/trigger.mjs imports it rather than declaring its own, and every --flag token it can
emit is one work:loop declares in both cli.spec.flags and its input schema.
The declarations door on mesh status
aof mesh status --json is byte-identical to what it was; the same verb with --declarations gains
exactly one key, declarations, carrying { ok, rows, skipped }. No second command and no work:*
verb answers this question. resolveNodeWorkspaces’s skipped list reaches the answer verbatim, an
ok: false resolver answers ok: false with empty rows and no standalone fallback, and each row
carries id, label, argv, cwd, scope, level and cap – the cwd being that workspace
descriptor’s own projectRoot, never process.cwd().
The producer as a closure-free module
src/mesh/declarations.mjs holds the producer and is reached only through a dynamic import inside
the --declarations branch, so it sits in no static import closure and the command registry is
loaded when an operator asks for declarations and at no other time.
126/03 · The supervisor reconciles a supplied set
A supervised child is an owned specification carrying its own working directory
SupervisedChild in app/desktop/crates/core/src/supervision.rs is { id, label, argv, cwd } with
owned strings; the two seeded daemons keep their mesh_serve() / mesh_ui() constructors and carry
cwd: None, and every declaration row carries Some(projectRoot). supervise_child calls
current_dir only when the child itself carries one, so no spawn site supplies a default.
The supervised set is composed, not matched
compose_supervised_set returns the two seeded daemons followed by the supplied rows in row order.
No match on is_control_node selects a supervised set; the role latch’s single server start is the
one named exemption, asserted as a named exemption so a second cannot arrive silently.
The reconcile is a pure plan in the core crate
reconcile(rows, live) -> Plan { start, stop, retain } is a pure function whose #[cfg(test)] sits
beside supervision.rs’s existing tests, where cargo test actually runs. A row with no controller
starts; a controller with no row stops; a held controller retains; a declared, unheld, non-running
child starts again – the level-triggered property. LiveController is { id, desired, held } and
carries nothing about why a child stopped, so two inputs differing only in “did it exit 0” are the
same value and an exit-code rule cannot be added without changing the type. The two reserved daemon
ids are named in neither direction.
One poll interval supplies the set every tenth tick
poll_carries_declarations(tick) is true on every tenth tick of the existing 3 s cadence – 30 s –
so the flag rides the one existing loop and no second cadence exists. The role latch still starts the
server once, a declaration row never does, and the UI daemon is still started immediately.
The runtime gate is narrower than the source roster
DECLARATION_ARGV_PREFIX (["work", "loop"]) is the gate’s one home and the parse in status.rs is
its only reader. A supplied row whose argv does not begin with that prefix – including a
string-instead-of-array argv – is dropped, and only that row: nodes, boards, isControlNode and
the surviving rows parse unchanged. A row’s scope, level and cap are not projected into the
child at all, so the parse’s gate reads exactly id, argv, cwd and label.
The spawn roster is an allow-list of exactly four
test/arch/ui/acd-desktop-read-only-fleet.test.mjs now asserts that every argv-array spawn in the
Rust production source is one of mesh status, mesh serve, mesh ui, work loop, with work loop
admitted in writing as local process supervision. The five forbidden mutation verbs keep their
deny-list in the same file.
A held declaration and a halted loop are both visible
An operator Start/Stop is a hold keyed by declaration id and survives every tick until its row
disappears. duplicate-run is the fourth named clean exit, classified from the child’s emitted
message rather than a code token. An exit-0 child’s last non-empty line reaches the operator through
last_clean_exit, which get_view_model copies into the IPC view-model’s notice and the window
footer renders; the tray menu does not carry it. A notice is cleared per child rather than on any
child’s start.
Rust learns no completion semantics
No loop, scope, phase, run-status or declaration-as-a-decision vocabulary appears anywhere in
app/desktop/**/*.rs (comments stripped); the token loop appears only as the second element of the
admitted argv literal. A completed loop simply stops appearing in the answer, and nothing relaunches
it – reconciliation replaces restart-on-exit.
126/04 · The installer fixes the daemon environment
Login autostart is a flag pair on the existing install verb
aof mesh desktop install --autostart writes one REG_SZ value named aof-mesh-desktop under
HKCU\Software\Microsoft\Windows\CurrentVersion\Run whose data is the absolute path of
aof-mesh-desktop.exe in the resolved install dir; --no-autostart removes it. Writing twice yields
one entry and two successes, and removing an absent value succeeds. listCommands() still holds
exactly mesh:desktop-install, mesh:desktop-run and mesh:desktop-stop – there is no fourth
mesh:desktop-* command. --autostart together with --no-autostart is the coded refusal
autostart-flags-conflict, raised on the input before any act.
Every registry path goes through one injected runner
src/commands/mesh/desktop.mjs contains no un-injectable reg, spawnSync or execFile call at
all; reg is only ever an argument handed to the injected runner, the shape stop already uses for
tasklist. Both invocations carry /f, so neither can prompt a child that has no console to answer.
CI never touches a hive.
Off Windows the answer is a coded, non-zero refusal
admitAutostartPlatform admits exactly win32 and answers autostart-unsupported-platform naming
the platform for every other value – for --no-autostart as well as --autostart, since a removal
that silently succeeds where no entry could exist is the same lie in the other direction. The platform
is an argument at the act, never a process.platform read inside it, so both branches run on any
host.
--dry-run covers the whole verb
--dry-run is new on install and performs nothing – no placement and no registry write – while its
render still names both what would be installed and what would be written. The verb’s existing
artifact refusals are unchanged and still fire first, so the CLI-bijection gate can spawn this verb
without performing the act it names.
A three-check preflight both verbs report and neither repairs
PREFLIGHT_CHECKS is ["claude-authenticated", "payload-build", "workspace-identity-pinned"],
reported identically by install and by run, writing nothing on any path – no config, no minted
identity, no registry runner. claude-authenticated parses loggedIn from claude auth status’s
JSON on stdout rather than its exit code, which is 0 either way, and spawns no session.
payload-build reads the launcher’s own mode and fails an embedded launcher by name.
workspace-identity-pinned reads workspacePaths(projectRoot).configPath directly and the node id
through readSidecar over the global identity path – no walk up, no mint, no heal – so a workspace
that only resolves by path derivation reads as unpinned. A failing check refuses neither verb.
A running app is named as a running app
moveInPlace rotates a locked target to <name>.bak.<ts> before placing, the way
scripts/install-local.mjs already does, and refuses desktop-app-running naming the real cause and
the verb that fixes it if even that fails. The pre-existing install-dir-not-writable – a permissions
message for a cause that is not permissions – is gone from that path.
126/05 · The warning has one home
One import home for the SQLite runtime
src/sqlite-runtime.mjs is the only module in src/** that spells import("node:sqlite");
src/effects/journal.mjs and src/global-work-store.mjs both reach the runtime through its
importSqliteRuntime export, and neither holds a resolve body of its own.
A targeted, self-restoring warning filter
importSqliteRuntime wraps process.emitWarning for the duration of one import and restores the
original in a finally, so the filter is never in force outside that call; it swallows only a
warning whose type is ExperimentalWarning and whose message names SQLite, across all three
emitWarning shapes (message + type string, Error whose .name is the type, and the options
form), and forwards every other warning unchanged with its code and ctor intact.
The callers’ refusals are unmoved
global-work-store still throws sqlite-unavailable (501) and still honours options.sqlite ===
false; journal still degrades in its own words; each still performs its own DatabaseSync check.
A throw from the import propagates unchanged to the caller, so the original error object and its
code still reach the refusal that raises on it.
No blanket suppression in the shipped tree
src/**, bin/**, scripts/**, src/bundle/** and package.json carry no --no-warnings, no
--disable-warning, no NODE_NO_WARNINGS and no NODE_OPTIONS warning flag; the deprecation
warnings this repository wants to see still print.
126/06 · The preflight names the missing heartbeat hook
A fourth preflight check, and the count that supersedes three
PREFLIGHT_CHECKS is ["claude-authenticated", "payload-build", "workspace-identity-pinned",
"heartbeat-hook-installed"] – the new code appended LAST, so the three that shipped with 126/04
keep their order and only the count moves. Both install and run report four lines through the one
shared render.
Every workspace on this node is asked whether it can record liveness at all
heartbeat-hook-installed walks the same workspace list its sibling does and asks two questions of
each: does its .claude/settings.json register the claude-run-heartbeat bundle hook, and is the
file that registration names on disk. Both, because a registration pointing at a missing file fails
at hook time and reads as healthy from the settings document alone. The registration is found by
WALKING the document for the aofManaged marker rather than by spelling a path, so a re-nesting of
the bundle’s hook tree does not silently turn the check green.
It fails closed, and names every offender rather than the first
An unreadable settings document, a node with no identity yet, a resolver that throws, and a resolver
answering ok: false are each a fail naming the cause – never a pass. Every workspace lacking the
hook is named; a workspace carrying it is not. A node with no workspaces registered is a pass.
Its message is readable, and skips are counted
The skip list is reported as a COUNT, never enumerated. Measured on the control node 2026-09-10 with
327 skipped workspaces: this check’s message is 1,353 characters and names seven real offenders,
against workspace-identity-pinned’s 16,827 on the same data.
It reports; it repairs nothing
No aof work update is invoked, nothing is written under any project root, every read goes through
an injected seam, and a failing check refuses neither verb. The remedy – aof work update – is named
in the message.
125 · The loop graph gets a published face
The published site
https://umairs-workspace.github.io/agent-orchestration-framework/ serves a landing page and
three pages – /loops/, /prd-acd-loop-engineering/, /prd-graph-engineering/ – and the landing
page links exactly those three plus the repository.
One publishing workflow
.github/workflows/pages.yml is the second and last workflow beside release.yml; it holds two
jobs, gate and deploy, and deploy declares needs: [gate].
The gate is story 79’s control, run
gate runs node scripts/test.mjs --only test/arch/loop/acd-loop-document-current.test.mjs under
a fresh AOF_GLOBAL_HOME on pull_request and on push to main; the workflow holds no byte
comparison and no regeneration of the loop document of its own.
The deploy runs on main alone
deploy runs for a push to main or a workflow_dispatch whose ref is main, never for a pull
request; pages: write and id-token: write are granted on that job alone, the workflow’s top
level and the gate job hold contents: read; concurrency group pages-deploy with
cancel-in-progress: false.
The graph page is projected, never copied
scripts/site/build-site.mjs stages the docs/ shell plus wiki/work/loops.md and the two
planning PRDs into dist-site/ (git-ignored, .dist-site-* staging sibling too); it takes the
document’s path from loopDocumentPath, spells no basename, composes no part of the document,
writes only inside --out, refuses an --out holding a foreign file by name, swaps atomically,
and stages a byte-identical site on a second run. Its static import closure is node built-ins
only, so the deploy job stages without npm ci.
Every published page states its provenance
Each staged page carries front matter source:, kind: (generated for the loop document,
authored for the PRDs) and, for the generated page alone, regenerate: aof work loops document
--write; each body is wrapped {% raw %}…{ % endraw % } and otherwise byte-identical to its source.
Mermaid renders on Pages
docs/_layouts/default.html pins mermaid@11.4.1, promotes each code.language-mermaid node
kramdown emits into pre.mermaid before running Mermaid, and fetches the library only on a page
that carries a fence.
The README names what ships
The /aof:autonomous row names milestone 53 as where aof work loop replaced it and still says
what the command does for a run in flight; a The loop machinery section lists work loop,
work loops show|graph|validate|groundedness|document, work loop-record, work tune,
work trigger, work acceptor; the section links the published site.
The README cannot name a command that does not resolve
test/arch/command/acd-readme-names-what-ships.test.mjs extracts every aof … invocation from
README.md’s fenced blocks and command tables (never prose), resolves each against
deriveRouteTable(), keeps no route list of its own, holds a declared floor, and applies the same
control to docs/index.md.
The builder’s placement is a control
test/arch/loop/acd-site-is-projected-not-copied.test.mjs holds that story 79’s reader-set row
still reports exactly two src/ readers with the builder outside its walk, that the builder
imports loopDocumentPath and spells no basename, and that nothing under docs/ is the document’s
bytes or the builder’s output.
The workflow lints share one read boundary
test/support/workflow/workflow-lint.mjs exports readWorkflowText (CRLF normalised at the read)
and stripYamlComments; release-workflow-lint and site-build both read through it.
124 · The edges aof does not draw
The declared graph is measured, and the instrument says how much of it it read
The depends: graph is checked for whether an edge is witnessed, not only whether it resolves,
and the check states its own denominator every run – m124/00 delivers the lane, the one-home
predicate it rests on and the class ratchet that keeps it advisory. What is new at the milestone
level is the posture: an aof instrument over this stream now reports the share of its subject it
could not evaluate (182 of 230 edges) in the same breath as its findings.
Two of the three return paths reach their node; the third is dropped with a trigger
Cap exhaustion hands a unit back to the plan that produced it and the range keeps running
(m124/01); the outermost splitter recalls memory before it cuts (m124/02). The SCOPE line on the
correction return is not built: ARCHITECTURE.md#ADR-004 records that its grounding premise was
false and that the correction cycle has completed 0 times in 90 run records, and states the command
whose non-zero answer reopens it.
The parallel wave and the census read one coverage rule
src/story-contract.mjs is the single home for what a story declares and what covers what; the
wave (src/ready-wave.mjs) and the census lane (src/work/doctor-depends.mjs) both call it and
neither carries a second rule (m124/00).
The fitness lane and the citation sweep read the tree as it is
scripts/test-rubric.mjs sweeps test/arch/** recursively (m124/00), and FF-11903’s citation
sweep reads authored documents only – an item’s runs/ and observability/ subtrees are excluded,
because they persist the instruments’ own output and had jammed the ratchet at a count no document
repair could lower (m124/F-01). Its shrink-only ceiling stands at 47, re-measured at accept.
124/00 · The census reports its denominator
The contract set has one home
src/story-contract.mjs answers both questions about a story’s declared set – what it declares
(resolveDeclaredSet, returning {present, malformed, values} plus each entry’s authored directory
intent) and whether one entry covers another (contractSetCovers) – with zero project imports and
no filesystem access. Directory intent is what the author wrote: a trailing / claims a subtree, and
nothing stats the disk to second-guess it.
The parallel wave collides on coverage, not on equality
src/ready-wave.mjs asks the shared predicate both ways and holds no coverage rule of its own. A
story declaring files: [src/commands/] and a sibling declaring a file beneath it are no longer
waved together, and every pair that collided under exact-string equality still collides – the
adoption is a strict tightening, asserted over a generated corpus.
A fourth advisory doctor lane names each unwitnessed depends: edge
aof work doctor runs src/work/doctor-depends.mjs as the thirteenth CHECK_GROUPS entry. For
every resolved story→story edge whose dependent reads: shares nothing with the dependency’s
files: it emits depends-edge-unwitnessed at warn, naming both endpoints and both sets. The word
“phantom” appears in none of the lane’s codes, messages or exports.
The lane states its denominator once per run
Exactly one depends-edges-unchecked finding per run carries the two exclusion reasons separately –
an endpoint that is not a story, and a story that declared no contract – and the four counts close
as an identity over the edge set validateWork resolves. Over this stream at accept: 230 considered
= 38 witnessed + 10 unwitnessed + 125 unchecked by type + 57 unchecked as undeclared.
An advisory lane cannot gate, as a class
Every module registered in CHECK_GROUPS that exports a frozen *_FINDING_CODES array is disjoint
from CONTROL_FINDING_CODES, names no "error" severity literal and consults no acceptance horizon;
src/work/doctor-controls.mjs is the one named exemption because its array is the gate’s source.
A fifth advisory lane cannot re-introduce a gateable code without failing FF-12402.
The fitness lane sweeps the interior
scripts/test-rubric.mjs walks test/arch/** recursively and its zero-lane diagnostic reports
modules swept, names registered and names matched. A control moved into a family folder is still in
the graded lane.
124/01 · Cap exhaustion returns to the plan
One module decides cap exhaustion
src/commands/loop.mjs mints no cap-exhausted halt of its own. At its cycle-cap branch it asks
decideCycleCapExhaustion in src/work/loop.mjs, handing over the facts it already holds (ref,
type, parent, phase, cycle, cap, scope, and the plan’s re-entry count read off its own counter).
Cap exhaustion returns to the plan
A unit that exhausts its cycle cap is answered with the existing drive act aimed at its plan under
the refine phase – the same shape decideLoopPhase already returns – with the plan ref derived
from the item graph: a story’s plan is its parent milestone, a driver’s plan is itself. The range
keeps running.
The escalation is bounded twice, and nothing new was added
A unit is handed back at most once per invocation and then set aside in the walk’s in-process
Set; a plan is re-entered at most cap times, counted under the shell’s existing
${planRef}\0refine key. LOOP_STOPS is still twelve, LOOP_FIX_TRANSPORT_KEYS still nine, the
loop declaration still eight keys; no counter or persisted key arrived.
Out of scope stays terminal, and the other eleven stops are unchanged
A derived plan ref outside the loop’s declared scope halts terminally with
engine:plan-out-of-scope, naming the plan; the read-only surfaces and the other eleven members of
LOOP_STOPS return exactly as before.
The engine’s own cap is ledgered, not half-wired
nextDecision still passes no cycle at any of its six call sites; wiki/work/TECH_DEBT.md item
91 records the dead guard, its three unreachable branches, the one live entry (the direct
decideLoop call) and the repair’s estimate. Item 76 is compacted to budget with its stale line
citations corrected.
124/02 · The learning edge reaches every cut
shatter recalls before it cuts
src/bundle/commands/shatter.md step 1 runs aof work memory recall "<objective and scope
keywords>" --block once per PRD, as the product owner, before the drivers are identified – with no
--item (no ref exists yet) and no --area (a milestone-level cut is cross-cutting). An empty
recall is a proceed, and any near-miss that changed a framing is acknowledged under a heading the
driver’s own template declares.
Every cut-making command carries a recall, by roster
FF-12405 holds a named roster of the two cut-making commands (refine.md, shatter.md) asserted in
both directions, classifies every other command file with its exclusion reason, and checks each
memory invocation in the bundle token by token against MEMORY_VERBS, SCOPE_FLAGS and
parseMemoryArgv – never against prose, and never against the command registry, whose
aof work memory door is asserted to be unrouted.
The three mirrors and both hash records agree with the source
The Claude, Codex and OpenCode renders of shatter and src/bundle/manifest.json re-render from the
edited source, and .aof/aof.lock.json matches the bytes on disk for every path it names – including
the four pay-debt and run-resilience paths whose recorded hashes were stale at HEAD.
123 · The review close mints no driver
The review close’s creation authority
routeFinding returns creates: null for every combination of its declared inputs, and
routeFindings’ creates subset is empty by construction – a review close deposits no item in the
work stream.
The destination of a checklist-shaped remedy
A checklistDischargeable finding that is not cheaperThanDriver routes to
story / operator-refines / creates: null / owner: operator under every reviewed-item type
the stream admits and with no reviewed-item context at all; the one exception is a reviewed chore,
which still routes to amendment / reviewed-chore-definition-of-done / owner: loop.
The cost question’s bar
The only driver question 2 weighs a remedy against is a story the operator must refine, so the threshold below which a remedy is fixed at the close is the cost of a story rather than the cost of a chore.
The <finding_triage> instruction
The block states “The loop creates NO item.”, contains no aof work <verb> invocation, and is
carried byte-identically by all four tracked renders –
src/bundle/commands/continue.md, .claude/commands/aof/continue.md,
.codex/skills/aof-continue/SKILL.md and .opencode/commands/aof/continue.md.
m71/FF-7103’s enforcement
The control asserts the no-creation bound over the decider’s whole declared input space – 9235
combinations – rather than over an enumerated set of rows, and its prose leg reports any aof work
verb the block names rather than checking which one it names.
The two promotion faces
work:promote-finding and work:promote-gap are both registered and reachable by a person;
work:promote-finding still refuses a finding raised while reviewing a chore, and
work:promote-gap still honours --at. No routing the decider can reach leads to either.
FINDING_ROUTINGS
The export carries ["amendment", "chore", "story", "recorded", "fixed"] in its delivered order.
chore is a member the decider cannot reach.
121 · The Import Specifier Extractor Has Twenty Homes And 119 00 Added The Twentieth
One import-specifier extractor under test/
test/support/module-family.mjs’s importSpecifiers is the only extractor a suite under test/arch/**
reads: 41 files consume it, the DoD’s own grep (function (directImports|staticImports|importSpecifiers|imports)\()
finds the home, two one-line !entry.dynamic delegates (acd-controls-never-execute,
acd-command-layer-imports-downward), and the two declarers left outside test/arch/** by recorded
decision – down from twenty.
FF-11901 leg 5 is a class over test/arch/**
acd-purity-is-external.test.mjs reads every suite under test/arch/** through a shape detector (a
quoted-specifier capture after from/import/require, over blankStringLiterals), so a new own
extractor – under any name, or inline at the call site – fails CI; the five remainders sit in a
shrink-only EXTRACTOR_BASELINE, each entry carrying its file and its reason.
The home reads a backtick-quoted call specifier
A template literal with no ${…} is a literal in both call forms (importSpecifiers reports
import(`./x.mjs`) and require(`./x.mjs`)); one with a substitution stays computed
(computedDynamicImports), and the two partition the call.
Static-closure walkers keep a static contract
acd-trigger-holds-no-clock and acd-session-driver-mesh-blind read the home through
.filter((entry) => !entry.dynamic), so FF-6301, FF-6303 (“one setTimeout, in src/fs.mjs”) and
FF-5301 (“reach is exactly 71”) hold the ceilings they held before the conversion.
120 · Ff 11902 Refuses A Narrower Species Than Its Register Row Claims
FF-11902 refuses every predicate-narrowed repository walk that has no floor
acd-control-derives-its-census.test.mjs exports predicateFilteredWalks, unguardedPredicateFilters and treeWalkEqualities; at HEAD they report 60 predicate-narrowed walks, 0 unguarded, 0 silent catches, 0 retyped tree-walk equalities and 0 retyped member censuses over 1,053 controls, and the control names each carrier it finds.
A walked set is followed through set-narrowing and root-derived reads
A set bound first and narrowed later, a read handed a root-derived path (readFile(path.join(root, …)), loadLoops(…)), and a derived set asserted equal to a string-array literal are each detected as their own species; derivation is position-scoped, stops at a call, skips a test body that builds a temp directory, and does not read import(…, import.meta.url) as a root.
Every converted control asserts a floor, a declared ceiling, or named members AMONG the set
Twenty-one further controls plus the door’s own equalities carry a non-vacuity floor over the directory walked, a declared ceiling only where the number is a decision, and named members asserted among the derived set; a policy allowlist is spelled as each named member among the set with every member admitted, never as the set enumerated.
The frozen-set censuses derive from src/bundle/frozen-set.jsonc
The five sites FF-6105 cross-reads (installed set, member ids, enforcement points, deny rules) derive from frozen-set.jsonc under one declared member-count floor, FF-6105 asserts no site retypes the set, and TECH_DEBT item 81 is deleted from TECH_DEBT.md.
The detectors read every control faithfully
The walk detector is paren-aware over literal-blanked text, an expression-bodied arrow’s body is cut at its own expression, and source-slice.mjs preserves offsets across an astral character; the control asserts by name that every control is read faithfully.
119 · The tree gets an interior
Every flat layer in this repository has an interior
src/ root fell 160 → 89 modules, src/commands/ 99 → 67, and test/ and test/arch/ went 591 and
439 flat siblings to zero, across 35 and 17 subject directories. The largest directory in the test
tree is now 68. Each layer’s interior is named by the families its filenames already declared, so no
grouping is an invention.
One table meters every flat layer, and it cannot be blind to one
Before this milestone three ledger entries each measured their own directory – item 10 walked src/
root, item 63 walked test/arch/, and the fastest-growing flat directory in the tree was the one
neither could see. A single named table now carries a per-directory ceiling for all four layers,
equal to the measured count with no headroom, asserted in BOTH directions so a table naming three of
four cannot pass on the fourth (m119/01).
A cited path survives a rename, across three readers
One resolver derived from git’s own rename records, with no hand-kept redirect table, serves all
three: aof work doctor’s control probe, FF-11903’s sweep of src/ citations under wiki/work/**,
and – added at 119/03 – suite paths cited in delivered .feature files. The third reader is what
made a 1,030-suite move legal without editing 156 immutable records (m119/00, m119/03).
Nothing changed behaviour, and that is asserted rather than trusted
The assembled test registry is membership-identical across the restructure – 9,203 entries, 0 added
and 0 removed. createMeshWorkerExecutionHandler’s exported surface is identical in both directions.
No route, command name or registry key is derived from a filename. A command’s route is declared in
the command, never in its path.
Five ledger entries are discharged; two are re-measured and kept open
Items 10, 61, 63, 78, 84 are deleted from TECH_DEBT.md, taking it 78 entries / 4,082 lines to
73 / 3,634 with the shrink-only ceiling re-stamped down. Items 81 and 83 are deliberately kept
– see Gaps.
119/00 · Rule the guards that forbid the fix
Purity is a claim about EXTERNAL dependencies
A purity guard in this tree resolves its subject as a family (src/<name>/ when the directory
exists, else src/<name>.mjs), extracts every static and dynamic import specifier through the one
comment-stripping home, and classifies intra-family as admitted and everything else – bare specifier,
node builtin, or a relative path leaving the family – as a violation naming the file and the
specifier. No control under test/arch/ asserts purity by banning the import token, and the claim
is held as a CLASS over test/arch/**, so a fourteenth purity guard cannot re-introduce the ban.
A control derives its facts about the tree
No control asserts an exact equality against a set derivable from the tree – a file count, a member census, a list of importers, an import allowlist – and no sweep can pass by going vacuous: every sweep asserts its own non-vacuity, so a directory move reds the control instead of emptying it. A stored literal is admitted only as a declared bound or a policy allowlist a reader cannot compute, and each admitted literal carries its reason in its own comment.
A cited path survives a rename
src/cited-path-resolve.mjs is the single home that resolves a src/**.mjs path cited in a
delivered document – at HEAD, or through the repository’s own git rename records, with no hand-kept
redirect table. Two readers consume it: aof work doctor’s control probe and FF-11903’s sweep over
wiki/work/**. Unresolvable citations are reported by name under a shrink-only ceiling whose
constant carries the command that measured it.
control-unresolved means one thing again
aof work doctor’s control-unresolved finding reports only that a register declares a control that
does not exist. It no longer fires merely because somebody moved a file.
119/01 · src/ gets an interior
src/ has an interior named by the families its filenames already declared
src/mesh/ holds the 31 mesh-* modules and src/work/ the 40 work-* modules, and the
face-named module the spine imports has a subject-named home (src/board-worker-stream.mjs →
src/cache-read.mjs). The flat root fell from 160 modules to 89 – exactly the families’ size, with
the invariant “the root falls by exactly the family’s size” holding to the unit.
Every flat layer is a row in ONE table
test/arch/testing/acd-source-directory-budget.test.mjs carries a per-directory sibling ceiling for
src/ root, src/commands/, test/ and test/arch/, and the ceiling EQUALS the measured count with
no headroom – so a new sibling fails CI and a story that shrinks a layer must lower its row. The
claim holds in BOTH directions: every flat directory under src/ and test/ is either a row or a
declared exemption, so a table naming three of four layers cannot pass on the fourth.
No path in this tree is load-bearing for behaviour
No route, command name, lane membership, bundle target or registry ordering is derived from a
filename or a directory name. src/command-core.mjs and src/spine/face.mjs spell no
path.basename/path.dirname/path.parse-derived identifier that reaches a route, a command id or
a registry key, and COMMANDS order is a declared array order the route table reads, never a
directory listing.
119/02 · src/commands/ gets an interior, and the registry stops explaining itself twice
src/commands/ has an interior
mesh/ (17), assets/ (9) and graph/ (6) hold the families their filenames already declared, and
the flat command layer fell from 99 siblings to 67. The layer is now a row in the one budget table
that meters flat layers, which it was previously invisible to – the fastest-growing flat directory in
the tree was the one neither ledger entry could see.
The registry cites; it does not explain
No entry in src/command-core.mjs carries a rationale paragraph: each command’s registry comment is
a SINGLE line whose content is a citation in m?<itemRef>/<ID> form, and the prose lives in the
command module’s own header. The file fell from 582 lines to 323. The deferred-import comments that
document the TDZ ring are the one exempt class and are asserted still present, because deleting them
is the failure this rule must not cause.
The claim is over SHAPE, never over a count
FF-11908 asserts no comment-density number, no comment ratio and no line budget other than one per entry. A cap with no admitted decomposition is item 61’s measured failure, and this story declines the same trap over the layer item 78 names.
119/03 · The test tree gets an interior
test/ and test/arch/ have interiors
1,030 suites live in subject directories: test/ went 591 flat siblings to 0 across 35 directories,
test/arch/ 439 to 0 across 17. The largest directory is now 68.
The registry spreads an INDEX instead of growing a line per suite
scripts/test.mjs fell from 5,193 lines to 333 and names directories rather than suites; 52
index.mjs files own their own membership. The assembled registry is membership-identical across
the restructure – 9,203 entries at the base commit and at the tip, 0 added and 0 removed.
registrationDecision is untouched and still the single decider; only the text handed to it is wider.
“Registered” is TRANSITIVE, through one home
test/support/registration-surface.mjs is the single home the three readers of “is this suite
registered?” consume – the runner’s text plus every index’s – so a suite imported by its directory’s
index and spread by the runner reads as registered rather than as an orphan.
A cited SUITE path resolves at HEAD or through a recorded rename
ADR-004 is amended to a THIRD reader: test/support/cited-suite-path.mjs resolves suite paths cited
in delivered .feature files, which is what admits a move that would otherwise strand 489 citations
across 156 immutable records. FF-11903 asserts that reader.
119/04 · The mesh god-node is split
The mesh worker’s two proven seams are their own modules
src/mesh/worker-launch.mjs (launch composition) and src/mesh/worker-repo-admission.mjs (repo
admission) hold the two of item 83’s four seams that the module’s own prior extraction proved
tractable. src/mesh/worker-execution.mjs fell from 2,462 lines to 1,957.
The split SUBTRACTS
Each extracted symbol is absent from the parent as a DEFINITION and present only as a re-export, and the extracted set is DERIVED from the children rather than retyped. No extracted module imports its parent back – not directly, not through a third module – and none re-exports the parent’s names. The parent performs neither extracted concern any more: what it still calls, it imports inward.
The exported surface did not move
createMeshWorkerExecutionHandler’s exported surface is identical across the split, asserted as a set
equality in both directions against the frozen names, so the 56 dependents are untouched by
construction rather than by inspection.
The two security controls the split would have disarmed still bite
acd-worker-clone-target-scoped and acd-worker-clone-no-credential-persisted assert negatives over
the parent’s source text, so moving the clone into a sibling would have made both pass over a subject
that no longer contains what they forbid – permanently, and invisibly, since their own self-checks
run over planted strings. Both moved with the seam, and the credential control’s positive leg is now
pinned to the clone’s own argv.
SINK_CEILING fell
The sink ceiling is lowered to the post-split measured count, carries the command that produced it, and stays shrink-only with no headroom – never softened, never deleted, which is item 83’s explicit “what not to do”.
118 · Finding triage weighs what a driver costs
The triage router has a fifth routing, appended last
FINDING_ROUTINGS reads ["amendment", "chore", "story", "recorded", "fixed"] – the four prior
members unmoved in their prior order and with their prior meanings, fixed fifth and final, and no
sixth member.
A remedy cheaper than the driver that would carry it is fixed at the close
A finding carrying cheaperThanDriver: true routes to fixed with vehicle fixed-at-close,
creates: null and owner: loop; the close’s creates subset holds no entry for it, so it mints no
item and grants no review round.
The cost question decides ahead of the checklist question and behind the two that already decided
routeFinding asks the cost question after the locked-contract question and after the Nit skip, and
before the checklist question: a finding that is both cheap and checklist-dischargeable is fixed, a
finding that is cheap and requires a delivered .feature or ADR change is amendment, and a Nit that
is cheap is recorded and can still never be promoted.
The router takes the reviewed item’s type as an additive second argument
routeFinding(finding, { reviewedType }) and routeFindings(findings, context) accept the type of
the item under review as the pass’s context rather than as a field on the finding; with no context
supplied the decider returns exactly what it returned before this story, so no existing caller changes
and no existing row moves.
Reviewing a chore, a checklist-dischargeable finding folds instead of promoting
With reviewedType equal to chore, that finding routes to amendment with vehicle
reviewed-chore-definition-of-done, creating nothing; with any other type – story, milestone,
spike, uat, or none – it still routes to chore/top-level-chore and still creates a top-level
chore. The bound is read from LOOP_CREATED_ITEM_TYPE, so the one type the loop may create is the one
type it may not be promoted from.
The verb refuses the same case, before it can write anything
aof work promote-finding <chore-ref> … exits 1 with promote-finding-reviewing-a-chore (400),
naming the reviewed item as a chore and naming both destinations open to the remedy. The refusal sits
after ref resolution and before the idempotence scan, so a finding already promoted once cannot
cross the bound by having crossed it; nothing is created, nothing is renumbered and no CHORE.md is
written or amended.
The operator’s own promotion face is unbound
work:promote-gap is byte-unchanged: it still declares at in its input schema and still advertises
[--at <P>], so a person who wants the chore anyway types the gap face.
The shipped rule says five questions, and every rendered copy says the same
The <finding_triage> region of src/bundle/commands/continue.md states five ordered questions, puts
the cost question second, weighs it against a driver’s ceremony rather than a line count or a
duration, and states the depth bound with both of its destinations. The three tracked renders –
.claude/commands/aof/continue.md, .codex/skills/aof-continue/SKILL.md,
.opencode/commands/aof/continue.md – carry that region identically to the source, and every
src/bundle/manifest.json entry hashes to its re-rendered member. No occurrence of “four ordered
questions” survives in the tree.
FF-7103 asserts the two new bounds, with a probe for each
test/arch/acd-promotion-creates-one-type.test.mjs gains three legs: leg (b) reports a block that
drops the cost question, asks it after the chore question, or prices it in lines; leg (b) reports a
block that drops the depth bound or either of its destinations; leg (c) reports the verb’s type check
removed, and reports it moved to after the idempotence scan. Its routing set is derived from the
exported FINDING_ROUTINGS rather than hand-listed, so a routing added to the export is asserted
against the block with no edit to the control.
117 · The Marker S Ref Is A Placeholder So The Copied Line Still Does Not Run
The regeneration line an operator copies out of EXECUTION.md is invokable verbatim
renderExecutionDocument spells the aof-generated marker as regenerateCommand(ref), so the marker
of a record rendered for item 117 reads aof work loop-record 117 --write – the copied line runs as
copied, with nothing left for the operator to substitute.
All three faces of the spelling name the same runnable command
The marker (src/loop-record-render.mjs:237), the sign-off prose (signoffProse(ref),
src/commands/loop-record.mjs:93) and the --json face’s regenerate field
(src/commands/loop-record.mjs:342) each interpolate the item’s own ref through the single
regenerateCommand(ref) home; ref reaches the sign-off block from item.ref at the call site, and
SIGNOFF_PROSE is a per-ref function rather than a module-load frozen constant.
A call carrying no ref degrades to the placeholder rather than to a malformed line
REGENERATE_REF_PLACEHOLDER (<ref>) survives as regenerateCommand’s fallback for an absent or
blank ref, so the well-formedness chore 100 delivered holds for every caller while the ref-carrying
callers get the runnable form.
The marker’s bytes are pinned over the interpolated form
78/01’s marker byte assertion is re-pinned as “the generated marker spells the regeneration command
with the ref the verb requires” (test/loop-record-render.test.mjs), so a marker that regresses to a
literal <ref> for a ref-carrying render turns that suite red.
116 · The command layer is imported upward by loop-record-render
The shared glyph table lives below the command layer
KIND_SHAPES lives in src/loop-graph-shapes.mjs; src/commands/loops-graph.mjs imports and
re-exports it, and src/loop-record-render.mjs imports it downward. No src-root module imports
src/commands/*, and the commands/loop-record.mjs → loop-record-render.mjs → commands/loops-graph.mjs
cycle is closed.
Two contradictory controls are jointly satisfiable
78/FF-7802 (the glyph table is imported, never restated) and m42 wave (d)
(acd-command-layer-imports-downward) are both green at once, which they had not been since the
renderer landed.
115 · The Contract And Register Condensers Still Emit A Husk At A Tight Budget And Boundedfill Starves A Skeleton To Hold Room A First Entry Cannot Use
No bounded reduction is offered as one while naming none of its own entries
condensedResult (src/phase-brief.mjs:430) returns null for any reduction with total > 0 and
kept === 0, so the never-a-husk rule sits in the one place a reduction becomes a section and binds
every bounded condenser – including a sixth one – rather than the single packer path chore 95 bound.
The contract index and the fitness register carry an OPENED first entry instead of a husk
condenseTaskContracts and condenseFitnessRegister share the architecture slice’s retry through
boundedFillOrOpen (src/phase-brief.mjs:533): where no scenario or row fits the room whole, the
first is carried opened under TASKS_OPENED_FORM / FITNESS_OPENED_FORM, and the stated count says
1 of N in the form that was actually emitted.
A contract’s opening names a scenario rather than its tags
What condenseTaskContracts opens is headlineOf(scenarios[0]) – the last line of the unit – so the
room is spent on the line that names the scenario, not on the @executable tag lines above it.
The fill’s reserve is held only for a first entry that could claim it
boundedFill (src/phase-brief.mjs:466) holds optional[0].length + join back only while that is
<= room, and holds nothing otherwise, so a skeleton is no longer starved of room nothing can spend –
the defect that lost three of milestone 72’s four ADR headings, now fixed once in the helper under all
three condensers instead of worked around in one of them.
Unshippability is priced against every declared form, including the best one
assemble (src/phase-brief.mjs:1141) takes the minimum of full, floorLength and bestLength,
so a section whose condenser now DECLINES a tiny room still reaches the brief in the larger form it
does answer at, rather than being called unshippable and dropped entirely.
The invariant is swept, not sampled
test/brief-carries-the-contract.test.mjs runs green (exit 0, 0 failures) with the never-a-husk rule
asserted at every budget for every bounded condenser, alongside a lane pinning the reserve’s
could-claim-it condition.
114 · Doctor’s lane module roster omits work-doctor-loop-record.mjs
FF-5905’s roster names every doctor lane the spine imports
DOCTOR_LANE_MODULES names seven modules, matching the seven ./work-doctor-*.mjs imports in
src/work-doctor.mjs, so acd-controls-never-execute reads green and the audit/doctor boundary it
guards is measured rather than assumed.
113 · An Explicitly Named Config Path That Does Not Exist Is Silent
A config the operator NAMED and that is not there is a fault; one merely not DISCOVERED is not
configFaultFrom (src/work.mjs:216) takes the explicit discriminator loadWorkspace already
holds and returns a missing-config fault for an absent config that arrived through --config,
null for one it failed to discover by walking up from the cwd, so an unconfigured project stays a
legitimate silent state while a mistyped path does not.
ENOTDIR is absence, not unreadability
A named path whose parent is a file reaches the same missing-config fault as one whose parent is
empty, so the operator is told the file is not there rather than that it could not be read.
aof work doctor carries a third code through the finding chore 94 added
config-missing (src/commands/doctor.mjs:228) is an error finding appended at the workspace-level
edge, so no scope filters it away, and its message differs from the two present-but-unusable codes
in the one way it must: with no file to fix and no parse position to report, it names the path that
was passed and offers dropping the flag to let the config be discovered.
The load door still degrades
loadWorkspace returns {} for a named-but-missing config and never throws, so every daemon and face
that loads through it still resolves its defaults; the fault is reported alongside the load, never in
place of it.
The two halves of the discrimination cannot drift apart
test/config-fault-visible.test.mjs pins that a falsy --config reads as DISCOVERY to the fault
predicate and the path-resolution predicate alike – 14 lanes green (6 from chore 94, 8 added here) –
so a discovered path can never be reported as a named one that is missing.
112 · The worktree prepare installs the ui workspace every dispatch
The prepare step’s install breadth is a measured ruling rather than an unexamined default
scripts/prepare-worktree.mjs runs an unnarrowed npm ci, and its header carries the three-arm
measurement that says why: cache-warm medians of 15.0s (shipped), 3.4s (--workspaces=false
--include-workspace-root) and 15.0s (--omit=dev), all taken in the same detached worktree with
node_modules cleared between alternating runs.
The ui workspace is priced, and the reason its 77% is not banked is recorded beside the price
The workspace is 11.5s of the 15.0s install, and both narrowings that would drop it red the suite
every lane runs: --workspaces=false reds test/terminal-harness-drives-a-grid.test.mjs with
Could not resolve "clsx" (ui/src/lib/utils.ts:1) because the root suite esbuild-bundles real
ui/src components through test/support/react-app-harness.mjs, and --omit=dev saves nothing
measurable while removing the root’s own esbuild and ajv.
A lane that touches ui/ has a stated answer, and it is “nothing special”
The workspace the lane needs is already installed by the shipped step, so no lane carries a conditional install, and the header states that narrowing would have inverted the cost – every lane paying a red suite so the ui lanes could skip an install.
The ~6 minutes that raised the finding is attributed to a cold cache, not to per-dispatch cost
A cache-warm install in this tree is 15 seconds, so the figure the finding was raised on is a once-per-machine cold-cache cost; the prepare step carries no six-minute per-dispatch charge.
The junction inside a prepared worktree is on the record, with its remover census
npm ci creates node_modules/@aof/ui as a Windows junction with an absolute intra-tree target
(<worktree>/ui), and the header records the probe: node fs.rm({recursive}), rm -rf, PowerShell
Remove-Item -Recurse -Force and cmd rmdir /s /q all unlink the reparse point and leave an
out-of-tree canary intact, while git worktree remove --force (git 2.47.0.windows.1) traverses it and
empties the canary – so the remover 72/ADR-007 §3 blesses is the one remover that follows a junction,
and §3 stays safe here because the target is intra-tree rather than because junctions are harmless.
72/ADR-007 §2 reads as a claim about aof’s own code, at the declaration a reader meets first
The header states that “aof creates no symlink or junction whose path lies inside a worktree, ever”
is true as written and is censused over src/ by FF-7207, and is NOT the claim that a prepared
worktree contains no junction – one does, on every dispatch, put there by this script’s own npm ci.
ADR-007 is unedited; the precise reading lives beside the declaration.
111 · FF-5308’s necessity leg is red, and it is the good news it says it is
LOOP_SCOPE_FORMS is a ratified freeze rather than an unruled default
src/work-loop.mjs exports exactly the two frozen forms driver (/^\d+$/) and range
(/^\d+-\d+$/), and 53_milestone_loop-artifact/ARCHITECTURE.md ADR-017 carries the argument that
holds after TECH_DEBT item 49’s payment, so the pair rests on stated grounds rather than on the
fall-through defect ADR-003 argued from.
The scope vocabulary is recorded as the admission grammar of four unattended launch surfaces
ADR-017 names commands/loop.mjs:814, commands/trigger.mjs:310, work-trigger/declaration.mjs:211
and mesh-assignment-directive.mjs:128 – the last dispatching to a remote worker – so widening the
vocabulary is a change to four doors plus the engine’s invocation, declaration and resume doors,
never to one regex.
No surface in the stream still points at a widening that will not happen
FF-5308’s register row and its State now cell, and the item-49 codebase-health note, are amended in
place in 53/ARCHITECTURE.md to the file’s own dated convention; wiki/work/TECH_DEBT.md carries no
copy, so those two plus ADR-017 are the whole set and a reader of any of them finds the ruling instead
of the stale follow-on.
FF-5308’s register row describes the leg that actually ships
The row’s necessity-leg text states the inverted claim delivered at milestone 96’s gate – a
story-shaped scope resolves to its own item and reaches no earlier milestone’s competitor – so the
register and test/arch/acd-loop-scope-guard.test.mjs (6/6 green) now make the same claim, and a red
on FF-5308 is a regression rather than the designed good news.
110 · The Promotion Family Has Two Listitems Reads The Cache Read Surface Gate Classifies Neither Way
Every promotion-family disk read carries an ADR-005 category
The acd-cache-read-surface-boundary gate names all three reads the promotion family makes:
appendPosition and findPromotedChore are pinned (c) STRUCTURAL against src/work-promote/promotion.mjs,
and runPromoteFindingToChore’s ref-resolution scan is an (a) CONTROL_SIDE entry – no promotion read
is invisible to the gate in either direction.
The reviewed ref resolves cache-first
runPromoteFindingToChore reads the stream through listItemsCacheFirst(ctx.workspace) rather than
listItems(workDir), so a finding raised while reviewing a worker-authored item resolves on control
instead of being refused as promote-finding-unknown-ref, and the depth bound is reached for a ref
this node’s disk has never held.
A CONTROL_SIDE entry fails on relocation instead of passing green
Every entry in the (a) list is { file, subject }, and the ARMED leg reports a straggler when the
module is gone or no longer declares that subject – so a module the migrated read has LEFT, or one
deleted outright, no longer satisfies the absence for free. Proven non-vacuous by a planted
runPromoteFindingToChoreMOVED subject, which trips the leg.
promote-finding-to-chore.mjs imports nothing from work.mjs
src/commands/promote-finding-to-chore.mjs holds no import of src/work.mjs, one fewer importer of
the 37-module god-node named by m41/ADR-001.
109 · Tech Debt Item 49 Still Records The Nextwork Fail Open As Open Though Story 86 Closed It
TECH_DEBT item 49 prices only the half that is still owed
The entry reads open – half paid by story 86 at severity low, and its “What’s wrong” states
that nextWork’s inRange refuses an unparseable story-grained shape (invalid-scope, 400) and
scopes NN/SS to that one story – so an operator scheduling from the ledger prices the surviving
duplication, not the fail-open that no longer exists.
Item 49’s cited line numbers resolve at their current homes
The three citations point at the code they name – src/work.mjs:1237-1273 (inRange),
src/work.mjs:1037-1044 (validateWork’s inScope copy) and src/work-doctor.mjs:757-759
(the leaf-backed inScope) – and the entry carries story 86’s FF-5301 reach-ceiling reason for why
folding both callers onto src/work-ref-scope.mjs was not taken, so the remaining half names the
condition that unblocks it.
108 · The Rendered Bundle Is Stale For Verify And Assimilate Code So This Repo Runs The Pre Story Prompts
This repo’s own installed bundle is re-renderable on a Windows checkout
.claude/**, .codex/** and .opencode/** are pinned text eol=lf, so on a core.autocrlf=true
checkout all 144 installed members classify keep and none classify drift-warning – aof work update
re-renders without --force, where before it could render none of them.
The installed bundle is current with its src/bundle/ source
Every rendered member across the three runtime trees is byte-identical to the source it renders from,
including aof-architect.md – the member that was actually stale, not verify.md or assimilate-code.md.
The install-tree EOL pin is guarded, and the guard can fail
acd-bundle-install-eol-pinned resolves every render target across all three runtimes through
git check-attr and asserts eol=lf, with a non-vacuity control outside the trees reporting
unspecified; it is registered in the scripts/test.mjs array, so it runs in the suite.
107 · Acd Cache Read Surface Boundary Is Red On A Clean Tree Its Pinned Reader Promote Gap To Chore Mjs No Longer Declares Defaultat
The cache-read-surface boundary is green on a clean tree
acd-cache-read-surface-boundary passes 5/5 with no standing red, so a failure in that suite is now
a signal rather than known noise a reviewer reads past.
The promotion engine’s disk reads are pinned at their real home
Both of src/work-promote/promotion.mjs’s structural reads – appendPosition and
findPromotedChore – are pinned STRUCTURAL subjects on listItems, and promote-gap-to-chore.mjs
imports no disk reader of its own, so both promotion faces are guarded through the one engine.
106 · Ff 5905 S Doctor Lane Module List Is Red On A Clean Tree And 66 S Fold The Family Ratchet Has Been Passed Twice Unfolded
66’s fold-the-family ratchet is answered, and the answer is a refusal with a price
wiki/work/TECH_DEBT.md item 10 records the ratchet as SUPERSEDED with
the measurement behind it – the fold buys 8 of 158 root modules (5.1%) and strands 214
src/work-doctor-*.mjs citations across 87 wiki/ documents that no gate in this tree resolves –
so an eighth doctor lane arrives at a decided gate instead of an unanswered one.
The doctor family’s growth is governed by FF-5905’s named roster, not by its placement
The three rules that replace the ratchet are recorded in the same item 10 amendment – placement is
decided at birth, this family’s growth is governed by the named roster in
test/arch/acd-controls-never-execute.test.mjs,
and the root count stays item 10’s own src/mesh/ + src/work/ partition – so the control that
actually caught the seventh lane is the one the tree relies on.
Every in-source citation of the ratchet resolves to the refusal
src/work-doctor-controls.mjs,
src/work-doctor.mjs (the controlsLane entry in CHECK_GROUPS) and
src/commands/audit.mjs each state the ratchet as superseded and name
where the refusal lives, and each records that 66’s “4 → 5” was mismeasured at birth – 54/04’s
rubric lane already made the roster six – so no reader of the doctor family meets a live gate that
was never untripped.
105 · Outcome Md Is Not In The Streamed Requestable Artifact Set So The Mesh Cache Can Never Answer For It
OUTCOME.md is a streamed and requestable artifact
WORK_ITEM_ARTIFACTS carries { name: "OUTCOME", file: "OUTCOME.md" } between VERIFICATION and
RETROSPECTIVE (src/work-artifacts.mjs:41), so a worker streams a built worktree’s OUTCOME.md
alongside its other records and work:doc OUTCOME resolves a document that exists on disk instead
of refusing it as a coded invalid-doc.
The derived view widened without an edit
WORK_ITEM_DOC_FILES names nine documents from that one definition rather than a second literal
list, and all seven cases of acd-work-artifact-set-single-home – including ADR-013/C9’s clause
that the derived view EQUALS the manifest’s file-kind entries in manifest order – pass untouched.
The drain suite is sized off the manifest, not off a copy of it
test/artifact-sync-drain.test.mjs derives its seed and both of its counts from the fixture’s
RECORD_DOCS, which derives from the manifest, so a suite that was self-consistently green while
asserting over a smaller set than the code streams now reds when the two diverge.
104 · The Coherence Lane Is The Third Reader That Was Missed
One exported home for “which numbers a depends: edge may name”
isDependTarget is exported from src/work.mjs and re-exported through the doctor spine at src/work-doctor.mjs, so every reader of the naming question reaches the same predicate instead of holding a module-local copy.
Doctor’s coherence lane resolves parentless stories
src/work-doctor-coherence.mjs builds driverStatusByNumber and gates depends-blocked-in-progress (:238) on isDependTarget, so a driver whose depends: names a done top-level story reports no unmet dependency, and an in-progress parentless story is itself judged for working ahead.
numbering-gap counts a top-level story’s number as filled
src/work-doctor-freshness.mjs resolves the number sequence over isDependTarget: on the real stream the check reports 42 alone – the genuinely item-less 42_structural-overhaul/ – where it previously reported fifteen existing story folders as absences.
duplicate-driver-number sees a milestone/story collision
src/work-doctor.mjs resolves over isDependTarget, so a milestone and a top-level story sharing a number are reported as the ambiguous findWork/nextWork resolution the check’s own message names.
isDriver still answers the phase question alone
The predicate at src/work.mjs and its spine mirror are byte-unchanged, and nextWork’s driver walk still filters on it – readiness and scheduling are unaffected by the widening.
103 · Doctor Reports Health Over A Stream It Cannot See
Depth-independent workspace resolution
findProjectConfig walks UP from the directory it is handed, so work.dir resolves against the
directory aof.config.json was found in rather than process.cwd() – every reader of a workspace
path answers identically from the repo root, from src/, and from a work-item folder.
Bounded walk
The upward walk stops at aof’s own config home (both the configured AOF_GLOBAL_HOME and the
conventional ~/.aof) and at an ancestor .aof state dir, so a temp-dir fixture never adopts the
operator’s real home and a mesh worktree resolves itself rather than the origin it was materialised
inside.
An empty scan is a refusal, not a pass
aof work doctor over zero items emits an error: empty-stream finding naming the directory it
scanned and exits non-zero; an empty result set and a clean result set no longer render identically.
The cwd-independence regression is pinned
test/doctor-cwd-independence.test.mjs drives the real CLI with a real cwd across five cases, and
asserts the finding set is non-empty before asserting equality across directories.
102 · The declaration names its loop
The loop declaration envelope carries a registry id
buildLoopDeclaration returns eight keys – loopRunId, scope, level, cap, phase, cycle,
startedAt, id – with the original seven unmoved in their original order and id appended last,
and it refuses to build an envelope at all when id is absent, empty or not a string.
The refusal vocabulary has a sixth member
LOOP_REFUSALS reads loop-scope-unsupported, loop-level-locked, loop-level-gate, loop-level-unknown,
loop-bound-unresolved, loop-id-missing – the five prior codes in their prior order, the new one sixth
and last, and the earlier guards still decide first.
The loop shell declares the one loop it is
src/commands/loop.mjs exports SHELL_LOOP_ID, the constant loop:autonomous-cascade, and hands it
to every declaration it mints – the same id across every phase, cycle and resume of an engagement, and
never derived from the invocation.
The shell reads no registry at run time
A tree with no .aof/loops/ directory runs the loop shell unchanged; an id the registry does not
declare is reported by 78’s projection as ran-undeclared and is never refused mid-run.
The shell’s id is pinned to the shipped registry by a check, not by a second literal
test/arch/acd-shell-loop-id-is-declared.test.mjs fails when the record declaring SHELL_LOOP_ID in
src/bundle/loops/ is re-pointed, naming both the id the shell mints and the ids the registry
declares; it takes the shipped records through test/support/registry-fixture.mjs, the one helper
that copies them.
A run minted before this change still resumes
The resume reader recovers exactly five keys from a stored declaration, so a seven-key run record written before the eighth key existed resumes unchanged.
78’s projection joins for real, end to end
test/loop-declaration-join.test.mjs drives the real four in series – producer → the run store’s mint
verb → readRuns → projectExecution – with no brief.loop literal hand-written anywhere: a
carrying run reports ratio 1 and takes its loop off declared-never-ran, an undeclared id reports
ran-undeclared, an empty-brief run is counted rather than dropped, and the store’s frozen sixteen
keys are unmoved.
101 · Committed Generated Markdown Under Wiki Work Has No Eol Pin
The committed loop document is byte-stable across platforms
wiki/work/loops.md is pinned text eol=lf in .gitattributes:86, so it checks out LF on every
platform and acd-loop-document-current compares the committed bytes against the composer’s LF
output rather than against a core.autocrlf=true checkout’s CRLF.
The eol pin carries its own guard
test/arch/acd-loop-document-eol-pinned.test.mjs asserts through git check-attr that
wiki/work/loops.md answers eol=lf, so deleting the .gitattributes rule now reds on every
platform instead of only on a Windows checkout.
The pin is path-scoped, and the scoping is asserted
The rule covers the one generated projection, not a wiki/work/**/*.md glob: the hand-authored
record docs under the work directory still answer unspecified, including
wiki/work/26_.../SPEC.md, which is acd-runs-eol-pinned’s non-vacuity control – and that
scoping is itself an assertion, so a widening of the rule reds rather than silently making that
proof vacuous.
100 · The Generated Marker Names A Command With No Ref
Invokable regeneration marker on every generated EXECUTION.md
REGENERATE_COMMAND in src/loop-record-render.mjs is aof work loop-record <ref> --write, so the marker line stamped into every rendered EXECUTION.md names the route the verb actually accepts, and 78/01’s byte assertions pin that exact spelling.
99 · aof’s own exemption ledger reddens a governed project’s audit – TECH_DEBT 72’s species, one lane over
Exemption ledger scoped to the project it describes
runCensus’s baseline defaults to null and resolves to UNREGISTERED_BASELINE only when
ledgerApplies(repoRoot) holds, so aof work audit over any project that is not aof reports no
audit-baseline-stale finding for aof’s own suites.
Project identity, not install directory, decides the ledger
ledgerApplies matches the subject against LEDGER_PROJECT by directory identity with the install
or by the subject’s own manifest name, so a payload install at ~/.aof/bin – which ships src/ and
no test/ – still applies the ledger when it audits aof’s own repository.
isToolkitRoot as the toolkit’s own root predicate
src/work-audit/toolkit.mjs exports isToolkitRoot(subjectRoot, toolkit), a pure predicate deciding
by path.relative, so drive-letter and separator comparison is Node’s single answer rather than each
caller’s.
A named baseline is honoured as given
A caller that passes baseline to runCensus receives exactly that list regardless of the subject
root, so the injection seam the audit lane’s tests drive is unchanged.
98 · The Reference Corpus, Re-Fetched And Confirmed At Source
Harness reference corpus confirmed at source
All six rows of src/harness-reference.mjs carry checked: "2026-09-04", the date each row’s value
was reached at its cited source, and wiki/reference/harness-baselines.md was regenerated from that
same run.
Every corpus row cites a source that states its number outright
langchain-agent-executor-max-iterations and openai-agents-runner-max-turns now cite the upstream
declarations (langchain_classic/agents/agent.py, agents/run_config.py) that carry 15 and 10
in the response body, replacing two client-rendered doc pages whose bodies never contained the value.
97 · validate refuses a story’s honest forward reference
A reads: entry may name a sibling story’s declared write
aof work validate accepts a story reads: path that does not exist yet when some story under the same milestone claims it in its own files:, and still reports story reads path "<p>" does not exist for a path no story claims.
The forward-reference exemption is claim-bounded, not existence-bounded
The claim set is the milestone’s own story files: declarations, matched case-sensitively on the resolved project path, so a wrong-case or cross-milestone read borrows no claim and stays a finding.
The two stories that had to under-declare now declare in full
62/04 carries all five src/work-tune/*.mjs modules in its reads: and 77/05 carries src/work-audit/declared-bounds.mjs and src/work-audit/toolkit.mjs unedited, and the work stream validates PASS.
96 · The declaration earns its keep
A milestone cannot be accepted on story-scoped greens
aof work status <NN> done refuses a milestone with no green whole-tree row beside its records, in
the command layer, with --gate-override "<reason>" the only way past and the reason written into
REGRESSION.md as its own row. The refusal is scoped to type: milestone; a story’s done is
untouched. Stated whole by 96/04.
The gate’s verdict is a document, and its reds are history
Every gate run appends to the accepting item’s own REGRESSION.md – never under runs/ or
observability/ – carrying the commit, the instant, the scope and what failed. A rerun appends, so
the newest row is what the door reads and the earlier rows stay readable. 96’s own record carries
five reds before its green.
The read/write declaration is proposed from the graph, not recalled
src/story-contract-derive.mjs proposes reads:/files: from the codebase graph’s imports and call
sites plus the contract’s own citations, every entry carrying a reason from a closed exported set. It
reaches the graph only through normalizeGraph/computeImpact, holds no write path to any
STORY.md, and answers an absent or unreadable graph with a citation-only proposal that says so.
A story’s test run is selected from its declared write set
aof test --scope impacted --story <ref> derives the changed set from files: through the existing
selectSuites, adding a second changed-set source beside the git one rather than a second selector. A
declared path the graph has not seen WIDENS the run under an existing reason and is never dropped.
The phase commands mint a run record that observe can join on
aof work run-start writes a record under the item’s own runs/ carrying itemRef and the session
id when one resolves, and null when it does not – ambiguity is absence, never a guess. The
transcript→item join stays the sessionId join and gains no second path.
The declaration is checked against what a bundle change actually lands
A story declaring a src/bundle/ member must also declare src/bundle/manifest.json and every
git-tracked render of that member. 96’s five stories were short by 23 such entries and now declare
them.
95 · The brief compiler sacrifices sections with budget unspent on a long story record, and condenses a long ARCHITECTURE to a husk
A sacrifice in a compiled phase brief is a true claim
A section named SACRIFICED in a phase brief is one that could not be carried in the room the
finished brief has left: after the bottom-up shedding, the room it freed is offered back
highest-priority first, the survivors are re-planned so the bounded condensers hold the shares the
departure changed, and any slack a granular fill strands is spent before the claim stands. No brief in
this stream now gives a section up with more than a tenth of its ceiling unspent.
The retained set of a brief is no longer a prefix of the declared priority order
A brief may carry tasks, fitness and dependencies while objective, which outranks all three,
is the section given up – the outcome on every one of milestone 72’s story refine briefs. The
priority order still decides who pays when two sections compete for the same room; it no longer
decides membership on its own.
Every story that declares a dependency edge receives it
All 58 real stories in this stream declaring a non-empty depends: carry a DEPENDENCIES section in
their refine brief. 72/02 and 72/04 were the last two without one.
The architecture slice never arrives as a husk
When no decision passage fits the room whole, the slice carries the first one OPENED – its heading and as many leading lines as the room holds, ellipsed – under a second declared form that says the passage was opened rather than listed, so the count beside it stays honest. Every declared ADR’s heading survives with it. No condensed section anywhere in this stream is now counted as carried while naming none of its own entries.
94 · A Malformed Aof Config Reads As An Empty One So Every Optional Declared Step Silently Does Not Run
A present-but-unreadable config is distinguishable from an absent one
loadWorkspace answers configFault – { path, code, message } when .aof/aof.config.json is present and does not parse or cannot be read, and null both when it parses and when there is no config at all – while still degrading to { config: {} } so no door ever dies on a torn config.
A torn config is a reported health fault, not silence
aof work doctor emits an error finding (config-unparseable / config-unreadable) naming the file, the parse error and what is consequently not running; it is appended as a workspace fact, so no scope filters it away.
readJson carries one vocabulary for a parse failure
A parse failure from readJson carries code: "malformed-json" – the word config-inspect.mjs and the board face already use – while the read leg keeps node’s own ENOENT.
93 · Ff 5809 Leaves Acd Prompt Bounds Name Their Home Unclassified In The Registry Reach Lane
FF-5809’s registry-reach taxonomy classifies every suite that reaches the shipped loops directory
test/arch/acd-prompt-bounds-name-their-home.test.mjs (FF-7101) sits in lane 3
(READS_WITHOUT_COPYING) of test/arch/acd-registry-fixture-closed.test.mjs with its reason
recorded beside it – it reads shipped src/bundle/** record text in place and copies no subset into
a temp registry – so the classification lane is green and no file reaching src/bundle/loops/ is
unclassified.
The red four milestone gates carried as inherited is closed at its source
FF-5809 is green on its own control run rather than named as an inherited red with a home, so gates 63, 71, 72 and 77’s carried finding has a verified discharge instead of a standing exemption.
92 · Ff 6109 S Harness Switch Lane Reds On The Shipped Declared Document
FF-6109’s harness-switch lane, evaluated over the shipped harness document
The lane derives its named/unnamed partition from src/bundle/commands/continue.md’s own text rather
than from a key list held in the control, so the fail-closed ground lifts for a knob the day the prompt
names it with nothing in src/work-acceptor/admissibility.mjs edited – and the lane is green at HEAD
over the document as shipped.
91 · The Cache Read Surface Pin Points At A Defaultat That Promote Gap To Chore Mjs No Longer Declares
A resolving subject anchor on ADR-016/G2’s structural-read pin
test/arch/acd-cache-read-surface-boundary.test.mjs’s STRUCTURAL entry pins the promotion family’s
append-position disk read at src/work-promote/promotion.mjs’s appendPosition (via listItems)
rather than at a defaultAt no module under src/ declares any more, so the assertPinned lane is
green on a subject that exists and answers for both faces of the promotion engine at once.
90 · This Repo Cannot Declare Work Worktree Prepare Without A Prepare Script No Story Owns
Declared worktree preparation for this repo
.aof/aof.config.json declares work.worktree.prepare as node scripts/prepare-worktree.mjs
(deadline 900000 ms), so every dispatch worktree materialised here is installed by the declared step
rather than by hand, and the declaration compiles under ADR-001’s no-shell rule because its program is
a real executable rather than a package-manager .cmd shim.
89 · FF-7106 is declared in milestone 71’s fitness register but owned by no story
A story that lands a bundle member is held to its whole write set, in CI
test/arch/acd-declared-writes-include-generated-siblings.test.mjs is on disk and registered in
scripts/test.mjs. Over every STORY.md under wiki/work whose item is open, a files: entry that
is a bundle member obliges src/bundle/manifest.json and every git-tracked rendered output of that
member; each omission is reported naming the story and the missing sibling. Milestone 71’s register
declares six controls and all six now resolve – aof work doctor 71 reports no control-unresolved
at either severity.
The member→render mapping is asked of the render engine, not pattern-matched
Each member’s outputs are obtained by rendering that member alone, so no runtime’s id-mapping
convention is encoded here. This is load-bearing rather than stylistic: the codex render of
src/bundle/commands/continue.md presents as skill:aof-continue, not command:continue, so a
control keyed on an output’s own resource id loses .codex/skills/aof-continue/SKILL.md – one of the
three tracked files commit 231ee134 moved as one – and would pass the story this control exists to
catch. The union of per-member renders equals the whole-bundle render exactly (143 = 143), and that
equality is asserted, so an unattributable member fails here rather than hiding.
Membership, the runtime set and the tracked set are all derived
Membership comes from loadBundle()’s descriptor, never a src/bundle/ prefix: frozen-set.jsonc
is a member that renders to .aof/frozen-set.jsonc, while manifest.json and bundle.json sit
under the same prefix and are machinery that render to nothing. The runtimes handed to the render are
the union of what the members themselves declare, so a fourth runtime is covered with no edit to the
file (renderBundleOutputs defaults to ["claude"] alone, so passing the derived set is load-bearing).
The demanded renders are intersected with git ls-files, so a render the repository does not keep is
never asked for.
The control is non-vacuous by construction, and green over the stream today
The decision is a pure function over story records; the live leg feeds it the real tree and four
further legs feed it synthetic ones. Measured at landing: 276 STORY.md walked, 84 member source
paths, 143 tracked renders, 9 open stories of which 0 declare a bundle member – so the live leg
has no subject today and the plants are what keep the control falsifiable. The scan’s own reach is
asserted separately from the existence of a subject.
The horizon is read through the one predicate, and the read itself is pinned
isOpen is imported from src/acceptance-horizon.mjs rather than re-derived from the literal
"done" (FF-6602). Every STORY.md here is checked out CRLF, and a status read that keeps the \r
hands isOpen a word it does not recognise, which fails OPEN and puts every accepted story back in
scope – measured, that is 33 violations across four done stories (59/04, 61/01, 83, 84). The
contract “a done record reads done in both checkouts” is asserted directly, so a rewrite to the
obvious line.slice("status: ".length) shape fails here.
88 · Three arch tests are red from 71/00 and 71/01, both already done
The four controls 71/00 and 71/01 moved out from under are green at their new subjects
acd-cache-read-surface-boundary, acd-registry-fixture-closed (FF-5809),
acd-progress-ledger-consumed (FF-6109) and the 61/03 task 02 outline row in
acceptor-admissibility.test.mjs pass with no gate deleted,
relaxed or carved out – the cache-read pin resolves at
promotion.mjs’s appendPosition() where 71/01’s engine extraction
took it, and every arch test file is classified into one of FF-5809’s four lanes.
Two of the four now derive their subject from the tree instead of pinning a snapshot of it
FF-6109’s harness leg reads its refused/considered partition off the harness document’s own text, and
the 61/03 task 02 row asserts the harness ground applies iff the harness of record does not name
the key – so the next config key commands/continue.md names moves sides with nothing edited, rather
than reddening a control that was only ever recording how many knobs the prompt happened to name.
87 · The test-isolation guard stops shipping
The shipped frozen set carries five members, and none of them is repo-specific policy
The declaration the framework ships names locked-contract, litmus, tag-vocabulary, gate-order
and anchors; no member protects a global store from unisolated suite invocations, and no member
names the tool-call hook enforcement point.
The bundle installs no test-isolation guard
Neither src/bundle/bundle.json nor the generated src/bundle/manifest.json targets a guard in a
consumer’s hook directory, src/bundle/hooks/ holds no such body, and the asset census is re-measured
to the smaller tree as an exact 79-file set-equality in both directions.
A consumer’s own build commands are the consumer’s own business
A fresh aof work init plants no framework-authored entry that judges a command before it runs; an
already-installed consumer has that entry retracted by the next aof work update, with the operator’s
own entries at the same event surviving in their own positions and no other compiled member’s output
moved.
55/FF-5505 is enforced against the declaration rather than against a census of what shipped
The control asserts that every rule produced at every enforcement point carries its declaring member’s id, that a point no member names compiles to nothing and is reported as unnamed rather than as satisfied, and that a declared member reaching no enforcement point fails – so it binds the next hook member anyone declares without being re-armed, and refuses the vacuously-true repair over an empty set.
The compiler’s hook path is exercised by a member built for the purpose
The marker, the surgical splice into a co-authored settings file, the retraction when the member leaves and the coded tamper are all driven through a synthetic hook-shaped member in the test, so the coverage sits with the compiler that owns the behaviour rather than with whatever the bundle happens to ship.
This repository runs the compiled predicate as its own hand-owned hook
.claude/hooks/aof/guard-test-isolation.mjs here is the segment- and token-aware predicate – it
strips here-documents, splits into segments, tokenises each, and clears on an isolation prefix in any
of its spellings – invoked from a PreToolUse entry carrying no aofManaged marker, so aof neither
adopts, edits, retracts nor drift-reports it. Its cases are re-homed onto
test/repo-test-isolation-guard.test.mjs and registered in the repo suite.
55’s pre-55-hook gap is discharged, by the route actually taken
55’s ledger entry reads discharged (story 87, 2026-08-27) and records that its written discharge
condition – an update replacing the file with the compiled member – was withdrawn with the member, and
that the file was replaced by hand instead.
86 · The span vocabulary, completed
A story-grained scope that cannot be parsed is refused
aof work next throws invalid-scope (400) for any scope matching ^\d+/ that is not a story ref or
a span – 44/01-03x, 44/, 44/01-, 44/01-02-03 and the en-dashed 44/01–02 each name the
admitted forms in the refusal instead of silently walking the whole stream.
find and next answer for the same vocabulary
The two surfaces agree about every story-grained ref: what findWork resolves, nextWork scopes to,
and what findWork answers [] for, nextWork refuses – there is no shape one admits and the other
widens.
A bare story ref is a scope
NN/SS is the one-story span NN/SS-SS: aof work next 44/01 answers with story 44/01 alone, offers
neither another milestone’s stories nor milestone 44 itself for acceptance, and parseStorySpan admits
the bare form through the same rule as the span rather than through a second narrowing beside it.
A free-text scope still falls through
A slug scope is unaffected by the refusal, which reaches exactly ^\d+/ – and the driver form 44,
the driver range 44-46, the span 44/01-03 and the descending span 44/03-01 (which parses and
admits nothing) all answer exactly as they did before.
The next command face resolves a span to the one driver it names
A finished span answers done rather than naming an unrelated held driver; a span whose own driver is
held still reports that driver and names its holder; and a driver range keeps its documented
over-reporting unchanged.
parseStorySpan is src/work.mjs’s exported, single scope parser
The span shape is defined once and read by three surfaces – findWork, inRange/inSpan, and
skippedEntries in src/commands/next.mjs – rather than re-derived in the command face.
The shipped continue.md span branch is pinned by a test
test/work-story-span-scope.test.mjs asserts that the shipped prompt dispatches on a story span,
scopes the walk to the span rather than to the bare milestone, and states each of the branch’s three
obligations; the pin is non-vacuous – the same assertions applied to a copy with the branch cut out
throw.
85 · Records follow the story
Both accepting doors instruct both story records
aof:verify and aof:assimilate-code each instruct the session to instantiate OUTCOME.md from
.aof/templates/work/shared/OUTCOME.md and to author a RETROSPECTIVE.md in the story’s own folder,
nested or standalone, over the same delivering-type partition.
The one-writer rule is role-derived rather than filename-pinned
test/verify-outcome-per-type.test.mjs admits the commands that ACCEPT an item and refuses every
agent prompt, and asserts its own non-vacuity by requiring that the accepting set is exactly
verify + assimilate-code and that both match the rule the others must not.
The spike and uat exclusions travel with the authoring permission
Each door names spike and uat as types that carry no OUTCOME.md and states the reason for each,
so neither omission reads as an oversight from either door.
A delivered story missing its records is reported by name
aof work doctor emits story-record-missing (warn) for any story at status: done whose own
folder carries no OUTCOME.md or no RETROSPECTIVE.md, naming which record is absent; it judges a
story on its own folder, so no record above it in a milestone satisfies it, and it raises nothing
against a non-story item or against a story at any pre-done status.
The new check cannot reach a gate
story-record-missing is disjoint from CONTROL_FINDING_CODES and is emitted only at warn, so no
severity change to it can fail aof work validate or the loop’s doctor rung. Measured on its first
real run over this stream: 278 findings, none of them an error.
The check is a pure function of the snapshot
It lives in lifecycleCompletenessGroup (src/work-doctor-coherence.mjs) and answers over a literal
snapshot with no filesystem and no clock, yielding byte-identical findings across runs and across
processes. It reads the disk that is the subject of its operation rather than the mesh cache, because
OUTCOME.md is not in the streamed artifact set and the cache answers for it 0 times out of 277.
The record-completeness backlog is measured and visible
The stream reports 278 done stories carrying no OUTCOME.md and/or no RETROSPECTIVE.md, up from
an unreported condition; 81, 84 and 85 are the stories that satisfy the rule.
84 · A story span is a ref
The story-span ref form
NN/MM-PP is an admitted work-item ref: aof work find 44/01-03 resolves it to the story rows
44/01, 44/02 and 44/03, ordered by story number rather than by directory listing order, and a span
that names no existing story answers with the empty set at exit 1 on the human face and [] at exit
0 on --json.
A span-scoped walk
aof work next 44/01-03 narrows the walk to driver 44 and to the stories 01..03 within it, and
answers with the same readySet/wave/heldSet shape every other scope form answers with. The
milestone’s own dependency answer is unchanged by the narrowing: an in-span story that depends on an
out-of-span sibling is reported blocked naming that sibling as a full ref.
A span never offers its milestone
No driver-grained offer reaches a span-scoped answer. Every in-span story being done answers
{ state: "done" } rather than offering the milestone for acceptance; a uat session, spike or chore
addressed by a span offers nothing rather than itself; and an un-broken-down milestone addressed by a
span offers nothing rather than “needs break-down”. A driver-level depends block still reports
blocked at the milestone.
A continue lane for a named subset
aof:continue dispatches on a span as well as on an item type. Its span branch reuses the milestone
lane and states three differences from it: the walk is scoped to the span and never widened to the
bare milestone mid-walk, an out-of-span dependency is reported and stops the walk rather than being
built, and the milestone’s own status: is neither moved nor accepted.
83 · The agent layer’s four bounds
A reporting bar on every reviewer lens
Each of the five reviewer agents (aof-architect, aof-qa, aof-designer, aof-security,
aof-compliance) ships a reporting bar in its own prompt – a >80% confidence floor, a four-gate
evidence checklist, Blocker/Important/Nit severities, an explicit do-not-flag list, and the statement
that a clean review is a valid review.
A declared context contract on the story record
A story record carries reads: and files: frontmatter lists, the authoring prompts populate them, and
aof work validate reports a malformed declaration, a missing read path, a drifting anchor, a
backslash-spelled path, a section anchor in files:, an entry escaping the project root, and a
both-present-and-empty pair as an unauthored scaffold. An absent declaration stays valid; an unwritten
files: entry stays valid.
A build wave partitioned by declared writes, in code
aof work next --json returns wave and heldSet beside readySet, computed by
partitionReadySetByDeclaredFiles as a greedy stable walk with case-folded collision keys. An unknown
or unrefined write set overlaps everything – it runs alone if first and is held otherwise – and the
orchestrating prompt is told to obey the result rather than perform the set arithmetic itself.
A review loop bounded by the runtime, not by prose
MAX_REVIEW_ROUNDS = 3 lives in src/loop-bounds.mjs, resolveReviewRounds clamps
work.loop.reviewRounds to it, src/commands/loop.mjs owns the persisted counters, and
src/work-loop.mjs decides admission, deduplicates structured Blockers across resume, stops on a
non-decreasing Blocker count, and refuses a fourth round. A bounded stop is reported as its own outcome
and never as an accept.
81 · The loop’s bounds survive a grader that takes real time
A grade that runs without stopping the loop
aof work grade <ref> --run spawns the declared rubric through spawnRubricAsync
(src/commands/grade.mjs), an asynchronous child that resolves the same
{ status, stdout, stderr, error, signal } shape spawnSync answered, so the event loop keeps
turning for the whole time a runner works: SIGINT reaches onSigint and becomes an
operator-interrupt halt naming the item being graded, queued timers fire, and every other run’s
heartbeat consumption keeps draining. The spawn stamps no heartbeat of its own.
A grade deadline derived from the window that supervises it
gradeDeadlineFromConfig (src/loop-bounds.mjs) returns min(startToCloseMs, heartbeatMs), and
src/commands/grade.mjs resolves its deadline only through that function – it hard-codes no timeout,
reads neither work.loop.startToCloseMs nor work.loop.heartbeatMs behind their resolvers, and
declares no work.loop.* key of its own, so LOOP_BOUND_CONFIG_KEYS, LOOP_BOUND_VALUE_RESOLVERS
and the tuner’s declared ranges are unchanged. On this repository the derived deadline is
900,000 ms, down from the 1,800,000 ms startToClose the grade used to resolve.
A runner that outlasts its deadline is graded, not reaped
A child that exceeds the derived deadline is force-killed and reported as ETIMEDOUT, which reaches
compileGrade as the existing runner-timeout code and an indeterminate verdict; the loop halts
on grade-indeterminate naming work:grade:runner-timeout as its producer and carrying the deadline
that was exceeded, and no run is left running to be reclaimed as stranded. A child that out-talks
the capture ceiling is a distinct outcome – ENOBUFS, classified ahead of the shared SIGKILL – so
an overflow is never reported as a deadline.
A capture ceiling counted in bytes
spawnRubricAsync keeps both streams as buffers and decodes the concatenation once at the end, so
the ceiling is the bytes its name and maxBuffer claim rather than UTF-16 code units, and a
multi-byte character split across a chunk boundary is settled rather than left to where the kernel
cut it.
One bound on every grade payload that is written
boundGradeFailures (src/work-grade.mjs) is the single function that bounds a grade payload. It
lives in the pure leaf, imports nothing from src/, takes its ceilings and its measure from the
caller, refuses to return an over-ceiling payload, never returns an empty one, and appends a
truncation entry – a key, not prose – naming how many failures were dropped and whether a message
was cut. All four write surfaces call it: the re-driven run’s brief.grade, the cap-exhausted report
line, the fix transport’s ## REVIEW FINDINGS block, and the operator’s rendered verdict, whose
former failures.slice(0, 20) is replaced by the call rather than left beside it. The entry ceiling
is GRADE_FAILURE_MAX_ENTRIES = 20 (the operator render’s own literal, promoted) and the character
ceiling is 70’s PHASE_BRIEF_MAX_CHARS; each surface measures the payload as it will be written,
including the gate key a statement entry gains after the bound returns.
The GradeRecord itself is unbounded, and still carries the whole truth
compileGrade returns the runner’s failures verbatim and whole, aof work grade <ref> --run --json
carries every one of them, the record’s key set is exactly the one it carried before, no tenth
GRADE_CODES entry is coined, and 54/ADR-005 §2’s ratchet is untouched.
A re-drive the resume path reconstructed declares that it carries no grade
drivenRow (src/commands/loop.mjs) emits graded: false and
gradeAbsence: "reconstructed-on-resume" for a drive the resume path rebuilt – a key a consumer
branches on rather than prose it parses – and only where a rubric is declared, so a repository with
no work.rubric gains no byte. No grade is fabricated, the pre-interruption grade is not carried
forward onto the new tree (it stays readable where it was recorded), and a resume launches no child
process. LoopState keeps its ten top-level keys in their order and actShape()’s whitelist is
untouched.
The fix transport carries the transport’s own keys, and the grade rides beside it
pendingFixes is 70/04’s declared shape verbatim, and ctx.loopDrive.fix is that shape – no
GradeRecord is among its keys and the driver’s registered input schema is unchanged. The grade
travels in a separate pendingGrades map keyed by ref, read at exactly one place: the seam that
writes brief.grade on the run being started, through transitionRunStart’s edge.brief.
src/effects/run-transitions.mjs and src/run-store.mjs are passed through and unedited. All four
sites that prepare a pending fix – the gate re-drive, the progress reset, the progress continue
and the resume reconstruction – now prepare it the same way, so every graded entry names work:grade
as its producing gate on every branch and a validate finding is told from a graded case by a key.
80 · Every item that delivers says what it delivered
The OUTCOME template has one home, and it is filed under no type
The template ships once at src/bundle/templates/shared/OUTCOME.md and renders to
.aof/templates/work/shared/OUTCOME.md; aof work update carries a delete action for the
milestone-filed copy, so an existing install ends with one copy of the grammar rather than two. This
supersedes m39’s delivered statement that the template ships at
src/bundle/templates/milestone/OUTCOME.md and .aof/templates/work/milestone/OUTCOME.md – neither
path exists.
aof:verify authors an outcome for a milestone, a story and a chore
Each of the three carries an OUTCOME.md in its own folder authored at Accept, whether a story sits
under a milestone or is parentless; a spike and a uat carry none, and both exclusions are stated in
the verify prompt rather than left as omissions.
A chore states what its ticking made true, without becoming a story
A chore’s deliverable stays its ticked ## Definition of Done, and its outcome adds ## Delivered
alone – the accept path for a chore reaches the shared template from its own dispatch branch, not
through the numbered process steps a chore never enters.
A milestone’s outcome is authored, never a concatenation of its stories’
The verify prompt directs a milestone’s outcome to state what is true at the milestone level and to
cite a story’s whole capability as m<NN/SS>/<id>; the aggregation happens in the index, where
buildRecords unions every item’s records into one recall surface.
The memory index reads an OUTCOME.md from any item that carries one
The scan is the whole item set rather than type === "milestone" && parent == null, so a parentless
story’s, a nested story’s and a chore’s capability and gap records enter the same recall surface a
milestone’s do.
A delivery record cites its item by ref
A record carries its item’s full ref in item ("39/02" for a nested story), so a recall renders the
citation as m39/02 – the ref the citation grammar already expects – rather than m02, which is a
different, real milestone.
An --item or --only scope matches the item and its subtree
A milestone-scoped recall or rebuild carries its own stories’ delivery records, from one subtree rule
in the zero-import leaf src/work-ref-scope.mjs shared by work doctor’s inScope and both memory
scopes; an unresolved scope returns an empty block at exit 0.
An OUTCOME.md is unbudgeted, whatever type carries it
doc-over-budget covers SPEC.md, ARCHITECTURE.md, STORY.md and *.feature and gains no kind
here, so a second Accept-time artifact anchors no finding at itself.
79 · The committed loop graph
The committed loop graph
wiki/work/loops.md is a tracked markdown document at the work-directory root holding the loop
registry’s Mermaid graph in a fenced block, a ## Health summary (declared records, declared edges,
error/warning totals and the per-check split), a leading `` marker naming its
own regenerating command, and no frontmatter at all.
work:loop-document – the registry’s one writer
A registered command whose bare face composes the document and emits it touching no disk, whose
--write is the only door to the filesystem, and whose input contract accepts no caller-supplied
output path – the single home is derived from work.dir and cannot be redirected.
Byte-identical regeneration
Running aof work loops document --write against an unchanged registry rewrites the same bytes and
reports changed: false, in-process and across separate processes; the composer reaches no clock, no
filesystem and no environment, so the same registry composes identically from any working directory.
The drift gate
test/arch/acd-loop-document-current.test.mjs reds the test suite when a loop record is edited
without regenerating the document, naming the committed document and the command that regenerates it;
it reads and renders only, repairs nothing, reports an absent document rather than passing it, and
gates the suite alone – no item transition, acceptor door or doctor severity reads it.
The frozen lists admit the writer, and the board does not
work:loop-document is a member of the frozen WORK_IDS census and of the BOARD_DEFERRED
carve-out with its deferral reason recorded beside chore 64’s four read verbs; no /api/work route is
served for it and ui/ carries no token naming the command, its modules or its id.
The registry family is untouched
The writer’s modules (src/loop-document.mjs, src/commands/loop-document.mjs) match neither of
52/FF-5201’s discovery patterns, that gate’s expected module list and read-only sweep are unchanged,
and src/commands/loops-graph.mjs is byte-unmodified – the frozen renderer is imported, never
restated.
78 · The loop execution record
aof can now show the loops that RAN, per work item, as a committed document
Before this milestone aof could render its loop registry – the same nine framework-wide nodes in every
repository, to stdout only – and could not answer “what ran for item 54”. It can now: one registered
command produces a per-item execution record, and --write commits it beside the work. This is the
additive face 52 pre-authorised (52/ARCHITECTURE.md:869-870) and it changed no byte of 52’s frozen
renderer.
Execution is legible against DECLARATION, not merely reported
The record states observed facts against what the registry declared: cycles against the declared
ceiling, phases entered, attempts, terminal outcome and stop reason. SPEC.md opened on the complaint
that “a ceiling: uncapped loop and a capped one must not produce identical records; today they do.”
They no longer do – the four ceiling states are distinguishable in the model (m78/00/) and, because
that is the property that regresses silently, in the rendered BYTES (FF-7806).
Absence is delivered as a finding rather than as an empty page
The record names three classes of gap – a loop that ran and is not declared, a declared loop that never
ran, an authority that cannot be resolved – under their own headings, and emits no heading and no
“None” placeholder for a class with no gaps. Measured on this repository at the accept gate: zero
coverage, zero engagements, and all seven registry loops reported under declared-never-ran. The
milestone’s central claim is therefore demonstrated in the one condition that matters – the condition
where there is nothing to show.
A machine-readable place for a human to say the execution was acceptable
The arc 52–63 had no machine-readable sign-off of any kind; @uat tags and prose in ## Accept
decision were the whole vocabulary. There is now a frozen block (m78/03/) with a shape a check can
read, and a signature in it survives regeneration verbatim (m78/02/) – which is what makes the
document safe to regenerate and therefore worth committing.
The whole thing is a face, with the writer as its only door to disk
Every execution fact reaching a consumer is computed from the run records; nothing recovers one from
the document. The record can be tampered with and the command’s answer does not move by one field.
Regeneration is byte-identical on unchanged inputs across processes and working directories, so a diff
in a pull request means an input changed. Cited whole at m78/02/ and m78/00/.
The milestone stayed outside the registry’s read-only law without weakening it
A milestone whose deliverable is a WRITER landed inside a domain whose defining law (52/FF-5201) is that it does not write. The modules take the execution family’s name, FF-5201’s discovery patterns match none of them, its expected six-module list is unchanged, and its read-only sweep is re-measured independently rather than re-run. The law is intact and the writer is provably outside it.
Ten declared controls, all landed, all red-probed
Every entry in this milestone’s ARCHITECTURE.md fitness register resolves to a file in the runnable
test tree, is green, and carries a red probe recorded in VERIFICATION.md – 24 probes in total, each
applied to the working tree, run in a fresh process, reverted, and the restore confirmed byte-identical
by sha256. One probe did not go red and is recorded as a finding (F-78-H) rather than replaced with
one that did.
78/00 · The execution projection
The execution projection
src/loop-record.mjs turns a loaded loop registry model and one work item’s run records into the
item’s execution facts – engagements, cycles against declared ceilings, phases, attempts, outcomes,
join coverage and gaps – as a pure function over injected values.
Engagement identity by loopRunId
Two engagements of the same loop are two rows: runs are grouped by brief.loop.loopRunId, so
separate engagements of one loop are never fused into one.
Four distinguishable ceiling states
bounded, none, unknown and uncapped are separate states carried in the model, and two
engagements identical in every observed fact but differing in ceiling are not equal models –
“terminates by construction”, “nobody has said” and “deliberately unbounded” no longer collapse to
one word.
Join coverage as a stated measurement
Every model carries how many run records were found, how many carried a loop declaration, and the ratio. A zero-coverage model over 14 undeclared runs is a different object from a model over no runs at all, so an empty answer is distinguishable from a broken projection.
Three separately named gap classes
ran-undeclared, declared-never-ran and authority-unresolved are reported under their own names
in a frozen order, no gap appearing in more than one class, and gaps within a class ordered by their
subject’s code-unit sort.
A structurally enforced purity boundary
FF-7801 (test/arch/acd-loop-record-projection-pure.test.mjs) holds src/loop-record.mjs and every
module it directly imports free of node:fs, a clock and a child process, and pins the direct-import
set to ["src/loop-bounds.mjs"] so a new import re-opens the question.
78/01 · The item-scoped renderer
The item-scoped execution graph
renderExecutionGraph (src/loop-record-render.mjs) draws a Mermaid flowchart LR of the loops one
item ENGAGED plus the authorities those loops cite – a 17-record registry whose item engaged 2 draws
2 loops and none of the other 15.
A two-pass scope rule that cannot readmit the framework-wide picture
An authority is drawn because a scoped loop cited it; a registry-declared edge is drawn only when both of its endpoints are already in the finished scope, so an edge to a loop the item never engaged adds no node.
The execution document
renderExecutionDocument returns Markdown wrapping a fenced mermaid block – frontmatter first and
the generated-file marker after it (the F-73-G order), then the coverage line, the per-engagement
facts, the graph, and one heading per non-empty gap class.
Coverage stated on the page, always
The document carries how many runs were found, how many carried a loop declaration, and the percentage, whether coverage is zero or complete; a zero-coverage document says “No loop ran for this item.” in words rather than rendering blank.
Absence rendered as absence, not as a placeholder
A gap class with no members renders no heading, and an empty scope still renders flowchart LR – an
empty answer is distinguishable from a renderer that failed to run, and no “None” placeholder is
emitted.
Four ceiling states legible in the rendered bytes
A capped engagement and a none / unknown / uncapped one render pairwise-different bytes, each
non-numeric state carrying its own remedy in words, and a numeric bound renders the observed cycles
against it – with , over the bound when exceeded and no claim that the bound was respected.
In flight is not failure
An engagement with no terminal outcome renders as in flight; a settled one renders its outcome and,
when one is carried, its stop reason.
One glyph table across all three faces
FF-7802 (test/arch/acd-loop-record-renderer-additive.test.mjs) holds the new renderer importing
KIND_SHAPES from src/commands/loops-graph.mjs, spelling no declared kind’s glyph itself, and
declaring no second table – and holds src/commands/loops-graph.mjs byte-unmodified, asked of git
against HEAD rather than of a pinned digest a maintainer would re-stamp.
A rendering with nothing in it that changes by itself
The document carries no generation timestamp, no absolute filesystem path and no host or node name, so two renderings of one model in one process are byte-identical.
78/02 · The record command
aof work loop-record <ref> – the per-item execution record as a command
work:loop-record (src/commands/loop-record.mjs) resolves a work item, projects its execution model
from the run records and the loop registry, and emits the composed document. It is registered once in
the shared command core, resolves as aof work loop-record <ref> with no ladder, requires a ref, and
accepts exactly two inputs – ref and write.
A read face that touches nothing
Run without --write, the command composes and returns the document’s bytes and writes no file. The
loop registry is left byte-identical, an item with no run records is still answerable, and a zero-
coverage answer exits successfully rather than as an error.
--write as the only door to disk
The record lands at EXECUTION.md inside the item’s own folder, on a path derived from the item that
no caller can redirect – there is no output-path input to supply. Exactly one file is created or
modified per run, through one shared atomic writeText, and nothing under .aof/ or at the work
directory’s root is touched.
Byte-identical regeneration
Running the command twice on unchanged inputs produces the same bytes, in one process, across two fresh processes, and from a different working directory. The read face composes the same bytes the writer writes, so a regeneration can be reviewed before it is taken, and a non-empty diff in a pull request means an input moved rather than that somebody ran the command.
A human signature that survives regeneration verbatim
The writer is a read-modify-write: it parses the existing document’s sign-off table, carries every
signed row forward as its own line – the operator’s spacing, date format and wording untouched – and
re-derives every other line. A signature survives even when the facts it signed have since changed. A
row counts as signed only when signer, date and verdict are all filled; an untouched – placeholder
never does. A document the parser cannot read is refused rather than regenerated, so a malformed
record loses no signature.
The record is a face, never a second truth
EXECUTION.md’s basename is named in exactly two modules – this writer and 78/03’s checker. The
document is read exactly once, and the only thing parsed out of it is the sign-off block. Every
consumer of an execution fact computes it through 78/00’s projection from the run records; the
renderer is imported in one place and only ever written to. A record whose fact lines have been
rewritten to lies does not move the command’s answer by one field, and a regeneration re-derives every
tampered line away.
A board-deferred command, by recorded decision
work:loop-record is a member of the frozen WORK_IDS census and of the BOARD_DEFERRED carve-out,
no /api/work/loop-record route is served, and ui/ gains no reference to it. It ships without an
ACD bundle command wrapper, and the CLI↔bundle parity control demands none – that control is scoped to
the work:insert-* family and names neither this command nor story 79’s writer.
The registry family is provably untouched
The module takes the EXECUTION family’s name rather than loops-record.mjs, so it matches neither of
52/FF-5201’s discovery patterns; FF-5201’s expected six-module list is unchanged, and its read-only
sweep over that family is re-measured independently rather than re-run.
78/03 · The signature and the doctor lane
A sign-off block in a shape a check can read
Every EXECUTION.md ends with a frozen block: the h2 ## Sign-off, the header row
| loop | signer | date | verdict |, and the divider |---|---|---|---|. One row per engagement, each
naming its own loop, and the block is emitted even for a document with no engagements. A human
signature in this record is now findable by a program rather than only by a reader.
The loop id stands alone in the first cell
Following m66/ADR-001’s positional rule, a sign-off row’s first cell carries the loop id and nothing
else. A first cell carrying prose declares nothing, and both the writer and the checker refuse such a
document rather than reading a signature out of it. The block is deliberately NOT a REGISTER_BLOCKS
member and this milestone added no new member to m66’s closed ID_FORMS set – the five it pinned are
still five.
Signed means complete
A row is signed only when signer, date and verdict are all filled. A partially filled row is unsigned,
an empty row is unsigned, and the – placeholder every freshly written record is full of is unsigned.
A rejected verdict is a signature, not an absence.
Three independent copies of the frozen shape, held byte-equal
The literals live in the writer (src/commands/loop-record.mjs), in the checker
(src/work-doctor-loop-record.mjs), and a third time in FF-7809’s own source. The checker’s direct
imports are exactly ["node:path"], so the instrument cannot inherit its subject’s opinion of the
shape it polices, and it drags neither the registry loader nor the filesystem into a work-doctor*
module’s import closure. The freeze is round-tripped as well as compared: the block the writer emits is
parsed back by the checker’s own parser.
A doctor lane that reports the record and gates nothing
loopRecordLane (src/work-doctor-loop-record.mjs) is registered once in CHECK_GROUPS, appended
after every lane that existed before it, and emits four codes – loop-record-unsigned,
loop-record-part-signed, loop-record-stale, loop-record-malformed. It reads the record when one is
present and never demands one: an item with no record produces no finding.
Every finding is a warning, structurally
The lane’s severity is one module constant and it names no error anywhere; it does not consult the
acceptance horizon, so a record on a done item reports the same warning it reported while the item
was open. Its four codes are a different frozen array from CONTROL_FINDING_CODES, and
DOCTOR_GATE_CODES is derived from that array – so no code of this lane can reach the gate ladder even
if one were hardened. No status, validate or acceptor door names the record, its signature or its
codes; the doctor engine reads the record at its one impure edge and renders no verdict about it. An
item carrying an unsigned record moves to done and work:validate stays green.
Staleness answered from the runs, not from the document
The lane’s staleness check compares the record’s engagements against the engagement list projected from the item’s run records – it does not parse the document’s own fact lines. An orphaned signed row is not staleness, a record behind its runs is reported with what is missing named, and with no engagement list available the lane makes no staleness claim at all.
77 · Harness audit – doctor for the machine, not the record
aof work audit audits the machine, not only the record
aof work audit carries seven lanes and 33 finding codes, four lanes and seven codes of them
added here, and its subject is now the harness that produces the records rather than the records
themselves – the sibling aof work doctor never grew, and 59/FF-5905 still refuses a shared code
between the two.
The rules travel
Every rule this milestone adds runs against an audited project’s installed layer rather than
against this checkout, and the command’s own child programs resolve against the toolkit root while
its subject resolves against the audited root – so aof work audit --strict runs in a governed
project that is not an aof checkout at all (m77/04).
The audit is clean over its own repository, and every error leg measures zero
aof work audit --strict exits zero here: 337 findings, all warn, with every error leg of the
seven codes added measuring zero on arrival. The 337 are a standing measurement of this repository –
262 duplicated prompt-document pairs, 9 capability gaps, 6 unwired seams, 5 undeclared bounds and 1
bound off the reference – not a backlog the command failed to clear.
The command’s report states what it could not see
Every lane declares a read floor and every text-basis sweep returns a limit naming its own
blindness, both enforced from the registry rather than per lane, so a lane that looked at nothing
cannot report the same thing as a lane that found nothing (m77/00, m77/02, m77/05).
Three bespoke local gates became travelling rules
The capability gap, the duplicated instruction and the hook twin were each gated in this repository
alone before this milestone; each now has a form that runs wherever the bundle is installed, and the
repo-local gates they came from (72/FF-7206 in particular) are unweakened and still in service.
A versioned reference corpus ships in the payload
src/harness-reference.mjs carries six bounds other systems declare, each with a source URL and a
checked date, and the audit joins a project’s declared bounds against it offline – the corpus
travels with src/, and the program that rewrites it is reachable from no CLI door (m77/03).
The audit’s code space is derived, so a fourth lane cannot arrive outside the disjointness check
AUDITABLE_CODES is computed from REPORT_LANES rather than enumerated, and the control that
asserts disjointness from doctor’s codes reads that derived set – a lane added tomorrow is inside the
check on arrival (m77/05).
Two ledgered debts are closed
TECH_DEBT items 70 and 72 are discharged: every program the audit family spawns is under
src/, named in one enumeration, and resolved against the toolkit root.
72 · The inner loop
The inner loop’s tools are declared by the project and launched by aof, and aof imports nothing of the project’s
One boundary holds across all five stories: the test runner and the worktree prepare step are work.test and work.worktree.prepare declarations compiled by one module (m72/00), every child process this milestone starts comes from runBounded with a declared deadline, and no test module enters the aof process on any path (m72/02, FF-7204). The framework works in any repository that declares its toolchain and refuses, by coded message, in one that does not.
Selective testing is a command, not a per-agent chore
aof test --scope impacted|file|all replaces the throwaway array-importing script: the selection (m72/01) reads the code graph and widens on every unknown, the face (m72/02) prints failures only from both streams, and this repository’s own runner accepts --only through its one execution loop. The result is a report that no transition door consumes.
A hook costs a hook’s worth of boot
aof session ping, fired on every prompt, loads a 25-module closure instead of the 277-module command registry, and fires once per event rather than twice (m72/03).
A worktree arrives prepared, and nothing is ever linked into one
Dependencies reach a worktree by the declared install at every materialisation door, a failed install removes the tree it half-filled before it throws, and FF-7207 ratchets the TECH_DEBT-36 hazard shut over all three worktree roots (m72/04).
Two of the six SPEC levers are declined, not missing
The pre-apply edit gate and the blocking write-thrash guard were declined on evidence (ARCHITECTURE.md#ADR-006, TECH_DEBT item 87), and the SPEC’s PreToolUse output-rewriting hook was replaced by the command’s own output contract (ADR-003). No hook of any kind ships from this milestone.
72/00 · The declared toolchain
The test runner aof launches is the project’s declaration, and there is no default
src/work-toolchain.mjs is the only module in src/ that reads work.test.command, work.test.args, work.test.selectArgs, work.test.roots, work.test.deadlineMs or work.worktree.prepare, and no program name a declaration may supply – npm, vitest, pytest, yarn, pnpm – is spelled anywhere in src/ in an executable position. An absent declaration is test-runner-undeclared naming the key, and no program is substituted for the one nobody declared.
The three ways a declaration fails are three different answers
test-runner-undeclared (nothing declared), test-runner-declaration-invalid (a present declaration with a bad field, the message naming the field) and test-runner-unresolvable (a well-formed command that resolves to no file) are distinct codes. Every field fault within one declaration shares the one code and is separated by the field its message names.
The program is resolved to a real file before anything is launched
A bare name is resolved against PATH with PATHEXT honoured case-insensitively, an absolute path is carried unaltered, and a relative one is resolved under the project root – each checked to be an existing file, not merely an existing path. The resolution sits in FRONT of runBounded’s argument-vector door, so that door refuses exactly what it refused before and a command that resolves nowhere is a coded refusal rather than a spawn that fails later and further away.
A shim is undeclarable, and the refusal says what to declare instead
A resolved .cmd, .bat or .ps1 is refused by the resolver with a remedy naming an interpreter plus its script, rather than left to the seam to report as a failure to start.
Every child process this milestone starts comes from the one bounded seam
src/work-toolchain.mjs imports node:child_process nowhere, names no exec/execFile/execSync/spawnSync/fork, passes no shell: option, and hands runBounded an argument vector plus the deadline the declaration states rather than the seam’s own 60,000 ms default. The injected launch parameter is named launch, after no process API.
Selection reaches the runner through one expansion rule
{file} expands in place, once per selected file; no other token is a placeholder – {root}, {files} and {suite} are carried through as literals. Zero selected files yields an empty selection, so a flag runner never emits a flag standing without its operand.
A run that produced no verdict is never reported as a passing one
exited maps to the run’s own verdict and its observed exit code; deadline-expired and not-started are each reported as themselves, with the bound applied or what was attempted, and each exits non-zero. The three are distinguishable from each other and from a run that exited non-zero.
The prepare declaration is compiled by the same module and differs only in absence
work.worktree.prepare absent is a silent no-op – no refusal and no warning – while a present-but-malformed one is worktree-prepare-declaration-invalid raised where the declaration is compiled, not at the launch, and told apart from absence. A prepare command resolving nowhere is worktree-prepare-unresolvable.
This repository declares its own test runner
.aof/aof.config.json carries work.test as node scripts/test.mjs, selecting with ["--only", "{file}"], rooted at test, bounded at 900,000 ms, reporting tap – an interpreter plus a script, never a shim. aof is now an installed project against its own boundary.
72/01 · The selection
A changed file reaches the suite files that can break it, through the graph the repo already builds
selectSuites (src/work-test-select.mjs) answers a changed set with the suite files coupled to it, taken from graphify-out/graph.json through the shipped normalizeGraph and computeImpact. It reads the artifact and never builds one: no graphify invocation, no second artifact read, no second parse and no child process exists anywhere in the module or in src/graph-impact.mjs.
An unknown widens the selection to the whole suite and says which unknown did it
A changed file the graph cannot account for produces scope: "all", the whole suite selected, and an entry in widened[] naming that file and one of exactly four reasons – no-graph, not-in-graph, no-registered-dependent, graph-unreadable. The vocabulary is frozen at four in the module itself, and the rule is checkable from both sides: wideningRuleProblems refuses a widened result that selected a proper subset and refuses a non-widened result carrying a changed file that never resolved.
Presence outranks the union, so a test file created this turn widens rather than selecting itself
A changed file absent from the graph widens even when its own path satisfies the suite predicate. The union of the changed file with its dependents is taken first and the path predicate last, so a changed file that IS a registered suite is in its own selection – but only after presence has been established.
No option suppresses a widening
selectSuites accepts projectRoot, changed, allSuites and roots and nothing else. No key that suppresses, narrows or overrides a widening is accepted, no --no-widen / --strict-scope / --narrow / --assume-fresh / --skip-widening is spelled anywhere in the story’s modules, and an unknown suppressing key passed anyway leaves the widening standing and named.
Every result carries the artifact’s own instant, or exactly null
builtAt is graphArtifactBuiltAt’s reading of the artifact’s file mtime on every path where the artifact was read – including the two widening paths that read one. On no-graph there is no file, and on graph-unreadable the file’s perfectly good mtime is discarded; both report exactly null, with the reason carried in widened[]. No module in this family reads a clock.
Selection is pure across calls
The same changed set against two different artifacts answers twice, differently, in one process, in either order. Nothing is cached – not the normalised graph, not the impact walk, not the answer.
A selection is a report and decides no transition
Every result carries gate: false unless the whole suite was selected AND nothing widened. Nothing in the tree consumes it, so 09/ADR-004 is not crossed by this story.
A selected suite the runner does not assemble is reported, in the census’s own words
registrationReport imports registrationDecision from src/work-audit/census.mjs and carries its verdict verbatim – the same code and the same message, the census’s audit-suite-unregistered / audit-suite-imported-never-spread / audit-runtime-membership-unavailable vocabulary and its baseline’s carried reasons. No second derivation exists in this story: no pattern over a suite import line, no spread-row matcher, no second unregistered baseline, no second read of the assembled array.
The changed set is read from git through the one bounded seam, and three ways of having nothing to select are three answers
src/work-test-changed.mjs runs status --porcelain, diff --name-only and rev-parse --verify through runBounded with a 60,000 ms bound, keeping untracked files and both ends of a rename. The default base is the working tree; no default branch is inferred, and no branch name – main, master, origin/HEAD, @{upstream}, symbolic-ref, merge-base – is spelled anywhere in it. since-rev-unresolvable, changed-set-empty and changed-set-unreadable are three distinct refusals, none of them a fifth widening reason.
computeImpact has a home below the command layer, and its face is byte-equivalent
src/graph-impact.mjs holds the pure coupling core; src/commands/graph-impact.mjs re-exports it, so both of its previous importers are unchanged and graph:impact behaves exactly as it did. matchFile, declared in the command and never used, did not come with it. Both frozen graph-reader allowlists now name src/work-test-select.mjs – and not src/graph-impact.mjs, which takes an already-normalised graph and names no reader symbol.
72/02 · The test command’s face
aof test is a registered top-level command, the one face this milestone adds to src/commands/
src/commands/test.mjs is registered in src/command-core.mjs under the id test with the one-word route ["test"], outside the work: namespace; it composes 72/00’s declaration and bounded launch with 72/01’s selection and changed-set reader, and holds nothing of its own beyond one frozen result object and two faces over it.
The scope forms are three, and a fourth is a refusal
--scope impacted, file and all are the only forms. An absent, differently-cased or unknown scope is test-scope-unrecognised; file with no file named is test-file-scope-names-no-file. Both are results rendered on either face with nothing selected, no runner launched and exit 1 – never a fallback to one of the three.
The report is failures-only by default and is derived from both streams and the exit code
Each failing case is printed with its assertion text, then one summary line – selected/total suites · scope · graph built <builtAt> · <widenings> · may / may not stand as a verdict. Stdout and stderr are concatenated before the one shipped TAP reader (normaliseReport) parses them, so a not ok this repository’s runner writes to stderr is read. A green-reading report beside a non-zero exit is reported as a contradiction and exits 1; a report no normaliser can read is reported as producing no verdict and exits 1.
One result object, two faces
--json returns the frozen result itself and the human face renders from the same object. --verbose adds the passing rows and removes no line the quiet run printed.
Every result says whether it may stand as a verdict, and only an unwidened whole run may
gate is true only when the scope ran as all and nothing widened; every impacted, file and widened result carries gate: false. No status, accept, merge, loop or audit door invokes the command – censused by invocation shape over src/commands/item-status.mjs, src/work-doctor.mjs, src/work-loop.mjs, src/work-audit/** and src/bundle/** – and the registered command declares no cli.launch.
No test module enters the aof process
None of the nine modules the command reaches resolves a specifier under a declared test root when imported alone in a fresh process, and the family holds no dynamic import of a test path.
This repository’s runner runs named suites additively, through its one execution loop
scripts/test.mjs --only <file>… imports each named suite file, takes every runner-shaped array it exports, and runs them through the same runSuite the full path calls – parameterised in place as runSuite(tests, { lanes = true }), the integration, cargo and shell lanes skipped by one early return on a selection. A file that is absent, does not evaluate or exports nothing runnable is reported not ok by path and the run exits 1; --only with no file refuses naming the option; the assembled array is not read, reordered, restructured or appended to; and importing the runner runs nothing, whatever the importer’s argv holds.
REG-MUT-11’s residue is re-derived and no leg is weakened
test/arch/acd-loop-suite-registration.test.mjs differs from HEAD by exactly one line, :269 (RUNNER_RESIDUE); the four region pins, residueFloor, the integration-lane count, both registration patterns and the resolve-on-disk leg are byte-unchanged.
72/03 · The cold boot
A session verb boots without the command registry
src/cli.mjs’s static import closure holds neither src/command-core.mjs nor src/spine/face.mjs – 25 modules, down from 277 – and src/commands/mesh-session.mjs’s closure holds neither and no dynamic import of them. The registry and the generic face are reached by await import() only on the dispatch arms below the session arm and inside helpText(), which is async and awaited at both of its call sites.
The session arm is the first statement of run()
command === "session" dispatches before helpText(), before the route table is resolved and before any registry binding is used; src/work.mjs stays a static import.
aof session start, ping and end behave as they did
Each verb records, refreshes or removes its own session leaf exactly as before, and the refusals it made before – invalid-input, unknown-subcommand, session-arg-missing-workspace, session-arg-missing-repo, session-cwd-not-workspace – it still makes, with no record written.
This repository’s settings carry one session hook per event, and the survivor is the managed one
.claude/settings.json holds one SessionStart, one UserPromptSubmit and one SessionEnd entry, each carrying its aofManaged marker; the three hand-authored copies are gone (six aof session … commands at HEAD, three now), so the presence ping shells once per prompt. The unmanaged Bash|PowerShell guard entry is present character for character.
No unmanaged settings entry may duplicate a managed one, and the merge is untouched
FF-7206 pairs entries by event AND matcher, compares resolved invocations (command plus args, ${CLAUDE_PROJECT_DIR} unexpanded, either spelling), admits the operator’s own guard with a managed neighbour beside it, and imports nothing from src/claude-settings.mjs. isAofEntry and spliceSettings are unchanged.
FF-7205 asserts structure and never a duration
The control walks both closures through the shipped importClosure, proves the walk transitive and non-empty against a planted fixture and two-sided floors, and censuses its own source for clock readings, elapsed-time subtractions and duration bounds.
The delivered drift lane pins a literal
test/bundle-claude-session-hooks.test.mjs’s reference for the hand-wired session command is the literal "aof session start", so a bundle changed to another spelling still reds; the delivered feature 49/07/00 is untouched, the supersession of its premise recorded in ARCHITECTURE.md#ADR-005 §5.
72/04 · The prepared worktree
A worktree is prepared through the project’s declared program, at the one choke point every door funnels through
src/mesh-worktree.mjs runs the compiled work.worktree.prepare step inside the tree each materialisation door returns – addWorktree, reuseWorktreeOnBranch (the dominant continuing-item path, now routed through the private runWorktreeAdd with its two thrown codes and messages byte-unchanged), addSessionWorktree and addDispatchWorktree – through runBounded, with the declaration’s own deadline armed.
An absent declaration is a silent no-op; a malformed one is a compile-time refusal
No work.worktree.prepare means no step, no error and no warning. A present declaration naming no command, with a non-list args, a non-positive deadlineMs or a command resolving nowhere is worktree-prepare-declaration-invalid or worktree-prepare-unresolvable, raised by src/work-toolchain.mjs before any door runs and told apart from absence.
A failed prepare is loud, coded, and leaves no half-installed tree behind
A non-zero exit, a deadline expiry and a failure to start are worktree-prepare-failed, worktree-prepare-deadline-expired and worktree-prepare-not-started; on any of them the module git worktree remove --forces the tree it just created and throws, with the prepare’s stdout and stderr riding the thrown message. A teardown that itself fails is reported on the module’s degrade channel, never swallowed.
The prepare outcome is observed, not returned
Every door still returns a bare path string; the outcome surfaces through the module’s existing options observer idiom, and the compiled declaration can be injected as options.prepare.
aof creates no link into a worktree and deletes no worktree by filesystem call
FF-7207 censuses every src/ module for symlink, symlinkSync, link, linkSync, mklink and New-Item -ItemType Junction whose path or target reaches a worktree, and for rm, rmSync and rimraf with recursion over a path derived from the worktree seam. src/mesh-worktree.mjs reaches no filesystem delete at all, and removal is git worktree remove, forced or not.
A worktree is recognised by derivation, over all three roots
isInsideMeshWorktree(projectRoot, path) composes the three shipped isUnder…Root predicates with resolve(path) !== resolve(root), so a keyed child under the assignment, session or dispatch root is inside, and the bare root, a lookalike sibling and a foreign project’s tree are not. The three shipped predicates are byte-unchanged.
71 · Loop discipline – one review round, and findings become work items
Every loop in the ACD prompt layer now terminates, and says so where an agent reads it
Before this milestone the build step had a written terminator and the review step had none – no round
cap, no re-review bound, no exit criterion. Both halves now terminate in the shipped assets and in
their fifteen rendered runtime copies: build stops on green-or-no-progress (m71/00), review runs one
round by default with a named-Blocker second and a hard third (m71/02), the free gate ladder runs
before any reviewer is spawned and again after every fix round, and a red ladder consumes no round.
The prompt layer and the runtime speak the same bounds in the same words.
A bound stated anywhere in the bundle names its home, and equals it – enforced across the whole bundle
This is the milestone-level property no single story holds: FF-7101 reads every asset under
src/bundle/, resolves every work.loop.* key it names through LOOP_BOUND_VALUE_RESOLVERS, and
binds every stated value to that bound’s own answer – to the exported clamp where a clamp
identifier stands in the same sentence, and to the config key otherwise. A numeral that names no key
and no clamp is out of reach by construction, and the control asserts that it is. Combined with 69’s
acd-loop-cap-single-home over src/**, there is now no home for a loop bound in this repository
that some control does not read.
The loop’s creation authority is bounded, and the bound is structural rather than numeric
An uncapped round loop became a capped one with a queue behind it. The loop creates exactly one item
type (chore), in exactly one place (top level, invisible to the walk that created it), appending
after the highest number and shifting nothing, idempotent per finding – see m71/01 for the verb, the
router and the engine. It is in live use, and that is the milestone-level fact: three chores in
this stream now carry a **Promoted from review finding:** back-reference – 88 and 90 from
build/review closes, and 89 from this milestone’s own gate. Before this milestone, grep -rl
"Promoted from review finding" wiki/work/*/CHORE.md answered nothing.
The milestone’s own gate was refused by a rule this milestone wrote, and the queue is what cleared it
71’s first gate pass failed its own accept rule: ARCHITECTURE.md declared FF-7106 and the
four-story partition gave it no owner, so a declared control did not resolve. The declaration was kept
rather than dropped – chore 89’s Definition of Done cited the register as its specification – and the
chore was executed to done, landing the control, whereupon the second pass cleared. The loop closed
on itself: a finding the cap stopped chasing became a named work item, which was worked, which cleared
the gate that raised it.
The design lane can no longer burn spawns on a surface it cannot render
m71/03 states the precondition and the skip; the milestone-level fact is that this repository is
itself the unconfigurable case – .aof/aof.config.json carries no work.ui key – and the lane now
costs one recorded INCONCLUSIVE instead of three breakpoints × N surfaces of failed invocations plus
two agent spawns. The cached-Chromium path that lived in seven records and no prompt is now the only
render the bundle names, and 07’s npx playwright clause is superseded in 71’s own contract with 07’s
delivered .feature files untouched.
Six declared controls, six resolving, seventeen planted defects
The register is fully discharged: FF-7101, FF-7102, FF-7103, FF-7104, FF-7105 and FF-7106 each resolve
to a registered file and each carries a red probe in VERIFICATION.md recording what was changed to
make it fail and the message observed – seventeen plants in all, every one run in a fresh process and
reverted with sha256 proven equal before and after. aof work doctor 71 reports no
control-unresolved at either severity.
71/00 · The build’s terminator, and the free gate that runs before any reviewer is spawned
The build phase has a spoken failure-to-progress terminator
aof:continue’s build step states two terminators and which one it stopped at: success is all three
legs together (every task’s @executable scenarios green, typecheck and lint clean, fitness
functions passing), and a build whose failing-scenario count stops falling for 2 consecutive rounds –
work.loop.buildNoProgressRounds – stops, hands back naming the failing scenarios and the bound, and
prints no accept hand-off.
The free deterministic gate runs before any review lane is spawned
aof:continue walks aof work validate <ref> then aof work doctor <ref>, both scoped to the driven
item’s own ref, as step 4 – between Build and Review. The first red rung short-circuits, a rung that
exits non-zero is a red rung, a red gate is reported as its own outcome rather than as a review
verdict, and the ladder re-runs after every fix round without consuming a review round.
The prompt’s ladder is derived from the shell’s, not copied from it
test/arch/acd-prompt-gate-ladder-parity.test.mjs reads the gate ids out of invokeGateLadder
(src/commands/loop.mjs) and asserts set equality in both directions plus rung order, so a rung added
or removed in the shell fails CI until the prompt agrees.
A loop bound stated in a bundled asset names its home and equals it
test/arch/acd-prompt-bounds-name-their-home.test.mjs resolves every work.loop.* key any asset under
src/bundle/ names through LOOP_BOUND_VALUE_RESOLVERS, compares every stated value against that
bound’s own declared answer – binding to the exported clamp when one stands in the same sentence, and
to the config key otherwise – and requires the three command bound facts (work.loop.reviewRounds,
MAX_REVIEW_ROUNDS, work.loop.buildNoProgressRounds) each to name their home. A numeral in prose
that names no key and no clamp is out of reach by construction.
71/01 · A finding the cap stops chasing becomes a named work item, or a named question for a human
The review close routes every surviving finding by one ordered rule, as code
routeFinding() and routeFindings() in src/work-loop.mjs are a pure decider over ADR-003’s four
ordered questions with a closed answer set – amendment, chore, story, recorded – where a
Blocker (chased or outstanding) and a claim that did not reproduce never reach the questions, a Nit
is recorded and never promoted, two lenses’ reports of one defect (title and file:line normalized)
are routed once, creates is only ever "chore" or null, the story routing is owned by the
operator, and no finding id and no finding tag is ever produced.
aof work promote-finding schedules a finding as a top-level chore
work:promote-finding is registered in the command core and reachable as aof work promote-finding
with a reviewed ref, a finding title, --remedy, and optional --location/--round: it creates a top-level chore born not-started
whose ## Definition of Done is the finding’s remedy plus the no-regression line, whose ## Notes
names the reviewed ref, the review round, the finding’s title and file:line and a visible
**Promotion key:**, which appends after the highest existing number with shifted: 0, which is
idempotent on (reviewed ref + title normalized for case, whitespace and backticks) – including after
the chore has been closed – which refuses a missing/unresolvable ref, a blank or unsluggable title and
a blank remedy with coded reasons, and which refuses any input outside its declared set (type,
at, parent, under, …) rather than ignoring it. It has no /aof: bundle wrapper and is deferred
from the board, by decision (ADR-004).
One promotion engine under src/work-promote/, two faces on it
chore-seed.mjs holds the DoD seed and both back-reference shapes; promotion.mjs holds the slug
derivation, the append-position resolver, the idempotence key and scan, and the seed writer.
src/commands/promote-gap-to-chore.mjs delegates to them with its refusals, codes, rendered lines and
--at flag unchanged (39/03’s suite is the control, 9/9 green), and the family imports nothing from
../commands/.
A promotion appends after the highest number, not after the count
appendPosition() answers max(highest top-level number + 1, count). Over this repository’s own
stream (87 top-level items, highest number 87) the count-derived default 39/03 shipped would have
inserted at 87 and renumbered one item; the engine answers 88 and shifts nothing. Both faces take
this default when no position is named.
aof:continue states the triage rule at the review close
The <finding_triage> region of src/bundle/commands/continue.md – and its three rendered runtime
copies under .claude/, .codex/ and .opencode/ – instructs the four ordered questions, first
answer wins; names aof work promote-finding as the close’s only creating verb; states that the loop
creates exactly one type in exactly one place; forbids allocating a finding id; and requires a report
of each finding’s routing, each chore created by ref, and each finding handed to the operator.
Two controls hold the creation bound
FF-7103 (test/arch/acd-promotion-creates-one-type.test.mjs) reads the insert engine’s admissible
types out of its module-private DOCS_BY_TYPE by source-parsing, refuses any insert type on the
promotion path other than PROMOTED_TYPE, refuses runInsertStory, requires the prompt’s review
close to name only promote-finding as a creator and all four routings, and binds the finding face to
no at/type/parent/under input, an unknown-input refusal, and a position resolved through
appendPosition. FF-7104 (test/arch/acd-one-promotion-engine.test.mjs) requires one definition site
per mechanic, both faces reaching the family by import, no module anywhere in src/ matching the
promotion signature outside the family, and no ../commands/ import inside it. Both are registered
in scripts/test.mjs.
71/02 · One review pass – lanes spawned together, mode read off the wave, and re-work confined to the delta
The review lanes are spawned together, staggered, and bounded
The <review_lanes> region of src/bundle/commands/continue.md instructs the review step to spawn
its lanes together and wait for all of them – the same terms the build fan-out already used – names
the concurrent set as aof-architect + aof-qa + the automated craft pass always, with
aof-designer joining as a fourth only when the story has UI, forbids any lane reading another
lane’s verdict or findings before all have returned, forbids a returning lane cancelling one still
running, staggers the spawns by a handful of seconds to warm the shared prompt prefix, and caps
concurrency at the bound aof work dispatch --list --json reports with the remainder spawned as
earlier lanes return.
Execution mode is derived from the wave, as code
decideExecutionMode() in src/work-loop.mjs resolves (configured mode × --solo × wave size) to
exactly one of EXECUTION_MODES with a reason from the closed EXECUTION_MODE_REASONS set, and
returns null for a wave it cannot count rather than guessing. A wave of one is inline whatever
the static config says; --solo and a configured solo both win over the wave in both directions;
an empty wave is inline and carries emptyWave: true, so a caller can tell “nothing ready yet”
from “milestone finished”. dispatches and spawns are both false for every inline answer – inline
is defined by what it does not do: no worktree, no agent, no dispatch record.
The derivation is taken before anything is dispatched, and widens nothing
The <execution_mode> region of continue.md places the derivation before the first dispatch,
states that it reads the wave the ready set already computed and never recomputes it, that a solo
setting is never widened to orchestrated by a large wave, and that the derivation changes how the
lanes run and never how many lenses exist – no review lens is dropped by running inline.
Round two re-reviews the delta, not the story
The <delta_review> region confines a granted second round to the fix it was granted for:
only the lens or lenses that raised a surviving Blocker are re-spawned, a lens that reported clean in
round one keeps that verdict as the answer of record, and each re-spawned lens is handed exactly
three things – the fix diff, the Blockers that lens itself raised, and the contract clauses those
Blockers cite – and not another lens’s Blockers, not another lens’s round-one verdict, not the
round-one findings below Blocker, and not the story’s whole reads: set. The design lane re-renders
only the surfaces a surviving design-gap Blocker named. One deduplicated Blocker re-spawns exactly
one lens, chosen by claim class (production-defect → architect, locked-contract-violation → QA,
design gap → designer, ambiguous → the lens whose report survived reproduction). The delta is named
by the reproduce-and-deduplicate step, never guessed at.
The gate ladder re-runs after every fix round, and a red ladder consumes no round
The <review_rounds> region states that step 4’s gate ladder runs again before every re-review and
that a red ladder after a fix round does not advance the round counter – the counter advances on
review rounds, not on gate walks.
An amendment ratifies in the beat that raised it
The <amendment_ratification> region of src/bundle/commands/refine.md closes the Decide stage
before the contract fan-out begins, states that each contract has exactly one authoring beat, and
routes every delta by WHEN it was raised across five cases – during the architecture pass, while a
contract is being authored, while the fan-out is in flight, after the contracts are authored, and
after the item is delivered (a superseding ADR in the accepting item’s own contract; the delivered
.feature is never edited, annotated or tagged). The one delta that still earns its round is the one
that would ship a wrong criterion – already a locked-contract-violation and therefore already a
Blocker inside the existing round bound. A re-authoring wave is stated to be no legal response to a
delta: no stage spawns an authoring agent whose only work is re-applying a decision.
The four rendered runtime copies carry all of it
.claude/commands/aof/{continue,refine,code-review}.md, .codex/skills/aof-{continue,refine,code-review}/SKILL.md
and .opencode/commands/aof/{continue,refine,code-review}.md are byte-identical to the live bundle
render, so the prompt an agent actually reads carries <review_lanes>, <execution_mode>,
<delta_review> and <amendment_ratification>.
71/03 · The render lane is gated on renderability – and this milestone supersedes 07’s npx playwright clause
The design lane resolves renderability before it spawns anything
src/bundle/commands/continue.md and src/bundle/commands/verify.md both state a renderability
precondition ahead of the render, evaluated before any render is attempted and before anything is
spawned. It resolves two halves in a declared order: a base URL (--url when given, else
work.ui.baseUrl) and a renderer (work.ui.renderer when declared, else the highest-revision
Chromium found by globbing the platform’s ms-playwright cache). Resolvable means exists and is
executable, not that the key is set – a declared path that is not on disk is the precondition’s own
finding, named with the path that failed, rather than a render-time crash.
A failed precondition is a recorded skip, per surface
When either half is unresolved, or the surface declares no Route, the lane attempts no render at
any breakpoint, records the reason naming the missing key / missing binary / missing Route, returns
INCONCLUSIVE, spawns no designer session and no QA session, and continues. The precondition is
evaluated per surface, so a surface that resolves is still rendered and judged when a sibling
surface does not. A missing baseline is a different recorded reason from a missing precondition half.
The render drives an already-cached Chromium, and carries its breakpoint’s width
The invocation both commands name is
<renderer> --headless=new --disable-gpu --hide-scrollbars --window-size=<W>,<H> --screenshot="<absolute forward-slash path>" "<baseUrl><Route>",
one invocation per breakpoint at that breakpoint’s own width, each writing its own screenshot to a
path made absolute and forward-slashed on every platform. The 390 / 768 / 1280 default survives
the mechanism swap and stays DESIGN-overridable per milestone. A render that exits non-zero, exits
zero but writes no file at the named path, writes a zero-byte file, or does not return within the
step’s own wait is INCONCLUSIVE with that failure recorded as the reason – an “exited zero, wrote
nothing” render is never judged as a screenshot.
work.ui.renderer is a declared config key
schemas/aof.schema.json declares work.ui.renderer as an optional string inside the closed
(additionalProperties: false) work.ui block: absent ≡ discover the highest revision in the
platform’s ms-playwright cache, declared ≡ use this path.
07’s npx playwright render clause is superseded, in 71’s own contract
No file under src/bundle/ names npx playwright. The three milestone-07 arch tests that pinned it –
acd-conformance-verdict-contract, acd-design-conformance-bundled and acd-design-role-split –
are amended in place: no surviving assertion requires the superseded render, while the legs 07
cared about are asserted still enforced (Playwright absent from package.json
dependencies/devDependencies; the designer never told to run the browser; the designer body free
of the render command; the render → hand-off marker carrying its hand-off half). No file under
wiki/work/07_* is written by any story of this milestone – 07’s delivered .feature files are
immutable and are neither edited, annotated nor tagged.
FF-7102 holds all three claims
test/arch/acd-render-lane-is-gated.test.mjs walks every file under src/bundle/ rather than a
listed set, asserts the render invocation itself (not the file) carries --screenshot= and
--window-size=, asserts the precondition precedes the render by order rather than by presence,
asserts QA’s own Playwright harness and toHaveScreenshot regression survive so the control cannot
be satisfied by deleting design conformance, and asserts the three superseded 07 controls were
amended rather than gutted. It is registered in scripts/test.mjs.
The six rendered runtime copies carry all of it
.claude/commands/aof/{continue,verify}.md, .codex/skills/aof-{continue,verify}/SKILL.md and
.opencode/commands/aof/{continue,verify}.md are byte-identical to the live bundle render.
70 · Warm start
A phase spawn is handed its context by value – end to end, for the first time
The chain each story delivered in isolation now exists as one path: a compiled brief (m70/00) that
carries the phase’s actual contract and architecture slice (m70/05, m70/03), launched behind a
deliberately shareable prefix (m70/01), crossing the PTY as one atomic input (m70/06), and landing
in a run record whose spend is read back as a per-phase cache ratio (m70/02). Before this milestone
the spawn brief was {itemRef, worktreeCwd, task, command}, every run record read "brief": {}, and
the prompt was /aof:continue <ref> – an instruction to go and rediscover the tree.
The loop’s economics are observable rather than inferred
aof work observe <ref> reports, per declared phase, the four token buckets, a priced cost, a
cacheRead ÷ cacheCreate ratio and a met/missed verdict against a configured target – and reports
unmeasured distinguishably from a warm phase that divided by nothing. Phase is read from
brief.loop.phase, never minted, so a run the loop shell did not mint reports as having no declared
phase instead of being silently pooled.
The milestone’s own thesis is falsifiable, and has been tested once
This is the state no single story delivers: the SPEC’s baseline figures have a measured successor
taken through the production door (m70/06), so “warm start worked” is a claim that can be checked
rather than asserted. The first check does not confirm it – see Gaps – but the instrument exists, its
target is derived from a measurement, and the next check costs a run rather than a milestone.
Four structural rules bind that did not exist before
ARCHITECTURE.md stays one artifact whose ADRs are addressable in place (m70/03, ADR-006); the
artifact budget refuses an accept rather than warning about it (m70/03, ADR-007); the reviewer is
structurally barred from resuming a build session (m70/04, ADR-008); and aof can never replace the
system prompt, which is the condition the cache flag’s admissibility rests on (m70/01, ADR-004).
The budget that refuses an accept was proved by refusing this milestone’s own
ADR-007 is not a warning that was believed – it bound at this gate. Recording F-21’s ADR-004 amendment
pushed ARCHITECTURE.md to 740 lines against its own 700-line budget, and the → done transition
could not proceed until the document was compacted back to exactly 700 by giving up prose that had
stopped paying its way.
Ten declared controls, each with a red probe that was observed failing
Every control in the register was planted against the live bytes, watched to fail with the message quoted, and restored byte-identically. Seven of the ten extend a guard already in service, where the red probe is the only evidence the extension is armed at all.
70/00 · The phase brief
A pure phase-brief compiler
src/phase-brief.mjs turns supplied document text into one rendered brief: it imports nothing,
reads no filesystem and no wall-clock, and returns a byte-identical result for identical inputs.
A caller-side reader that keeps the compiler pure
src/phase-brief-read.mjs is where the item’s documents are read; compileBriefForItem is the one
function both spawn seams call, so there is one reader and one compiler rather than two
implementations of the same assembly.
A declared, priority-ordered section set, selected per phase
BRIEF_SECTION_PRIORITY is item, story, objective, tasks, fitness, dependencies in that order, and
refine, continue and verify each resolve to their own subset of it rather than to every section
that exists.
A size ceiling enforced inside the writer
compilePhaseBrief cannot return an over-ceiling brief: PHASE_BRIEF_CEILING_CHARS
(PHASE_BRIEF_CEILING_TOKENS 2000 × PHASE_BRIEF_CHARS_PER_TOKEN 4 = 8,000 characters) is the only
ceiling literal in src/**, and no caller applies a bound of its own.
Loud, non-empty truncation
An over-ceiling brief drops from the bottom of the priority list, retains the item section, and
carries a notice naming which sections were dropped or shortened; the compiler never returns an
empty brief and never silently ships the overflow.
Honest degradation, and refusal of a subject-less brief
An absent or whitespace-only section is omitted rather than faked, an empty task-contract set is stated as empty, a read fault contributes an absent section rather than failing a spawn, and a brief with no item ref is refused by the compiler rather than returned.
The brief on the bag that already existed, at both spawn seams
src/commands/drive.mjs and src/mesh-worker-execution.mjs both set an additive context key
beside the four existing brief keys (itemRef, worktreeCwd, task, command), which keep their
meaning; the driver’s signature stays driveInteractiveClaudeSession(brief, options) and it reads
brief.context. No rival context/payload/digest object exists.
The context arrives as input, not as an instruction to go and read
composePhaseBriefInput appends the rendered brief (and its truncation notice, when present) to the
command the driver types into the session, so the phase receives the content by value.
Absence is benign at both seams
A caller that supplies no compiled context produces a byte-identical launch to the pre-story behaviour, so the two seams were landable independently and every existing run record stays readable.
Three declared controls in service
FF-7001 (one brief bag), FF-7002 (the compiler is a pure leaf and the driver’s export set stays the frozen seventeen) and FF-7003 (the bound is in the writer) are armed, registered in the suite, and each was observed failing against a planted violation.
70/01 · A launch whose prefix is shareable
The stable-prefix flag on every production launch
resolveInteractiveDriverLaunch builds its argv with --exclude-dynamic-system-prompt-sections
beside --append-system-prompt, so cwd, environment info, memory paths and git status ride the first
user message instead of the cached system prompt – for both spawn callers, unconditionally.
The append form is the only system-prompt form in src/**
No --system-prompt replacement argv is constructed anywhere in the source tree, which is the
condition under which the stable-prefix flag is not silently inert.
A session model and effort resolved per phase and passed explicitly
src/session-model.mjs resolves --model and --effort from work.agents.session
({ models: { <phase>: <model> }, effort: { <phase>: <level> } }) and src/commands/drive.mjs hands
them to the seam, so a phase-scoped launch’s cache key is a configured decision rather than whatever
the session defaulted to.
Two model surfaces with no join between them
The render-time role map (work.agents.models, milestone 30) and the session path
(work.agents.session) are resolved by different modules and neither reads the other’s path;
SESSION_MODEL_CONFIG_PATH is the single literal naming the session path in src/**.
Absence is silence at the launch
No work.agents.session, no routing entry for the phase, an unknown phase name, an empty model
string, or a work.agents.session that is not an object all resolve to {} – the launch passes
neither flag and is byte-identical to the pre-story spawn.
The one-hour prompt-cache window held by the spawn rather than by the billing mode
The launch environment carries ENABLE_PROMPT_CACHING_1H=1, set after the IDE-attachment scrub,
so the scrub cannot delete it and the window is the same on a subscription as on usage credits.
Per-phase routing bound to the phase-scoped caller
The local drive command carries --model/--effort; the mesh worker dispatch – assignment-scoped
and carrying no phase – carries neither, while still receiving the stable-prefix flag and the
one-hour window.
Three declared controls in service
FF-7004 (aof never replaces the system prompt, and the append form and the flag travel together),
FF-7005 (one launch seam – no module outside it assembles a claude argv) and FF-7006 (the two model
surfaces resolve from distinct paths) are armed, registered in the suite, and each was observed
failing against a planted violation.
70/02 · Cache economics per phase
The cache ratio, reported per phase from the run record
aof work observe reports cacheRead ÷ cacheCreate for every phase of an item’s runs, derived from
the spend.tokens buckets milestone 68 writes and never re-counted from a transcript.
Phase read from the loop’s own declaration, with no rival column
Runs are grouped by brief.loop.phase and by nothing else; a run the loop did not mint reports under
an explicit “no declared phase” row carrying its own figures, rather than being folded into a phase
or dropped.
An unmeasured run is unmeasured, never a zero
A run carrying no spend envelope is counted in unmeasuredSpend, excluded from the ratio, and
reported as unmeasured – it is never a 0.000 that would read as a cache failure.
Three non-arithmetic cases each carry their own reported state
cacheState is measured, unbounded or unmeasured: cache reads with no creations is a warm phase
reported ∞ (warm, unbounded) rather than a division by zero; creations with no reads is a measured
0.000 distinguishable from an unmeasured phase; measured runs that populate neither cache bucket are
unmeasured, because there is no cache signal to divide.
A stated target turns the ratio into a per-phase verdict
work.observability.cacheRatioTarget yields met or missed per measured phase – at the target is
met – and the target it was judged against is stated in the report beside the verdicts.
An absent or unhonourable target is a first-class silent case
No target, a non-numeric target and a negative target all resolve to cacheTarget: null: no verdict
is stated, none is invented, and the ratio is reported unchanged in every case.
An unmeasured phase is never judged missed
A phase with no cache signal carries no verdict under a configured target – cacheVerdict is null
and the row reads –, so a missing measurement cannot be mistaken for a failed one.
The verdict changes nothing about any run
applyCacheTarget returns a new rollup and mutates neither its input nor any run record; no run is
failed, retried, capped or killed on a verdict. The milestone records and does not enforce (ADR-008).
The reporting leaf still writes no record and adds no key
src/work-observe.mjs reads what milestone 68 made true; src/run-store.mjs and its seventeen src/
dependents are untouched by this story, and the run record’s shape is unchanged.
70/03 · A story reads its slice
adrs: is a story frontmatter key, optional and additive
A STORY.md may carry adrs: [ADR-006, ADR-007]. Absence is the pre-existing behaviour exactly –
every one of this stream’s 201 declaration-free stories validates untouched – and the key is
de-duplicated in declaration order rather than sorted.
A declaration that does not resolve is a validate finding, by ref and by file
aof work validate reports ADR declaration "ADR-999" does not resolve in 70/ARCHITECTURE.md for an
id the story’s own milestone does not carry, and ADR declaration adrs must be an inline list for any
present-but-non-list shape. A story with no parent milestone is told it has no architecture to resolve
against rather than borrowing an identically-numbered ADR from another item.
One ADR is addressable inside the one architecture document
extractAdrBlocks(text, ids) returns the requested ## ADR-NNN blocks from a single
ARCHITECTURE.md text, in document order, byte-preserved, ending each block at the next h2 – so a
following register or partition section never leaks into the last ADR. It matches exact ids only:
ADR-00 never resolves to ADR-001, and every requested miss is reported.
Fenced and commented headings are examples, not document structure
The one structural-h2 scan behind both ADR addressing and the register fallback ignores headings
inside code fences and HTML comments, so a documentation example of a ## ADR-NNN heading or of a
## Fitness functions register is neither an address nor an early block boundary.
A story’s brief carries the slice it declared; a declaration-free story carries the register
compileBriefForItem puts the declared ADR blocks in the brief’s architecture section, names any
unresolved id inside it, and falls back to the milestone’s ## Fitness functions register – never the
whole architecture document – when a story declares nothing. A milestone’s own brief still carries the
full architecture record.
ARCHITECTURE.md remains one artifact, and a control says so
WORK_ITEM_ARTIFACTS enumerates exactly one architecture entry, REGISTER_BLOCKS’s declaring file for
the fitness register is still ARCHITECTURE.md, and the extractor names no sibling per-ADR file or
directory. FF-7008 fails if any of those three change.
The artifact budget binds at accept, on the item being accepted
aof work status <ref> done runs the same budgetGroup, over the same measurement and the same
resolved budgets as the advisory sweep, with the accepting ref injected, and refuses the transition
with artifact-budget-exceeded (409) naming artifact, measured lines and budget. No other status edge
applies the gate.
Nothing already accepted is re-litigated, and the sweep is unchanged
An open item’s over-budget artifact is still a warn, the stream-wide sweep still reports every
overage as a warning, and an artifact exactly at its budget is healthy. The 15 milestones already past
700 lines stay green.
The budget numbers keep one home
budgetsFromConfig is the single resolution path for both the warning and the refusal, so the two
gates cannot be given different numbers.
The frozen work core is left as it was found
The declaration’s resolution lives in src/commands/validate.mjs, not in src/work.mjs; the core
validator’s bytes at HEAD are the bytes it had at 70/00’s merge.
70/04 · The fix loop resumes the build
A fix resumes the build session that wrote the code
When the loop’s review gate produces a fix, the drive command resolves the session id recorded on that
particular build run and launches with --resume <id>. The two halves that existed separately – the
driver’s resumeSessionId option and the run record’s persisted sessionId – are joined.
A resumed fix is handed the findings and the diff, not the tree
The compiled brief is attached only when no resume target resolved. A warm fix receives the review findings and the change under review; the session it re-enters already holds the tree.
The change under review is the build’s delta, not the working tree
The diff handed to a fix is taken between the build’s recorded baseline and the tree it produced, so staged and unstaged work that predated the build is excluded while the build’s own delta is retained.
An unresolvable target degrades cold rather than refusing to run
A pruned transcript, an absent session id, a session id that is not a bare basename, and a run recorded on another node all resolve to no target; the fix then runs as a cold spawn with a compiled brief. A fix that cannot be warmed still runs.
The cold fallback is a visible retry record, never a hidden second process
A positively unavailable target produces its own retry record rather than a second process inside one attempt, and a resumed process that fails for a generic reason is not re-dressed as a cold one.
A resume whose spend baseline cannot be established says so
When the resumed session’s transcript cannot be snapshotted, the run reports
resume-spend-baseline-unavailable and the spend settle is skipped by name, rather than being stamped
from an unreliable baseline.
The resume target is derived from the phase, never from a caller
A caller’s resumeSessionId is stripped from the options before they reach the launch, and a fix
payload is admitted only on the continue phase. A refine or verify launch supplied with both a
raw resume id and a rich fix payload resumes neither.
Node placement is decided by the loop, not by the executor
admitResumeBuildRun returns the build run only when it was recorded on the node now running; the
drive command receives either an admitted local run or null and makes no placement decision of its own.
A review is never resumed, and a control enforces it
FF-7007 drives the real production commands rather than the resolver helper, so a helper that returns null cannot stay green while a caller still forwards a build session into a review launch.
Interruption preserves the pending fix
A fix interrupted after its review keeps its identity and its findings through the existing run lineage, and the review-round cap survives repeated interruption and resume.
Bookkeeping is unchanged by warmth
A resumed fix mints its run, attributes its session, settles its spend, and carries its attempt and retry lineage exactly as a cold one does.
70/05 · A brief that carries what the phase must satisfy
Every brief section declares a reduction, and the declaration is exported and frozen
src/phase-brief.mjs exports BRIEF_SECTION_CONDENSERS, BRIEF_NON_CONDENSABLE_SECTIONS and
BRIEF_BOUNDED_CONDENSERS, all Object.freezed. The union of the first two is exactly
BRIEF_SECTION_PRIORITY and their intersection is empty, so a section cannot exist without a
declared reduction – an eighth one added without either declaration fails on its first commit.
A section too large for the budget is condensed, never dropped for size alone
Every section is offered its condenser before it is considered for sacrifice. Across all 219 story
folders in this repo’s wiki/work/, compiled in all three phases through the production reader, the
only disposition the compiler emits is condensed – 0 sacrificed and 0 unshippable in 657
compilations.
A condensed section states its own count, not merely its size
A bounded condenser reports kept / total / omitted, and the notice renders it in words –
“All 27 scenarios listed, none omitted”, “2 of 3 decisions listed, 1 omitted”. A condensed
section that named none of its own entries is therefore visible as such rather than indistinguishable
from a full one; no such section exists in this stream today.
Sacrifice runs strictly bottom-up, so one miss no longer cascades
A section that does not fit no longer evicts the sections beneath it. A section whose condensed form exceeds the whole ceiling is reported as unshippable and costs nothing below it.
The truncation notice distinguishes three dispositions and points at the source
CONDENSED (shortened to fit – still carried) is distinguishable from dropped and from unshippable,
and each condensed section names the form that survived and the file the complete text is read in.
The compiler is handed addressed extracts, never documents
src/phase-brief-read.mjs binds each section to its own named …Section const, produced by an
imported addressing helper, before the compilePhaseBrief call. A milestone’s specification
reaches the brief as its ## Objective block and a story record as its ## User story block –
never as the whole file, and never carrying frontmatter or scaffold comments.
The ceiling is unchanged and still has one home
PHASE_BRIEF_MAX_CHARS is 8,000, enforced inside compilePhaseBrief, and no second ceiling literal
exists outside the compiler. No brief in the stream exceeds it in any phase.
The contract reaches every story in this repository that has one
211 of the 211 story folders carrying a tasks/ directory receive their acceptance criteria in every
phase, and 194 of 219 receive their milestone’s architecture or fitness register. Mean brief size is
7,440 / 7,234 / 6,753 chars of 8,000 at refine / continue / verify.
A story’s declared ADR slice reaches its brief on real data
A STORY.md carrying adrs: receives those ADRs’ headings and decision passages, counted, in place
of the milestone register. 70/05 is the only such story of the 219 and its brief carries all three
of its declared ADRs’ headings.
Two record shapes the reader previously answered nothing for now resolve
All 34 stories declaring a non-empty depends: receive a dependencies section at refine, and the 3
records writing ## User Story rather than ## User story receive a story section.
The brief is pinned against this repository’s own work stream
test/brief-pinned-to-the-stream.test.mjs compiles briefs for real items under wiki/work/ and
asserts invariants that hold for any real item rather than byte counts for particular ones. Its
declared-slice assertion fails if no story in the stream declares any ADRs, so the path cannot pass
by emptiness.
Two structural controls guard the declarations and the call site
FF-7009 fails if the frozen declarations stop covering BRIEF_SECTION_PRIORITY exactly; FF-7010
fails if any section value at the compilePhaseBrief call site is an identifier bound directly from
a disk read. Both were observed failing at this gate against the live bytes.
70/06 · The saving is a number, not a claim
The stream carries a measured phase, where every run record said unmeasured
Run 20260824T101918473Z-0005 is settled done with brief.loop.phase: "continue" and a spend
envelope carrying all four token buckets, the model, the effort key and a priced cost source
($4.0940, price-table-2026-08-v1). aof work observe reports a cache ratio of 12.921 for that
phase instead of unmeasured.
The milestone’s before-and-after is a committed artifact, not a claim in a commit message
observability/snapshots/2026-08-24T10-28-17-153Z/report.md holds every figure this milestone is
judged by, each re-read rather than quoted: a 143-row baseline across six pre-70 milestones
(2026-08-08 → 2026-08-14), the after side taken by the same per-agent instrument, the delta for each
headline figure, four figures reported not taken with a reason each, the sample size behind every
number, and the confounders that bound them.
A delta that went the wrong way is recorded with its measured direction
The per-agent cache read÷create ratio is stated as 23.936 → 10.383, a regression, with the structural reason it is probably a lifetime artefact and the explicit statement that the comparison cannot settle whether prefix sharing improved. The comparison is not restricted to the figures that improved.
work.observability.cacheRatioTarget is a number derived from a measurement
Set to 13 – ceil(12.921), the smallest integer strictly above the worst measured phase – and
recorded with the run count, phase count and date it was taken over. The report states it, and states
missed for the measured phase.
The cache-ratio verdict actuates nothing
cacheRatioTarget and cacheTargetStatus are read in exactly two modules, both reporting surfaces
(src/commands/observe.mjs, src/work-observe.mjs). No loop, drive, run-store or transition module
reads either: a missed target fails, retries, caps and kills no run.
A directive crosses the PTY as ONE atomic input at any length
The body is written as a bracketed paste and the Enter follows as its own write, after a settle derived from the caller’s readiness delay. A 43-line directive that previously arrived as eight separate user turns now arrives as one; the live transcript records it as a single 1,249-character message with no ESC byte in the content.
A directive body cannot close its own paste
An end-of-paste sequence occurring inside assembled brief content is stripped before the write, with a
coded directive-paste-marker-stripped degrade, so content can never be reinterpreted as protocol.
A settled run is never sent a late Enter
The queued submit reads the settle flag at fire time as well as being cleared at cleanup, so a PTY that exits on the body write cannot receive a keystroke afterwards.
A worker session no longer presents as a child of the operator’s Claude Code session
The launch env is scrubbed of CLAUDECODE, every CLAUDE_CODE_* key, CLAUDE_PID, CLAUDE_EFFORT
and CLAUDE_AGENT_SDK_VERSION. With those inherited, a real turn ran to completion and no
transcript was written at all, leaving the session id, the spend envelope and the phase ratio
permanently absent; scrubbed, the transcript appears ~2s after the directive write. Everything aof
sets for itself is set after the scrub, so the widened scrub cannot eat the 1-hour cache window.
The folder-trust pre-write lands under the key claude actually reads
Trust keys are normalised to the separator spelling claude uses. ~/.claude.json held 661
backslash-spelled project keys carrying exactly one field each – aof’s own write, never read – beside
claude’s own forward-slash keys for the same directories.
The shared PTY double separates the wire from the input, and consumers no longer re-derive it
createScriptedPty exposes pty.writes/rawChunk as the bytes written and chunk as the input a
paste-aware terminal would present. Suites that previously carried their own paste-strip now read the
view they mean, and the local re-derivations were deleted rather than duplicated. A transport change
invalidates doubles in one place instead of in every suite that models a spawn.
69 · Loop bounds
The runtime enforces bounds at all, and their values are declared rather than assumed
Before this milestone there was no token limit, no turn limit, no wall-clock limit and no cost
ceiling anywhere in aof, and the interactive claude path had no timeout of any kind. There are now
seven declared bounds resolving from work.loop.* through one home, four of them deadlines the
runtime enforces against a process it holds – see [[m69/00]] for the declaration and its refusals,
[[m69/02]] for the enforcement.
Liveness and progress are two signals with two different consequences
This is the milestone’s central claim and no single story makes it. A run that has gone silent is killed and retried against the liveness deadline ([[m69/01]] supplies the signal, [[m69/02]] the consequence). A run that is alive and getting nowhere is reset with a summary and, on repeated resets, escalated with its work preserved ([[m69/03]] supplies the ledger, [[m69/06]] the caller that consults it). The two are deliberately not merged: the eight-day zombie and the two 11h07m burns are different failures, and a heartbeat alone catches only the first. One signal carries one terminal behaviour throughout – a block the pending-question detector misses costs an attempt rather than parking, because the runtime cannot read human intent out of silence.
Concurrency is a property of the system on both surfaces, and waiting no longer costs capacity
A slot is acquired before work is accepted – the local slot is the git lane, counted before one is materialised; the mesh tick consults its counted set before it sends ([[m69/04]]) – and a run blocked on a human ends its process, releases that slot and resumes the same conversation when the answer arrives ([[m69/05]]). The two compose into the milestone-level fact: the largest recorded lost-time category (107h28m in m47, 58h05m in m48, 46h38m in m50) no longer holds a slot while it waits.
The bound four other milestones were waiting on has a value
Milestone 54 was not-started in its own words because “it enforces a bound; it does not invent its
value”, and 53, 62 and 65 deferred to the same arc. Review is N = 1 by default with a named
blocker admitting a second round; build is bounded on failure-to-progress rather than an iteration
count; maxAttempts stays 3, paired with a total-duration ceiling. Every framework loop record’s
ceiling: now resolves, and a ceiling whose pointer resolves while nothing reads the bound is a
named finding rather than a declaration ([[m69/00]], [[m69/06]]).
The run record gained nothing, and that is enforced rather than intended
src/run-store.mjs has seventeen src/ dependents. This milestone adds no key, no state and no
transition to it: its key set and LEGAL_TRANSITIONS set are byte-frozen by SHA-256 under FF-6908,
and there is no lease table, claim file or per-slot persisted object anywhere. A slot is a count over
rows and lanes that already exist.
Enforcement is out-of-process, and no bound is spoken to the model
No --max-turns, --max-budget-usd, -p, --print or --output-format argv is constructed for
the interactive driver anywhere in src/. The deadline is armed against a held process handle and
reaches term.kill(). Telling the agent about its budget is milestone 71’s subject and is refused
here by ADR-004.
Eleven declared controls, each with a red probe that was observed failing
The register in VERIFICATION.md carries all eleven with the plant and the message observed, not a
placeholder. Four of them extend a guard already in service, where the probe is the only evidence the
extension is armed at all.
69/00 · The declared cap
Every loop deadline and cap resolves from one home
src/loop-bounds.mjs resolves all seven declared bounds from work.loop.* – heartbeat,
schedule-to-start, start-to-close, schedule-to-close, review rounds, build no-progress rounds and
progress max resets – each with a documented default, and a malformed declared value falls back to
that default rather than crashing the caller.
The two bounds that already had homes still have them
work.dispatch.concurrency keeps its single pre-existing reader and work.autonomous.maxAttempts
keeps its closed reader set; neither was moved into the new leaf, and the existing cap single-home
guard was extended to cover the leaf rather than joined by a sibling.
A second review round is refused unless a blocker is named
decideReviewRound admits round one and refuses round two without an explicit blocker claim,
reporting which blocker class would admit it; an exhausted cap halts rather than looping.
The declared review cap binds the production re-review path
decideReviewGate (src/work-loop.mjs) consumes the gate’s real { path, problem } finding shape
and is called from src/commands/loop.mjs, so the cap is enforced where re-review actually happens;
the review-round count is counted apart from the engine cycle, and a blocker is an explicit claim
rather than an inference from finding prose.
No framework loop record declares an uncapped ceiling
Every ceiling: under src/bundle/loops/*.md is none or a pointer list, and a config: pointer
naming a key no resolver resolves is reported as a finding rather than accepted as a declaration.
69/01 · Heartbeat by consumption
A driven run stamps its own liveness from the tool results it produces
A bundled PostToolUse hook (src/bundle/hooks/run-heartbeat-enqueue.mjs +
claude-run-heartbeat.json) enqueues a liveness record for the run named in its environment, and
src/run-heartbeat-consumption.mjs drains that queue into run-store.heartbeat() – which had zero
callers in src/ before this story. heartbeatAt now moves while a run works, rather than staying
null from mint to terminal.
The hook derives nothing and cannot break the session it rides in
The hook body imports nothing from src/, opens no store, derives no workspace identity from cwd,
and exits 0 on every path – the clause set already proven for the artifact-sync hook, now enforced
over every bundled hook body as a class.
The run’s identity is handed in, never derived
The run id reaches the hook through the spawn environment (OTEL_RESOURCE_ATTRIBUTES), which is
per-process and untracked, so the absolute-path-in-a-tracked-file failure that a git worktree
inherits cannot recur through this route.
The periodic scan reaps on the absence of liveness
The reclaim tick consumes the queue and reclaims a run whose liveness has gone stale against the one declared threshold, through the reclaim edge that already existed rather than a third copy of it.
One staleness constant answers for every consumer
The reclaim threshold, the loop shell’s default and the session driver’s undeclared-completion idle
window all resolve through DEFAULT_HEARTBEAT_MS in src/loop-bounds.mjs; src/ holds exactly one
15-minute literal, and declaring work.loop.heartbeatMs moves all three together.
The dual-staleness gate has a real second signal
dualStalenessDecision reads the linked run’s heartbeatAt before falling back to updatedAt, so
the AND of node presence and run liveness runs on two signals for the first time; a fresh node with a
silent run stays hands-off, and an absent presence record is unknown liveness rather than staleness.
69/02 · The four deadlines
An attempt has a deadline aof enforces against a process it holds
driveInteractiveClaudeSession – the only function in this repo holding a live process handle –
arms the per-attempt (start-to-close) and liveness deadlines against that handle and reaches
term.kill() on expiry, resolving a retryable timeout. All three production callers supply the
policy from the workspace: src/commands/drive.mjs:212 and src/mesh-worker-execution.mjs:1689,2230,
each through loopBoundsFromConfig(ws).
The total-across-attempts ceiling gives up rather than retrying
Schedule-to-close escalates instead of spawning another attempt, and preserves the worktree for triage rather than cleaning it up.
A startup grace suspends the liveness deadline and nothing else
A clone plus a dependency install produces no tool-result events at all; the grace covers that window without pausing the wall clock, so a five-minute allowance does not become an unbounded one.
No bound is spoken to the model
No --max-turns, --max-budget-usd, -p, --print or --output-format argv is constructed for
the interactive claude driver anywhere in src/; enforcement is entirely out-of-process.
The exit vocabulary 68 fixed is reachable for the first time
timeout was already classified retryable and already a member of 68’s exit vocabulary; this story
is what makes it – and its siblings – a state a run can actually reach.
69/03 · Progress, not liveness
An attempt’s progress is sampled from deterministic signals only
src/loop-progress.mjs samples files touched, lines changed, commits made and the failing-scenario
count from git status --porcelain inside the lane’s own tree – the reader the lane sweep already
used – and nothing else. No model, prompt or agent surface is imported, so the sample is a
measurement rather than a judgement.
The ledger is append-only and invisible to the run reader
Samples land in a runs/[<node>/]<runId>.progress.ndjson sibling through appendFile; nothing opens
an existing sample file for truncation. readRuns skips every entry that is not *.json in both the
flat and the node-partitioned branch, so the ledger is invisible to the god-node’s reader by
construction, and src/run-store.mjs – seventeen src/ dependents – gains no key.
Consecutive no-progress samples reset the attempt, and repeated resets escalate
At maxStalls the policy resets the attempt and carries a summary forward; at maxResets it
escalates. Both bounds are declared in work.loop.* and resolved through [[69/00]]’s leaf.
The build loop’s ceiling is a failure-to-progress rule, not an iteration count
decideBuildProgress stops the build when consecutive measured rounds show no reduction in the
failing-scenario count – the derivative of the number the build loop already drives to zero – rather
than after an arbitrary N.
69/04 · Slots before work
The bounded worker pool has a production caller
dispatchReadySet – the only bounded worker pool in this repo, and one with no production caller
before this story – is reached from src/commands/dispatch.mjs:104, so at most bound lanes are
materialised at once and the remainder dispatch as lanes free.
The local slot is the git lane, counted before one is materialised
The dispatch door derives its counted set from the lanes git itself reports under the dispatch root
(inspectDispatchLanes, prunable excluded) and reads it before any lane is opened – before both
the pool call and resolveDispatchLane. Reusing an existing lane is free; each new open counts
against the next member; a member over the bound is refused with a code in the reported result
rather than opened, thrown or dropped.
No occupancy fact is persisted outside git’s own worktree list
There is no lease table, no claim file, no per-slot persisted object and no new run-record column –
a slot is a count over rows and lanes that already exist, not an object with its own lifecycle.
src/run-store.mjs is byte-unchanged from milestone 68.
The mesh control tick is bounded
The tick derives its counted set from assignment rows and tests it before the send; a row over the
bound stays assigned for a later tick – the same leave-it-assigned branch the not-connected case
already used, so a send that did not go out is retried rather than silently dropped.
Occupancy has one home and every door that opens work reads it
assignmentOccupiesDispatchSlot is the single definition of the accepted/running/needs-input
predicate; no other module re-spells it, and no path treats the unpersisted once-guard set as
occupancy. Both doors that can put a target back to work – the tick’s directive send and the
parked-answer resume – read countDispatchSlotsByTarget against the bound before sending, and the
whole-src closure enumerates those doors, so a new one fails the gate rather than bypassing it.
Admission survives a scheduler restart
Occupancy is proved by the row’s own state rather than by in-process bookkeeping, so accepted and running rows still count after the scheduler restarts, and a parked run’s answer re-acquires a slot through the same count or is refused.
69/05 · A blocked run releases its slot
A run waiting on a human ends its process and releases capacity
A pending human-input tool call is detected directly, so the park does not wait out the idle window:
the PTY exits, the park is published, and the row leaves the counted set – so a machine no longer
sits idle behind a question nobody has answered. createMeshParkResume (src/mesh-park-resume.mjs)
is reached from src/mesh-worker-execution.mjs:132.
The park is published once, after a confirmed exit, and releases nothing until the row carries it
No assignment-status frame carrying the needs-input code is constructed from a path holding a live
PTY handle; every park publish sits on an exit-confirmed settle path; and the durable
assignment.reported reactor names the park edge (running + needs-input) in its admitted set
rather than refusing it as non-terminal. A process whose exit cannot be confirmed is not reported as
parked, and the reason is recorded.
Answering resumes the same conversation and the same run record
The park’s code is cleared before a resumed process is spawned, never after; the resume is admitted
before a process exists; and the run stays running with no new state, no new failure reason and no
retry lineage – a needs-input park is the same run resuming, never a second record.
Silence is not a park signal
The explicit detector is the whole of the park. A block the detector misses is observationally just
“no tool results”, which is [[69/02]]’s liveness deadline – killed and retried – and exhaustion
preserves the worktree under on-max-attempts: pause. One signal carries one terminal behaviour, and
no fifth deadline was introduced.
69/06 · The ledger binds the build loop
The build loop writes a progress ledger
Every measurable build round the loop drives appends a deterministic sample – files touched, lines
changed, commits made, failing count – to that run’s own runs/[<node>/]<runId>.progress.ndjson
sibling, through src/loop-progress.mjs; src/commands/loop.mjs and src/work-loop.mjs are its
production importers, and the run record’s key set is unchanged.
A round that cannot be measured leaves no trace and no consequence
A round whose failing count could not be measured appends no sample at all, and a git call, lane or ledger write that fails degrades that round rather than failing or halting the build.
A stalled attempt is reset, and a repeatedly-reset one escalates
At the declared stall bound the loop resets the attempt and carries its summary into the next
attempt’s brief, spawning a fresh session rather than resuming the stuck one and leaving the
uncommitted work in place; at the declared reset bound it halts on progress-exhausted for a human,
with the work preserved for triage and the resets tally reconstructed from the run’s own persisted
brief on --resume.
A build that has stopped reducing its failing count halts
decideLoopProgress consults the sample policy first and the failing-count derivative only when the
sample policy says continue; consecutive measured rounds with no reduction reaching the bound stop
the build on no-progress with cycles still remaining on the engine cap, and the failing count is
work:grade’s own cases.failed, with indeterminate read as an absent measurement rather than as
a count of zero.
An unmeasured round is dropped from the derivative
An absent round adds no stall and clears none: the bound counts non-reducing transitions between
measured rounds, so 9, 9, –, 9 halts on the same two transitions as 9, 9, 9, and a build nothing
could measure is an unbounded continue that reports itself unmeasured.
Every framework config ceiling is checked for a consumer, not just a resolver
test/arch/acd-progress-ledger-consumed.test.mjs walks every config: ceiling on every framework
loop record and names a loop-ceiling-unconsumed finding for any whose pointer resolves while no
production module outside the bound’s declaring home reads it; all four resolve to readers today.
This repository can measure a round
.aof/aof.config.json declares a work.rubric – a runnable command, a report format and a floor –
so a build round driven here yields a failing count rather than an indeterminate grade and the ledger
is written on this tree. The declaration’s own command and floor are milestone 54’s surface and have
already moved once (F-69-V18); what this story delivers is that a declared rubric is consumed, not
which command it names.
68 · Loop telemetry
aof measures its own spend on stored keys, end to end
The chain from spawn to answer runs on values written at the time they were true, with no
reconstruction step anywhere in it: the driver stamps a session id and OTel resource attributes at
spawn (m68/01), the settle copies that session’s own token counts and prices them once
(m68/02), and observe resolves an agent run to an item by joining on the stored session id
(m68/03). No figure aof reports about its own cost is derived by matching free text.
The four measured defects in this milestone’s objective are each answered, and each answer is a different mechanism
The SPEC opened on four evidenced failures of the transcript miner. They did not share a fix:
double-counting is answered structurally, because a run belongs to one session which belongs to one
record (m68/03); the overwriting of cited evidence is answered by writing a new timestamped
artefact per run (m68/05); the classifier blind to this repo’s toolchain is answered by
classifying on what a result EMITS rather than on the command name (m68/03); and the inability to
answer for one story is answered by a resolver that reads one level into stories/ (m68/04).
Absence is reported as absence, at every layer that could have guessed
One posture holds across the whole milestone rather than in any one story: a value that was not
measured is never folded in as zero and never inferred. spend: null reads not measured and is
distinct from a measured zero (m68/00); a run whose session never reported an id stays null
rather than being synthesised (m68/01); an agent run with no resolvable session is counted as
unattributed rather than assigned or dropped (m68/03); and observe carries unmeasuredSpend as a
count beside every total it prints, so a small total and an unmeasured one are distinguishable on
the face of the report.
Every production path to the run fact goes through the transition seam
Both of the driver’s production callers – the local drive command and the assignment sink – mint
and settle through src/effects/run-transitions.mjs, so every run this milestone added raises its
run.started / run.completed event and inherits the declared cascade. This was not true when the
milestone’s stories were individually accepted: 68/01 first wired the drive command straight to the
store, which turned milestone 42’s two ledger controls red and was caught only at this gate
(F-09, ADR-009).
68 records; it does not act
No cap, budget, timeout, reaper or routing decision landed. run-store.heartbeat() is still
unwired, nothing branches on exitReason, and no dispatch, lease or slot behaviour changed – the
scope refusal ADR-008 declared is intact at the close, and it is what makes milestones 69–72 a
measured before/after rather than a rewrite of what they are about to enforce.
68/00 · The spend-bearing run record – a sixteenth key, and a writer that refuses a lie
The run record’s sixteenth key
Every run record aof mints or reads carries spend as its sixteenth and last key – a validated
envelope or null – and a fifteen-key record written before this milestone normalizes forward with
spend: null, every prior value verbatim.
Not-measured is distinguishable from measured-zero
A record whose spend is null and a record whose spend.costUsd is 0 are two different
answers, readable apart from the record alone without consulting anything outside it.
Token buckets are a closed set the writer enforces
input, output, cacheRead and cacheCreate are the only token keys a spend may carry, and
run-store.settleRun() refuses – with a typed error, persisting nothing – any envelope that is
partial, carries a fifth token key, or holds a negative, fractional or non-numeric count. The
enforcement exists in exactly one place: the write path.
Cost is stamped once and carries its provenance
costUsd is written only at settle, alongside costSource (the closed set reported | priced)
and a priceTable version that is present exactly when the cost was priced; no read path in
src/ recomputes a cost from buckets, so a later price-table correction changes future stamps and
never rewrites a settled run.
A closed exit vocabulary that decides nothing
spend.exitReason records how a run ended over a seven-member closed vocabulary, and a value
outside it is refused at write. Two runs identical but for their reason differ in nothing else: no
retry, kill, bound or state change is triggered by any member.
One phase authority, structurally
Neither the run record nor the spend envelope carries a phase key, and no module in src/ reads
one off either – brief.loop.phase is the single authority.
The additive-discipline guard actually runs
test/arch/acd-run-record-node-additive.test.mjs is now spread into scripts/test.mjs’s suite
registry, not merely imported, so the record’s key freeze is enforced on every run rather than
silently unexecuted.
68/01 · Attribution at spawn
The run record carries the session it ran as
sessionId is populated on the run record at the moment the driver captures it, at both of the
driver’s production callers – src/commands/drive.mjs and src/mesh-worker-execution.mjs. The key
the record has modelled since milestone 19 now holds a value rather than null on every row.
A session id is recorded or absent, never invented
A run whose session never reports an id reads sessionId: null; no id is derived, copied from a
sibling, or synthesised from a path.
The id is written once, and re-presenting it is a no-op
recordSessionId with a byte-identical id rewrites nothing – updatedAt, state, attempt and
the retry lineage are untouched – so the mid-run write and the pre-settle write can both run
without churning the record.
The attribution write cannot outlive the settle it races
The drive caller awaits the persist inside onSessionIdCaptured, which the driver awaits inside the
watch chain that finish awaits before settling. A captured id is therefore on the record before
completeRun runs, and no in-flight whole-record write can land afterwards and restore a pre-settle
snapshot. The mesh-worker caller reaches the same guarantee by allSettled-ing the persist against
the up-channel update.
Neither the attribution write nor the assignment update is conditional on the other
A failing assignment update still leaves the id on the record and still lets the run settle; a failing persist is reported rather than swallowed and does not stop the settle.
work:drive-<phase> mints and settles a run record, through the transition seam
The bare phase-driver commands, which previously minted none, now mint a run, persist the captured
session id, and settle done/failed – needs-input settles failed with reason needs-input,
because a one-shot local drive cannot service a parked session. The mint and the settle go through
transitionRunStart / transitionRunComplete (src/effects/run-transitions.mjs), never the bare
store, so each raises its run.started / run.completed event and inherits the declared cascade –
the same doors the sibling caller src/mesh-worker-execution.mjs has used since milestone 42.
The settle seam can price a run without publishing it
transitionRunComplete takes projectsDir as an opt of its own, defaulting to
workspace.projectRoot. A caller that needs its run’s spend stamped from the transcript tree no
longer has to pass workspace, which would also set the event’s workspaceRoot and turn the
completion into a global work-snapshot publish. The drive command is priced and does not publish;
every pre-existing caller is unchanged.
The attribution build and the race-free session-id persist each have one home
buildRunAttribution (src/otel-attribution.mjs) derives the milestone/story attribution object,
and captureSessionIdOnRecord (src/run-session-capture.mjs) is the persist-without-racing-the-settle
shape. Both production callers – the drive command and the assignment sink – call them rather than
carrying hand-copies, so the F-04 guarantee cannot be present in one caller and absent in the other.
Every spawned session is labelled with the work it belongs to
OTEL_RESOURCE_ATTRIBUTES is set at spawn carrying run.id, story.id, milestone.id, phase,
machine.id and worktree.id, alongside CLAUDE_CODE_ENABLE_TELEMETRY; a run with no declared
phase carries no phase attribute rather than a fabricated one. The attributes are applied after
the existing IDE-attachment scrub, so the scrub still removes exactly what it removed before and
removes none of these.
aof ships no OTLP receiver
No module under src/** opens a listening socket for, parses, or serves an OTLP payload. The OTel
surface is env-set-at-spawn only, and every figure this milestone produces is correct with no
collector running anywhere (FF-6808).
68/02 · Spend ingest at settle
A run’s spend is stamped as it settles
src/run-spend-ingest.mjs reads the session’s own transcript at settle and writes the spend
envelope onto the run record. completeRun applies the state transition first and then stamps,
so the spend a run reports is written while the transcript that evidences it is still there.
The four token buckets are copied, not computed
input, output, cacheRead and cacheCreate are each the sum across the transcript’s own
per-turn usage objects; none is derived from another, and the writer’s mutual-exclusivity
convention (FF-6803) accepts the result.
A session’s subagents are part of the session
The ingest walks the whole transcript tree under <projectsDir>/<sessionId>/, so a session’s
subagent turns are counted in its totals – the same boundary the driver’s own transcript walk uses,
so the two never disagree about what a session includes.
Model, effort, turns and tool calls are recorded from the transcript
The envelope carries what the session actually ran; turn and tool-call counts are counted rather than estimated. A session that ran more than one model records that it ran more than one, and the buckets still total every turn.
Absence stays distinguishable from zero
A run with no session id, an unmatched session id, a missing transcript directory, an unparseable
transcript, or a transcript with no usage leaves spend: null – not measured. No zero is
fabricated; a genuinely free run is the only thing that records costUsd: 0.
An ingest failure never changes what the run did
Outcome, state, attempt and retry lineage are unchanged when the ingest fails, and the failure is
reported (run-store: spend not stamped at settle: …) rather than swallowed.
Spend is stamped once and read verbatim
Settling a second time does not re-stamp, and a later read returns the stored envelope without re-reading any transcript to produce it.
A stamped envelope survives the drive path’s attribution write
A spend stamped at settle is no longer reverted to null by a late whole-record attribution write
on the local drive path – pinned by story 68/01’s tasks/02 scenario a stamped spend survives the
attribution write, which drives a real transcript through the real drive command.
68/03 · One run, one item
Attribution is a join on a stored key
aof work observe resolves the item an agent run belongs to from the run record’s sessionId, not
from the text of the prompt; an agent run whose prompt names another item is attributed to the item
its session’s run belongs to.
The free-text matcher does not exist
agentMatchesMilestone has no definition and no caller anywhere under src/**; no attribution path
in the codebase tests item identity against free text.
One agent run belongs to exactly one item
Across a whole work stream no agent-run identity appears in two items’ attributed sets, so the per-item totals can be summed without billing the same hours twice.
An unresolvable run is reported, never guessed and never dropped
A run with no resolvable session is counted into an explicit unattributed total that the report and
the --json document both state; it is not assigned to an item by fallback and not silently
discarded.
Tool calls are classified by what the result says, not by the command name
classifyToolCallResult reads the tool result’s own output for count-bearing and structural
test-report markers. The retired TOOLCHAIN_RE command-name pattern – which matched npm test,
vitest and jest, none of which this repo is permitted to run – no longer decides anything, and
this repo’s real isolated AOF_GLOBAL_HOME=… node --test … invocations classify as test runs.
68/04 · Observe answers for a story and a phase
Story-scoped observe
aof work observe <NN>/<SS> resolves to the story’s own folder under <milestone>/stories/ and
reports on it; the bare milestone form answers exactly as it did before.
Refusal of an ambiguous ref
A bare story-shaped ref such as 00 is refused by name rather than substring-resolved to a
top-level item, and every refusal states the ref it was asked for.
Per-phase rollup
The observe report breaks its totals down by the phase declared on brief.loop.phase, each phase
row drawn only from its own runs and summing to the attributed total.
An explicit no-declared-phase grouping
Runs carrying no declared phase are reported under their own grouping with a run count, never distributed across the declared phases and never given a guessed one.
A stable --json contract for the scoped answer
The registered observe command’s --json document carries the item’s totals, the per-phase
breakdown and the per-agent rows under one key set, identical in shape for a story ref and a
milestone ref, with unattributed counts and unmeasured spend stated rather than omitted.
68/05 · Append-only snapshots
Observe snapshots are append-only
An observe run writes a new timestamped artefact under observability/snapshots/<ts>/; no write
path in src/work-observe.mjs opens an existing snapshot for truncation or rewrite, so a snapshot
a retrospective cited still holds the figures it cited after any number of later runs.
Snapshots are ordered without being read
Each snapshot is identifiable by when it was taken, and their order is determinable from their names alone.
A newest-snapshot read path
readLatestSnapshot resolves the most recent snapshot and leaves every older one unchanged.
Pre-68 snapshots are marked, not migrated
An existing pre-68 in-place snapshot gains exactly one derivation header naming the pre-68 miner and the four provenance facts it is subject to – possible double-count, blind toolchain figures, possible overwrite, pre-68 producer – while its figures are left exactly as they are: none recomputed, corrected or removed.
Marked and unmarked snapshots are distinguishable without reading figures
A post-68 snapshot carries no pre-68 header, and marking is idempotent – an already-marked snapshot is never given a second one.
66 · Controls That Run
Structural contract lint at validate
aof work validate parses every .feature under <work.dir> and reports free text in step position
as a structural finding naming the opening line; run today it reports one file and is silent on the
thirteen under done items.
The acceptance horizon
One exported predicate in src/acceptance-horizon.mjs decides whether a record is still editable;
every check calls it, it holds the frozen five lifecycle words, and it has zero imports.
One Gherkin reader
src/feature-parse.mjs is the only module under src/ that recognises a Gherkin keyword; it returns
structural findings beside the scenarios it could recognise, and src/work.mjs reaches the grammar
only by import.
One declaration grammar
src/declared-id.mjs is the only home of the id namespace, the register-block set and the
declaration/citation recognisers; memory’s two parsers build their headers from it, and the records
they produce are byte-identical across the extraction.
The controls lane
aof work doctor carries a fifth lane of pure (snapshot, ctx) => Finding[] groups emitting exactly
eight frozen codes; it performs no process spawn, no dynamic import() and no filesystem read outside
the snapshot, and on this repo it reports 20 errors and 17 warns against work that predates it.
Two-leg control resolution
A declared control resolves when its cited path exists on disk and a declared runner names it; an
unlanded control marked pending reports at warn, and an unmarked one at error while its item is
open.
The accept gate
No item whose status is done may carry a register declaring a control that does not resolve –
enforced over the real stream, marker or no marker, and green today at 2 registers / 16 declarations.
The shipped VERIFICATION.md schema
ACD ships a VERIFICATION.md template carrying four frozen headings, a findings register on seven
frozen columns with the id alone in the first cell, and a fitness register whose red probe cell is
byte-equal to the constant the check compares against.
Seven authoring asks
src/bundle/ carries the declaration form, the red probe, the runnable path and pending, unnumbered
findings, the citation form, the accept precondition, and write-apart – each present in every file
whose reader must obey it, and each guarded by a frozen-token assertion.
64 · Green the reds that arrived on main – six arch gates and two racy tests
The inherited fitness lane is green with no gate relaxed
All seven inherited-red gates (acd-graphify-backend-selection, acd-memory-backend-selection,
acd-no-new-silent-catch, acd-work-command-route-coverage, command-core-contract’s registry
list, acd-bundle-manifest-hashes, and the resume lane) pass because the invariant each asserts
holds at the source – no baseline was moved, no carve-out was widened, and config.memory?.backend
has exactly one code read at work-memory.mjs:77.
The mesh timing-race class is fixed, not observed green
The three lanes that resolved or advanced without waiting for their observer – the resume lane in
mesh-terminal-input-path.test.mjs, and the premature-done
and session-tree lanes in
mesh-worker-completion-detection.test.mjs – now
settle in every interleaving, and every sibling resolve/lever site in the suite carries the same
waitFor guard.
The work:* family rule is exercised, not assumed
A newly registered work command lands in WORK_IDS and in BOARD_DEFERRED (or a served route) in
the same commit – held the first time it was exercised, by milestone 63’s work:trigger.
63 · Event-driven triggers
aof’s loop can be woken by something other than a person
A trigger is reviewable data that resolves to a work:loop input and the argv carrying it, with no session acting as orchestrator anywhere on the path. Four sources – a cron cadence, a mesh assignment, a CI signal and an inbound feedback finding – all resolve through one compiler and one face. The declaration this tool ships is non-vacuous over itself: every declared source has a trigger that resolves. Stated whole by m63/00, m63/04 and m63/05.
The trigger layer is a caller, and that is structural rather than careful
Nothing under src/work-trigger/ or src/commands/trigger.mjs spawns a process, drives a phase, performs gate arithmetic, holds a clock or a receiver, or authors a slash command – and work:trigger declares no cli.launch, so it cannot launch even by accident. The one launcher 53 left the loop stays the only one.
An unattended run is bounded by a declaration the machinery enforces, not by convention
The frozen set’s fourth enforcement point compiles: gate-order leaves deferred, and an unattended launch resolves only the declared program and leading argv – anything else is a coded refusal carrying nothing spawnable. Every attended launch is byte-identical to what it resolved before. Stated whole by m63/02.
A declared level is a request the gate re-decides at every fire
No trigger caches an admission verdict, no config key or flag admits L3 by the back door, and a refused level is named with its failing half rather than silently downgraded. Stated whole by m63/01.
The one dispatch phase with an orchestrator to remove no longer has one
autonomous resolves to a loop launch carrying a scope; refine, continue and verify send the bytes they always did. Stated whole by m63/03.
An unattended loop run is watched as a process and settles on its own exit
Stated whole by m63/06.
What holds all of this is eight controls, each of which has been seen red
Every declared control has a recorded red probe – the change made, the message observed, the revert proven byte-identical. Three of them are green over trees that never held the defect, and their probes are the only evidence they are armed.
63/00 · The trigger declaration
A reviewable trigger declaration at .aof/triggers.jsonc
Which signal may wake which scope at which level is one JSONC file in the repository, installed from src/bundle/triggers.jsonc through the existing hashed, drift-protected bundle path and byte-identical to its source.
One compiler, and no partially-compiled set
src/work-trigger/declaration.mjs is the only module that parses the declaration; a member that fails validation raises a coded refusal and the whole compile refuses, so no trigger set is ever returned with a member silently skipped.
The cadence grammar is imported rather than copied
parseCadence is an additive function export from src/work-loops.mjs; the trigger family authors no cadence regex, duration-unit table or periodic:/event: literal of its own, and src/work-loops.mjs still exports eleven frozen sets.
A trigger faster than the loop it wakes is a computable contradiction
A trigger and the loop-registry entry it points at are compared on the operands the parsed cadence already carries (ms for a duration, scopeRank for an ordinal), and the faster trigger is reported by name.
Every .aof/** bundle asset is line-ending pinned by a ratchet
.gitattributes pins .aof/**/*.jsonc text eol=lf, and the pin is asserted from the bundle’s own asset list rather than a hand-kept path list, so the next .aof/ declaration cannot arrive unpinned.
63/01 · The level is a ceiling, not an admission
A declared level is a ceiling request re-decided at every fire
src/work-trigger/level.mjs resolves a trigger’s declared level by importing resolveLoopLevel and resolveLoopLevelGate from src/work-loop.mjs and being handed the two gate facts; the same trigger resolved twice across moved gate facts gives two different answers within one process, and no compiled trigger carries an admission verdict.
A refused level is refused by name, never downgraded
A declared level that fails its gate resolves to a coded refusal naming the failing half – the score with its failing check ids, or the components that came back self-referential or stale – and the resolved set contains no entry for that trigger at any level, so a downgrade cannot hide as a successful resolution one rung lower.
An absent level and a refused level are different answers
An absent level takes the loop’s own default; a declared level that fails takes nothing. The two are never rendered as the same outcome, and a gate fact never supplied is its own coded refusal naming which one was missing, distinct from a fact that failed.
Admission stays in one home
The leaf holds no score threshold, no 100, no groundedness predicate, no component-state literal and no level literal beyond what it imports; it performs no filesystem read and no invoke, so it computes nothing the loop’s own gate would have to agree with.
63/02 · The launch envelope compiles
The frozen set’s fourth enforcement point compiles
compileFrozenSet(bundledFrozenSet()).deferred is empty and installed carries gate-order; COMPILED_POINTS equals FROZEN_ENFORCEMENT_POINTS in full, so all four declared enforcement points now produce compiled output.
The unattended launch shape is a declaration, not a convention
gate-order’s rule is { "program": "aof", "args": ["work", "loop"] } in both the bundled source and the installed copy, and the compiled artifact’s program and argv come from that declaration – changing the declared shape changes the compiled artifact. The declaration is the milestone’s one program literal; no other program spelling for this launch exists in src/.
An unattended launch that is not the declared one is a coded refusal carrying nothing spawnable
A mismatched program, mismatched argv, absent program, absent argv, reordered argv or spliced argument each returns a coded refusal naming the envelope member, with no program, no arguments and no environment attached – and it is distinguishable from the answer an unresolvable runtime has always produced.
Every attended launch is byte-identical to what it resolved before
For a human session, a phase-driver session, all three single-phase mesh directives, a resume and a session carrying --model/--effort, the resolved { bin, args, env } is identical key-by-key to the same call with the fourth point compiled out. resolveInteractiveDriverLaunch keeps its 23 dependents, its NEEDS_INPUT sentinel and its cache/telemetry decisions untouched.
The fourth point is traced, not merely counted
The compiled unattended-launch artifact carries the id of the member that declared it, and a declared, owned member at that point that reaches no output is a failure – the same trace the other three points already get.
63/03 · A mesh assignment resolves to a loop call
The autonomous phase dispatches a loop launch, not a slash command
An assignment on the autonomous phase resolves to a launch of kind loop carrying the assigned scope and nothing else. Exactly one of the four dispatchable phases resolves to a non-session launch, driven from ASSIGNMENT_PHASES rather than a literal list, so a fifth phase cannot be added silently.
The other three phases are byte-unchanged
refine, continue and verify produce the exact directive strings a delivered tree produced before this story, including refine --autonomous, and exactly one module in src/ authors an assignment phase’s slash command.
The launch rides the directive additively
The launch travels on the directive beside baseBranch and commit; the assignment record’s key count is unchanged at the frozen ten, and the worker reads the launch in exactly one place.
A story-shaped ref is refused before a directive is sent
The resolver refuses a story-shaped ref on the autonomous phase with a code, and the dispatch tick over such a row sends nothing – while the same ref on continue and verify resolves exactly as before.
No topology, PTY or NEEDS_INPUT machinery moved
Leasing, reclaim, presence, routing, PTY spawn, output chunking, completion detection and the NEEDS_INPUT sentinel behave as they do at HEAD; src/agent-session-driver.mjs holds none of this story’s identifiers.
An old worker handed a loop directive idles rather than doing the wrong work
Observed live on a genuinely skewed pair: a worker built before this story receives the directive, finds no command it understands, and spawns its session with nothing typed into it – no cascade fallback, no loop argv as text – holds the assignment rather than dropping it, writes no commit to its worktree, and settles on the deadline policy it already had.
63/04 · The signals that are not the mesh
Three non-mesh trigger sources that answer only which scope
src/work-trigger/sources.mjs resolves a cadence, a CI signal and an inbound feedback finding to a scope and nothing else – no source carries a level, a cap, a gate or a launch.
A finding-triggered wake never classifies
The family cannot reach a feedback record’s body: neither RAW_FEEDBACK_KEYS’s text field nor FEEDBACK_CLASSIFICATION_KEYS’s verdict field is reachable from it, and two planted captures whose bodies differ and whose attribution is identical resolve byte-identically. Only existence and attribution are read.
A CI signal is read for its ref, never for its outcome
No source reads a build status, a pipeline name or a failure class, asserted over a planted signal carrying all three.
Every scope resolves through the loop’s own forms, or is refused by name
Scope is resolved only through LOOP_SCOPE_FORMS; no scope pattern, range grammar or item-ref regex is authored in the family. A story-shaped signal is a coded refusal naming the driver it belongs to rather than an implicit widening to a whole-stream walk, and a well-formed driver ref bearing no item resolves – the leaf performs no filesystem read at all.
A source that cannot answer refuses, never resolves to nothing
An unresolvable signal produces a coded refusal naming the source and what it could not resolve; the refused and resolved sets are disjoint and together account for every signal, so nothing is dropped between them.
63/05 · The trigger’s face
One registered command that resolves a trigger and launches nothing
work:trigger composes the four src/work-trigger/ leaves and emits, for each declared trigger, the work:loop input it resolves to plus the argv that carries it. No module in the family spawns or execs a process by any spelling, drives a phase, holds gate arithmetic or authors a /aof: slash-command literal – proven through the family’s whole static import closure and by a real run that starts no process.
The projection validates against work:loop’s own declared input
The object the face projects carries work:loop’s input keys plus one identity key naming the declaring member, and the level flag it composes is read back from getCommand("work:loop").cli.spec.flags – so a renamed loop flag cannot leave this face composing an argv the loop refuses.
The gate facts are obtained through the registry and handed in
The two gate readings arrive through invoke at the command boundary, exactly as work:loop gathers them when it fires, and are handed to the leaf uncoerced – no ??, || or ?. fallback stands between the registry’s answer and the leaf. A registry that cannot answer is a reported failure rather than a locally computed verdict.
aof holds no clock and no receiver, and the face writes nothing
No timer, cron evaluator, HTTP server or listening socket is reachable from the family or its closure; the one setTimeout in the closure is renameWithRetry’s bounded write backoff and is pinned as a shape. Two runs over one fixture tree, in both renderings, leave every byte where they found it. The family declares no --strict and no --dry-run, and --json and the human face render from one object.
The exit code is a cause, not a case table
A run that produced a resolution exits 0 however many refusals it carries; a run that produced no resolution states the failure first and exits non-zero. Refusals for an unknown trigger, an unknown source, an unresolvable scope and a refused level are each reported by code with the sources that exist named.
The shipped declaration actually resolves
Over this repository’s own .aof/triggers.jsonc, every declared source has at least one trigger that resolves to a well-formed work:loop input, every resolved scope resolves through LOOP_SCOPE_FORMS, every resolved argv begins work loop and names a registered command, and no resolved trigger carries a level this workspace’s gate would refuse.
63/06 · The loop launch is watched as a loop
An unattended loop launch is watched as a process, not as a session
loopShapedTranscriptWatch(launchOptions) (src/mesh-worker-execution.mjs) returns null for every session launch and, for an unattended loop, supplies both transcript-watch seams as functions resolving null – each the driver’s own documented no-op – so a dispatched aof work loop is no longer bound to the first inner session it writes into its own worktree and killed roughly ten seconds later with done.
An unattended loop run settles on the process exit the loop already has
Settlement falls through to term.onExit and the delivered exit mapping: exit 0 settles done, non-zero settles failed. No thirteenth loop stop, no second completion signal and no halt vocabulary exists in any mesh module; LOOP_STOPS is still 53’s frozen twelve.
Exactly two spawn-bag forwards are launch-conditional, and each takes exactly one expression
watchTranscriptSessionId and watchTranscriptCompletion are injected ?? loopShape; ptySpawn, which, onOutputChunk and onSessionEnd remain bare shorthand, and a fifth that stops being one fails FF-6306. src/agent-session-driver.mjs is byte-unchanged – no PTY, streaming, completion-detection or NEEDS_INPUT machinery moved.
An injected watch still wins over the loop shape
A caller supplying its own session-id or completion watch keeps it for every launch kind, so no launch kind can render a supplied producer inert.
A session assignment is handed exactly what a delivered tree hands it
A session-phase directive supplies no session-id watch and no completion watch at all, so the driver reaches its own session-shaped default unchanged.
The loop-shaped seam is held by a control that has been seen red
FF-6306 carries the ADR-013 §1 leg with its own positive control – the session-shaped default is run first over the same worktree and must bind a planted inner transcript before anything is concluded from the loop-shaped seam refusing to – and the relaxed §3a form’s red probe is the revert to bare shorthand, which reds both the structural and the driven leg.
62 · The self-improvement loop
The hill-climbing loop is closed
The raw material aof accumulates about itself – retrospective lessons, run lineage, observability
readings – now has a consumer. aof work tune is the first path from that material to a named harness
change, and the loop that was open at exactly the point where it would close is closed at that point.
The pass says something about this repository, and says it completely
Run over its own corpus at accept, aof work tune emits 63 proposals, both lanes populated
(62 advisory, 1 tunable on work.loop.reviewRounds), every proposal carrying at least two distinct
resolved source documents and a measured distance, at exit 0. This is the one property no suite over
fixtures could establish, and it is a standing condition rather than a recorded figure: the control
re-derives it from the corpus as it stands on every run.
The proposer ships with no acceptance rule of its own
62 was sequenced after 61 and holds no second opinion about when a change may be believed: the tunable
lane’s verdict is obtained by invoking work:acceptor and by no other means (m62/04), and no module
in the family carries 61’s ruling vocabulary, thresholds or arithmetic. The configuration the
self-evolving-agent literature measures failing – a proposer that can also decide – is not reachable
here, and is not reachable by falling back either: a registry that refuses the acceptor makes the lane
report a construction failure rather than compute its own verdict.
What stands between a proposal and a commit is measured, not asserted
Across the whole emitted set the answer is 5 distinct obstacles, and the largest is a fact about this harness rather than about the loop: 3 tunable knobs are declared on the arbiter’s tuning edge, and 0 of them reach a decision site – every one is resolved and then discarded, or composed into an object nobody reads. The tuner’s first honest finding is that this repository’s declared knobs are not yet wired to anything that decides.
The generated half is separated from the applied half by a locked level
Proposals are data at every point in the arc. No path applies one, the only write in the arc stays 61’s
store, and the L2 diff is rendered for a human to accept (m62/04). What ships is a proposer whose
output must still earn its commit.
Nine controls that have each been observed failing
Every structural invariant this milestone declares is enforced by a control that has been broken, run,
seen red for its stated reason, and reverted – recorded per control in VERIFICATION.md. Two of those
probes found defects no green run had, which is the only evidence the register is armed rather than
merely populated.
62/00 · The corpus and its floor
The three-lane tuning corpus
assembleCorpus returns exactly three declared lanes – lessons, lineage and observations – measured
at 410 retrospective lesson sections, 61 run records and 6 observation readings over this repository.
A floor beside every count
Each lane is emitted with the root it walked, the population it found and the floor it was measured against; a lane declared without a floor is refused before a count or a report can exist, so “found nothing” and “looked at nothing” cannot be rendered alike.
tune-ran-on-nothing
A lane below its floor emits a finding naming the lane, the root, the floor missed and the scope it was measured under, and that finding differs from the audit lane’s in its code string alone.
Source-owned readers
Every lane reaches its source through the module that already owns it – parseRetrospective,
readRuns, readLatestSnapshot – and no raw runs/*.json or snapshots/*/agents.json path grammar
exists in the family; scope resolves only through src/work-ref-scope.mjs.
62/01 · The proposal, its lane and its patch
The proposal object
A proposal carries class, target, lane, laneBasis, evidence, patch, applier, reason,
finding, provenanceResolution, sourceFacts and distance, and its class comes from a frozen set
equal to SPEC’s four.
A lane the registry decides
Lane membership is computed from tunableSet(model)’s keys – the arbiter’s own parameter-tuning:
edge – so no module under src/work-tune/ holds a tunable key literal or a class→lane map, and a knob
removed from that edge leaves the tunable lane with no edit here.
Routing is not committing
A non-ordinal key declared on the tuning edge is routed to the tunable lane and its refusal comes back from the acceptor; no path in the family decides ordinality or diverts a proposal on the ground that 61 would refuse it.
No computable patch, no applier
A proposal with patch: null carries applier: null and a coded reason, while a patch with no
applier is a reachable, rendered state – the asymmetry runs in one direction only. Every applier
resolves through the injected resolveCommand to a registered command, and is never a shell string, a
file path or a prose instruction.
absent is a value
A patch over a key the config does not carry renders from: absent with a fromSource that says so –
at HEAD that is every work.loop.* key and the whole model map – and the shipped bundle default
appears nowhere as a from.
A patch renders only on the base its evidence assumed
The four-row base table is driven whole: absent/assumed-absent and held/assumed-that-value render;
absent/assumed-a-value and held/assumed-otherwise are moved premises with both readings named. A
proposal whose from equals its to is absent from the emitted set and present in findings as
already-in-force.
62/02 · Provenance that resolves
Citations checked against disk at emit time
Every citation an emitted proposal carries is resolved when it is emitted – the file is present, and where a line is cited the file has that line. Over this repository 63 proposals are emitted and none carries an unresolved citation.
Demotion rather than a footnote
A proposal carrying one unresolvable citation is absent from the emitted set and present in findings
with the failing citation named; the two sets are disjoint. A real run reports 24 such demotions and
506 below-evidence-floor findings, and none of them reaches the exit code.
Both grammars imported, neither re-authored
Path citations come from pathCitationsIn / splitPathLocator (src/work-doctor-controls.mjs, two
additive exports) and id citations from qualifiedRefsIn / QUALIFIED_REF (src/declared-id.mjs);
no module under src/work-tune/ holds a citation regex, locator pattern or ref/id pattern of its own.
controlPathsIn is byte-unchanged by the split
The doctor’s control lane answers exactly as it did before the two exports were carved out, asserted
as a self-comparison over every ## Fitness functions register in wiki/work, with
isControlFileName’s test-shaped filter still applied on that path.
A qualified id or nothing
Every id citation matches m?<itemRef>/<ID>; a bare id is never emitted as a citation and no bare-id
extractor exists in the family.
An evidence floor counted in documents
The floor is two distinct resolved source documents, so an id citation and a path citation naming one document count once. Over this repository every emitted proposal clears it, at between 2 and 9 distinct documents.
62/03 · The distance to a live proposal
A measured distance on every proposal
Every emitted proposal carries what stands between it and a commit as a structured obstacle list, not a sentence. Over this repository all 63 carry one, across 5 distinct obstacles.
Removals that are the acceptor’s own words
The removal text for a tunable-lane proposal is taken from the acceptor’s refusal records
(refusals[].removal); no removal sentence for any member of 61’s ruling vocabulary is authored under
src/work-tune/, so a copy cannot drift from the original.
One fact renders once
Where the acceptor reported not-admissible, limb (a) appears only as the measurement beneath that
entry and never as a second entry; a standalone limb entry appears only where the acceptor never spoke.
Limb (a) is read, not re-derived
Its sites, inspections and dispositions arrive on the acceptor’s own report – at HEAD, 3 knobs
examined, 0 consumed, 3 remaining, with the decision sites named down to file and line. No module under
src/work-tune/ builds a unit set or walks src/, so 62 cannot become a fourth tree-walker that
disagrees with 61 about one tree.
unknown and not-reported are different answers
A limb the acceptor was never asked about is unknown – over a registry declaring no tunable key the
acceptor is not invoked at all – while a limb that was asked and does not stand is simply absent, and
neither is reported as zero.
Shrink-only
The limb set only ever gets smaller: one accepted item whose consumed runs are all attributed drops the attribution limb out, and a closure planted for each limb removes that limb alone.
62/04 · The tuner’s face
aof work tune [scope] [--json]
A registered core command whose bare face is a read. It composes the four stage-1 leaves into one
report – headline, scope, corpus, formation, proposalEvidenceFloor, proposals, findings,
acceptor – and --json and the human face render from one object.
A verdict obtained only by invoking the acceptor
The tunable lane’s verdict comes from exactly one invoke("work:acceptor", …) call site, with the id
resolved from getCommand rather than spelled twice. A registry that refuses the id makes the lane
report a construction failure; there is no locally computed fallback, which is the second acceptance
rule this milestone may not carry.
A read face that writes nothing
Running the command twice over one tree leaves every workspace byte unchanged and accumulates nothing
between runs. The two writes it can reach are outside the workspace and named – reportDegrade’s
mesh-log append and censusSnapshot’s temp copy inside work:acceptor – and the family opens no
journal of its own.
A two-sided exit code
Every refusal, finding, demotion, below-floor candidate and empty result exits 0, so no findings gate
arrives by the back door; an unreachable work:acceptor is reported first in --json as
acceptor-unreachable and then exits non-zero.
The registry ring stays open
No module in the family statically imports src/command-core.mjs – asserted per module in a fresh
process, the only probe that sees this class – and the applier resolver is injected by the face rather
than obtained by a leaf.
A lesson’s structured config pointer becomes its target
loopPointersIn (src/work-loops.mjs) lets the corpus join the one unambiguous config: pointer in a
lesson section to that lesson, and the section’s path citations to its evidence; embedded pointer-like
text inside a URL or a larger token does not promote a target.
62/05 · Candidate formation
A partition, proven two ways
formCandidates puts every record on exactly one candidate, asserted as membership and as arithmetic,
so neither a dropped record nor a double-carried one passes. Over this repository it takes 796 records
into 593 candidates.
Lossless citation carry
A cluster carries every citation its members carry – 654 citations behind 402 lesson records at the gate, where 154 records carry more than one – so the distinct-document evidence floor downstream is measured against everything that stands behind a candidate, never a sample.
A tie-break that reads content
The tie-break is the lexicographically least source citation; no input index, position or iteration order is reachable from it, and a shuffled input yields byte-identical candidates over both planted ties and the real lessons lane.
A criterion with a declared ordered range
minimum-shared-metadata-fields names its four dimensions, declares a loosest and a tightest admitted
value, refuses anything outside that range with a coded error, and defaults to 3.
A default that must re-earn its place
The tradeoff justifying 3 is recomputed against the corpus as it stands on every run – the largest loose cluster must collapse, recurring multi-source classes must survive, and the tightest setting must fragment – with the dated measurement kept beside it as provenance rather than as an expectation.
A pure leaf
formation.mjs takes records and returns candidates: no filesystem, no clock, no argv, no static
import of the command registry. A candidate carries exactly sources, citations and a bare-ref
target, and attaches no lane, patch, applier, verdict or distance.
61 · The disciplined acceptor
aof has an acceptor, and applied to itself today it refuses everything and says why
Every lane a harness-change proposal must clear is in service: a range each step stays inside
(m61/00), an epoch whose criterion cannot be accrued across (m61/01), a census that declares what
it filtered (m61/02), an admissibility check worded on consumers (m61/03), one derived commit
condition over a ledger that accrues (m61/04), the ruling recorded beside the value it moves
(m61/05), and one command that renders all of it (m61/06). Run against this repository, it admits
three knobs and refuses all three – each for at least two independent reasons – and reports the
reasons with what would remove them. That is the deliverable: not a system that tunes itself, but one
that can state precisely why it will not, in terms an operator can act on.
The threshold is unarguable because it is nowhere written down
No number in this machinery is a preference anyone can revisit at eleven at night. The commit level,
the bet, the earliest crossing and every later crossing record are arithmetic over the criterion’s own
inputs, and the controls forbid any literal equal to them appearing in the engine (m61/01,
m61/04). Revising an input moves the whole lattice; a criterion that would make the rule trivial is
refused at construction. The one quantity that is chosen rather than derived – the budget – is frozen
inside the epoch it is scoring, so extending it mid-flight to reach a crossing is the same p-hack as
moving the yardstick, one axis over, and is refused as one.
Report-only is structural, not a default that could drift
The property holds across the whole milestone rather than at any one point in it: the command
declares no strictness flag and no board route serves it (m61/06), its only mutating form takes an
explicit named request, the evidence that request is weighed against is read from the ruling store
rather than accepted from the caller, and the event the machinery raises is named for the ruling
rather than the change – so the overwhelming majority of its work, every honest refusal, leaves a
record (m61/05). “The acceptor did nothing” is its steady state and is reachable by no other path.
Three enforcement layers, and the load-bearing one is not access control
A permission boundary (the criterion as the frozen set’s sixth member), a writer refusal at the
criterion seam, and beneath both the ledger’s own arithmetic. The first two can be walked past by
anything able to write a file. The third cannot be walked past by anything, because the leaf that sums
evidence never sees a criterion digest: summing across two criteria is not a path it refuses, it is
not a sentence it can express (m61/01, m61/04). A criterion edited mid-epoch does not have to be
caught.
The instruments the gate trusts are bounded by what this system can actually see
Two milestone-level facts sit beneath every verdict. The evidence journal has no fixture boundary and
no workspace identity, so every population the acceptor counts is filtered, folded and declared
against a floor, and a sweep that found nothing says so rather than returning a confident zero
(m61/02). And what may be proposed at all is read from the loop registry’s own declaration, never
from a list this machinery keeps – so widening the tunable set is an edit to the registry, and the
acceptor holds no knob key anywhere in its code (m61/03).
The acceptor is a gate, and nothing in the graph can point at it
It is deliberately not a control loop: no node kind, no loop record, no reference of its own, no
cadence it sets, nothing it optimizes. Its epoch equals the cadence of the audit loop that checks its
instruments, which is a decision rather than a coincidence – an acceptor whose epoch differs from that
cadence is trusting instruments audited on a different clock. And no proposal may author or revise
what “better” means: actor:operator is the only revisor, permanently.
Thirteen declared controls, every one of them observed failing
The milestone applied its own thesis to its own gates. Each of FF-6101…FF-6113 was run green, then
run again with the invariant it guards deliberately broken, and the failure message recorded in
VERIFICATION.md. Four of the thirteen extend a guard that was already in service, where the probe is
the only evidence the extension is armed at all. Two of the probes are evidence in the other
direction: FF-6102 and FF-6110 stayed green when the metric name and the knob key were planted as
strings, which is exactly the line those controls draw – a pointer is data an operator edits, and a
key quoted in a diagnostic is not a claim of membership.
The gate was applied to this milestone’s own last story, and it refused it
61/06 reached acceptance built, green, and marked in-review by a hand-edit – a claim of review
nobody had earned. The record refused it (D-61-3), the review then ran, and it found a Blocker a
green suite could not: the reporting face preferred a pair sequence carried on its caller’s
proposal to the one accrued in the ledger, so an explicit commit could be granted on fabricated
evidence – the one enforcement point this milestone says cannot be routed around, routed around
through the command’s own input. The story’s own test drove that path and asserted success, which is
why the suite agreed with the defect. Fixed, locked by a regression, and red-probed.
61/00 · The clamp
Every steppable knob resolves inside a declared range
work.loop.buildNoProgressRounds resolves through a ceiling as well as a floor at its single funnel,
resolveBuildNoProgressRounds in src/loop-bounds.mjs, so a value past either end comes back inside
whichever door supplied it – both stall paths and the config path arrive through that one resolver.
Admissibility is the resolver’s own answer, and there is no second table to disagree with it
A proposed step is in range exactly when the knob’s own resolver returns it unchanged.
LOOP_BOUND_VALUE_RESOLVERS is derived from the callables in src/loop-bounds.mjs rather than
declared beside them, so a range this machinery believes in and a range the system enforces cannot
diverge.
A key that resolves to TWO bounds is refused as a step rather than given an invented range
work.autonomous.maxAttempts resolves to an attempt ceiling and a per-phase drive-cycle ceiling –
counted from its own call sites, measured in different units, exhausted by different events. Every
step on it is refused by name with step-would-be-compound, and no range is manufactured for it. The
key stays proposable: the refusal is about committing, not about proposing.
61/01 · The epoch and the frozen criterion
An epoch is one milestone, and its boundary is any transition INTO done
closesEpoch is exported from src/acceptance-horizon.mjs alone, compares against that module’s own
CLOSED_STATUS, and takes the destination status the payload actually spells – status, the field
item-status.changed carries, not to, the name the lifecycle table uses and no payload carries. A
milestone parked in review before acceptance closes its epoch exactly as one accepted from build does.
A criterion that moved mid-epoch cannot be accrued across, and does not need to be caught
Every ruling carries a digest of the criterion it was rendered under, and
rulingsUnderCurrentCriterion – the only module in src/ that selects rulings by digest – returns
the maximal trailing run sharing the current digest. It is a suffix, never a filter, so a
criterion revised and revised back resurrects nothing. Evidence gathered under a superseded criterion
is not refused; it is not summable.
The criterion is the frozen set’s sixth member, revisable only by the operator and only at a boundary
A write through the criterion seam away from a boundary is a coded refusal that names the open epoch
and writes nothing. The member compiles at permission denials, is aofManaged, and names both
acceptor paths for Edit and Write; all four sites that pin the member set agree with the declaration.
A knob value that moved is reported by name rather than mistaken for a criterion move
A knob whose value changed under an accruing ledger is named in the report, so the two kinds of movement under a running trial read differently.
61/02 · The observation census
Every population the acceptor counts declares what it read, against a floor
Each population is emitted with a declared read record and a floor, driven from the lane registry – so a lane added without one fails rather than passing silently over nothing, and a sweep that read nothing says so instead of returning a confident zero.
Test fixtures are excluded and dispatch worktrees fold into their parent, in ONE home
src/work-acceptor/observations.mjs is the only module in src/ that classifies a fixture itemDir
or a dispatch worktree. It derives the dispatch case from src/mesh-worktree.mjs’s exported
predicate and slug rather than spelling dispatch-worktrees a second time, and a worktree nested
inside a worktree folds all the way home rather than half-way.
A census that filtered nothing is a finding rather than a count
Below its floor the census reports a finding. The read record, the floor discipline and the problems
list are imported from the audit family’s zero-import leaf (src/work-audit/reads.mjs) rather than
re-spelled, so the acceptor’s shape and the auditor’s cannot drift; only the finding code differs,
because that code is the auditor’s.
61/03 · No executed consumer, no proposal
A bound whose resolved value reaches no DECISION refuses every proposal on it
The rule is worded on consumers – a resolved value that reaches a decision – never on readers, and
it ships as a second predicate beside unconsumedCeilings rather than as a sibling check, so the
existing guard’s legs keep their semantics unchanged. At HEAD it refuses all three declared knobs,
which is the correct answer: the same tree that satisfies the reader leg fails the decision-site leg
for every one of them. The reported set is a subset of the registry’s declared tunables, so a knob
that gains a decision-site consumer drops out without the gate needing an edit.
Where consumption cannot be decided, the answer is refusal rather than a guess
The harness switch is evaluated over the declared document. A prompt names no configuration key, so consumption is not statically decidable for it and every proposal is refused – fail-closed. The report names which condition it fell back on, and the switch re-opens the moment that document names a knob.
What may be proposed at all is the registry’s declaration, never the acceptor’s
No module under src/work-acceptor/ holds a work.loop.* or work.autonomous.* key in code – not
in code, not in a string, not in a comment in the admissibility module. The admitted set is resolved
from arbiter:speed-thoroughness-autonomy’s parameter-tuning: edge through loadLoops and is
identical to it; a key outside that edge is a coded refusal, and an empty declaration admits nothing
rather than falling back on a built-in set.
61/04 · The rule and the ledger
The commit rule is ONE object, and its numbers are derived rather than typed
No literal equal to N, to 1 + lambda, to 1 / alpha or to alpha appears in the engine – every
one of those quantities is obtained from the criterion, so a criterion carrying a different lambda
moves the whole crossing lattice rather than leaving a typed number standing. The commit predicate
has exactly one leg; “all favourable” is spelled nowhere. lambda is refused at construction outside
(0, 1), so the hard reset cannot arrive through the parameter.
A loss carries rather than resets, and “spent” reads differently from “short”
An unfavourable pair multiplies the wealth by (1 - lambda); no path resets the ledger or drops a
proposal. A losing proposal that can still reach a crossing is reported recoverable, and one that
cannot is budget-exhausted – which is never reported as evidence-short. B is read from the
criterion and refused below N.
The trial metric is a declared pointer, and an unmeasurable arm is neither a tie nor a win
Metric and counter-metric are pointers resolved through a registry derived from callable
resolvers, so a pointer naming a symbol that does not exist is refused at construction and a
criterion carrying a metric and no counter-metric is refused outright. Both pointers resolve into the
same zero-import deterministic-counter leaf, and no second counters home exists in src/. An
unmeasurable arm is counted in neither total: no path maps it to a tie or to a favourable pair. At
HEAD every arm is unmeasurable, and the machinery reports that rather than concealing it.
One threshold, many baskets – computed per knob, in integer arithmetic
Every admitted knob resolves a declared trialUnit with a unit price, and its basket is the single
declared expression, recomputed independently per knob; no threshold or basket constant is applied
across knobs. The pair count is ceil(B * d / (d - n)) over a declared rational with integer terms,
so the 90% case returns 110 raw pairs and not the 111 the float path gives. A knob priced above the
budget is trial-unaffordable: it accrues zero pairs and stays on the surface carrying its price and
the ceiling it exceeded.
A step is one knob and one notch
A proposal naming two knobs is a coded refusal rather than a split, and a knob whose values carry no order is refused as a step by name.
The ledger accrues ACROSS epochs and refuses an incomplete ruling at construction
The epoch id is carried and reported but is not a filter on the sum. RULING_KEYS is a frozen set
carrying dwell and dwellFrom and no computed dwellExpiry; a record missing any key is
refused at construction and again at assembly. The W/L/T sequence is stored in the order it arrived
and no code path re-sorts it.
The arithmetic is blind by construction, not by rule
The engine and the summing leaf import nothing, read no clock, and take now on the call. The
summing leaf never sees a criterion digest, so summing across two criteria is not a path that has to
be refused there – it is not expressible there.
61/05 · The event a ruling raises
harness.ruled is a declared event with exactly one reactor
EFFECTS gained exactly one name and no tenth; its reactor set is one entry carrying a known locus
and an async apply, and the eight names already declared are byte-intact. The event is named for
what actually happens – the ruling, whether or not anything moved – because report-only is the
permanent steady state and an event called “changed” would leave every honest refusal untraced.
The why travels with the change, and an incomplete record is refused
The ruling record carries the key, both values, the epoch, the evidence in the order it arrived, the
attained level, the counter-metric reading, the dwell declaration and who rendered it. The knob write
is surgical: every other key, its order, the file’s indentation and its trailing newline survive a
write. No module in src/ assigns at a declared tunable knob path outside that one seam.
The ledger has ONE writer, and it is git-tracked beside the config it justifies
src/work-acceptor/store.mjs is the only module in src/ that writes .aof/acceptor-ledger.jsonl;
the store is reached only from the seam and its reactor, and no command or face reaches it. The
ledger path is absent from AOF_GITIGNORE_ENTRIES – it is neither derived nor regenerable, and being
tracked is what makes the record reversible from git rather than merely present.
The seam cannot report success with a knob written and no ledger line
The two halves are bound: a write that lands the configuration change without landing its justification is not a state the seam can return.
Redelivery changes nothing
Re-appending the same ruling identity yields a byte-identical ledger rather than a second line, and a different record under that identity is refused – so at-least-once delivery cannot inflate the evidence count the commit rule reads.
An undeclared event name is refused, so the vocabulary’s own comment is true for the first time
applicableReactors refuses an undeclared name with a code. Before this, a misspelled name appended
silently and resolved to zero reactors – a consequence quietly owed to nobody.
61/06 · The acceptor’s face
One registered command shows every proposal, its evidence, its verdict and every reason it did not commit
work:acceptor is a member of the same registry every other work command is registered in, reachable
as aof work acceptor, and it renders the five lanes rather than recomputing them: the range each
step must stay inside, the epoch’s criterion, the census as it was counted, the admissibility answer,
and the rule with its ledger. The machine-readable face is the canonical report object itself – the
human face is a rendering of that same object, so neither derives a number the other does not carry.
Report-only is the default, and it is permanent rather than a state that drifts
A bare run reports and moves nothing; crossing the level changes what the report says about eligibility and changes nothing about what the command does. Committing is reached only by naming a key explicitly, and an explicit commit on an ineligible proposal is refused carrying every reason. The command declares no strictness flag at all, and its exit status is a fact about whether it could run – never about what it found.
The evidence a report reads is the ledger’s, and a caller cannot hand in its own
A proposal names a key and a step and says nothing about how much evidence stands behind it. Every
pair the commit predicate weighs is read from the ruling store and passes through the criterion-digest
suffix on the way in, so evidence recorded under a superseded criterion accrues nothing toward the
current one. input is caller input; the ledger is read at the composition root.
Every applicable refusal is reported, in a frozen order, each naming what would remove it
The ruling lane is the eight-member vocabulary assembled from the constants at their declaring
modules, and the reported codes are the declared order filtered to the knob’s own set – an ordering no
discovery order satisfies by accident. A refusal matching no member is refused with a code rather than
rendered as free text. Refusals raised while a ruling was being built never enter the lane: they are
reported in their own right, and the admissibility grounds travel as detail beneath not-admissible
rather than as members of it.
A structural silence reads differently from a short ledger, and both distances are computed
A knob accruing toward its threshold states its standing, the threshold and the rulings still needed;
a knob whose observed yield cannot reach the floor states the epochs that yield would need; a proposal
that can reach no crossing record inside its budget is named budget-exhausted and is offered no
distance at all. The numbers move when their inputs move – a rising yield shortens the distance with
no message edited – and a report in which every knob is yield-bound says so as one statement.
Dwell is recorded as declared and is unreachable from the response to harm
The settling period is read from the arbiter that governs these loops and recorded exactly as declared, with the landing epoch beside it and no date, duration or expiry derived from either. An operator’s reversion is refused naming the counter that would have to exist to show the period discharged. A counter-metric degradation withdraws the change at once on a path that reads no dwell value, and no option the command offers delays or waives it.
59 · The audit loop
aof has an auditor, and nothing in the machinery can point at it
The registry admits a sixth kind whose subject is the measuring apparatus (m59/00), and the
endpoint vocabulary was deliberately not widened to match: no scheme resolves to an auditor:,
so an auditor is a source and never a target. Nothing can supervise it, set its reference, veto it or
tune it – the structural form of “the audit does not answer to the loops it audits”, rather than a
sentence in a document. It has no vocabulary for acting either: the kind admits no actuator, so the
prune reports candidates and removes nothing (m59/03).
The audit executes; the health command still cannot
Milestone 66 froze work:doctor as a command that may not run anything. This milestone built the
thing that runs, and put the boundary on a command rather than a convention: no module under
src/work-audit/ is reachable from doctor’s spine or from any module in its check registry, over the
whole import closure, and the audit’s finding codes are disjoint from doctor’s so neither command’s
severity table can decide the other’s meaning (m59/02). The two verbs are siblings that share a
finding shape, a scope semantics and a --strict flag whose policy deliberately differs in exactly
two cells (m59/04).
Absence has one shape, and a lane cannot report clean without using it
Every lane in the audit returns what it read against a floor, and states what it could not see. Both
records – the read (m59/03) and the limit (m59/04) – live in one module, are complete by
construction, and are refused rather than defaulted. That is the milestone’s actual mechanism: a
dashboard cannot stay green through a report whose shape has no way to express “clean” without also
expressing “and here is how much I looked at, and here is what I could not see.”
The recurrence guard for “part of the fitness gate is dead” exists, and it found a live instance on its first run
TECH_DEBT item 5 was repaired by hand in milestone 42 with nothing watching for the recurrence.
There is now a watcher, and it is not a promise: registration is decided by membership of the
runner’s assembled array inside the runner’s own process, twenty-six de-armed suites carrying 117
test entries were re-armed, six more that only scripts/test-unit.mjs had ever assembled were
registered, and two suites that had rotted red while dead were repaired (m59/01).
Recorded evidence is re-executed by something that did not write it
The gap the objective names on the record – ACD’s @manual evidence written by the same agents that
did the work – is closed for the fitness register: every row’s cited control is run in a bounded child
and judged on the message it produces, with no verdict reachable from the recorded prose (m59/02).
Eleven declared controls, every one of them observed failing
The milestone applied its own thesis to its own gates. Each of FF-5901…FF-5911 was run green,
then run again with the invariant it guards deliberately broken, and the failure message recorded in
VERIFICATION.md. Four of the eleven extend a guard that was already in service, where the probe is
the only evidence the extension is armed at all.
The audit found real defects on its first runs, including two of its own
Run over this repository rather than over fixtures, the audit reports 0 errors and 49 warnings across
three lanes, each declaring its population against its floor. Two defects in this milestone’s own
delivery were caught at its gate rather than by its stories’ lanes – a limits footer that printed
undefined – undefined (D-59-3) and a bundle census that had not been moved by the diff that grew the
tree (D-59-6). Both are fixed, and the first is now a control.
59/00 · The auditor kind
The sixth node kind
NODE_KINDS is closed at six – loop, actor, anchor, watcher, arbiter, auditor – and the
five kinds that existed before parse exactly as they did, every record shipped before 59 producing
zero new findings in the same codes and the same counts.
An auditor that has no vocabulary in which to act
ADMITTED_KEYS.auditor is its four required declarations (audits, measurement, cadence,
escalation) plus the edge keys, and it omits actuator, optimizing, controlled, reference,
ground, counter, determinism, layer, owner and ceiling; each omitted key is refused by
the loader’s existing loop-key-not-admitted-for-kind rather than by a new code.
A declared subject that cannot be the work
An auditor’s audits: is a non-empty list of instrument pointers over
module | command | config | loop | watcher | anchor, and an item: endpoint is refused – so “the
audit does not review the work” is a checked property of the record, not a promise in prose.
Reporting is an outbound edge, and nothing points at an auditor
EDGE_KEYS gains exactly reporting and no seventh; ENDPOINT_SCHEMES is unchanged, so no record
in the registry may name an auditor as a target. An auditor’s admitted edges are data-feed and
reporting alone, and target-setting, veto, parameter-tuning and monitoring are refused at
the endpoint.
An anchor that says when it was checked
checked: is admitted on kind: anchor alone, is optional there, takes an ISO calendar date, and
refuses every SENTINEL_TOKENS member and every reserved field prefix – so an anchor is dated,
undated, or refused, and there is no spelling by which one opts itself out of freshness.
59/01 · The instrument census
Registration is runtime membership, not a mention in the runner’s source
test/arch/acd-test-suite-registration.test.mjs imports the assembled tests array and decides
registration by name-set membership for every *.test.mjs under test/, test/arch/ and
test/integration/. No source-text lane survives in the file, the walked-file floor is asserted
before the membership claim so a moved root fails rather than passes over nothing, and a suite whose
module exports a runner-shaped array the assembled suite does not contain is reported by name.
The twenty-six de-armed suites, and six more nobody had counted
The suites whose spread sites vanished in 15e0a92 – 117 test entries dark for a month – are members
of the assembled suite again, and two of them (mesh-node-identity, mesh-registry-store-seam) had
rotted red while dead and are repaired. Six further suites registered only in
scripts/test-unit.mjs, carrying 122 test entries that npm test never assembled, are registered in
what CI executes.
One bounded seam for every child process the audit family starts
src/work-audit/spawn.mjs is the only way a module under src/work-audit/ starts a process: it
carries a deadline, kills the child on expiry, reports a terminal deadline-expired outcome, and hands
back the observed exit code. No caller passes a shell string and no second spawn helper exists.
The audit never runs project code inside its own process
No module in the audit family’s import closure contains a dynamic import() or a require, and
every static import is a node builtin or resolves inside src/. The runner’s assembled suite is
obtained from a child process rather than by importing it.
A sweep that reports what it read
Every lane the census registers returns a population count with a floor; a clean lane result is not representable without one, and a count below the floor is a finding naming the sweep, the root walked and the floor missed – so a gate that ran on nothing says so instead of reading green.
59/02 · Evidence re-run
The fitness register is executed rather than read
src/work-audit/evidence.mjs resolves every FF-NN row’s cited control in scope and runs it in its
own bounded child process, one run per (row, control) pair – a row citing two controls is two runs and
two verdicts. Every verdict in the lane is derived from a spawn result: an observed exit code and
captured output. With the child’s result withheld, every row reports evidence-unrunnable; no verdict
is reachable from the recorded prose.
The oracle is the failure message, and no code path reaches a count
A control’s disposition is decided by the message it produces, not by a pass/fail tally. The lane carries no tally identifier, no comparison against the length of a pass/fail collection, and no test-runner surface – an observation varied only in its MESSAGE moves the verdict, and one varied only in its CASE COUNT does not.
A citation that cannot run says what was tried
An unresolvable path, a control no runner assembles, and a control that exceeds its deadline are three distinct reports, each naming the path, the runner and the deadline. “Slow” and “broken” do not collapse into one verdict.
Recorded case count is a separate axis from pass or fail
A recorded size that no longer matches the observed one is reported as evidence-size-drift against
the item whose register recorded it, and it changes no confirmed row’s verdict. The observed number is
what executed, not what the file’s text declares.
The register’s own claims are re-run against this repository
Over the shipped work stream the lane reads 114 register rows and reports on each: measured
2026-08-30, four evidence-size-drift warnings against milestone 66’s register and 44
evidence-no-register statements for items that declare no controls – the latter reported as
declaring no controls, which is not the same as clean.
Doctor still never executes, re-asserted from the executor’s side
src/work-doctor-controls.mjs reaches no child process, no dynamic import(), no node:fs/promises
and no wall clock, and no module under src/work-audit/ is reachable from src/work-doctor.mjs or
from any module in its CHECK_GROUPS registry – asserted over the whole import closure rather than
over direct imports. The audit’s finding codes are disjoint from CONTROL_FINDING_CODES, so neither
command’s severity table can decide the other’s meaning.
59/03 · Staleness, silence and the prune
An anchor that has not been refreshed is not an anchor
src/work-loops-checks.mjs judges every kind: anchor node against a window handed in on the call
and answers in three states, not two: fresh, stale, and undated. An anchor exactly at the window
boundary is not yet stale. Undated is its own answer, so the sixteen anchors shipped before 59 are not
red on arrival.
A stale anchor degrades a grounding verdict rather than deleting one
A loop grounded only through stale anchors is not reported unanchored – the edge is declared and the
authority resolves. The report distinguishes never grounded from grounded a while ago, which is
the distinction an operator acts on.
Silence is one rule across every channel
An instrument that has produced no reading inside its own declared cadence: window is named, and the
window comes from the record rather than from a per-channel table – so a new channel needs no new
check. A declared cadence with no occurrence count supplied is unjudgeable with reason
no-occurrence-count, never reported silent: a verdict about a question nobody asked is the shape
ADR-004 §1 forbids.
A metric that has not moved is named, and the threshold has one home
A counter unchanged across the declared number of cycles is reported, and that number appears exactly once in the module – at its declaration. No check states it as a literal of its own.
A loop nobody consults is a prune candidate, reported and never removed
A declared kind: loop node with no inbound consumer and no observed execution is reported by name.
Nothing is deleted: the auditor kind admits no actuator, and removing a node is an edit to a governed
declaration.
Every audit lane declares what it read, against a floor, in one shape
AUDIT_LANES declares an id, a population description, a basis and a floor for each of the four
lanes; a clean lane result is not representable without a read count, and a count below the floor
emits audit-ran-on-nothing naming the sweep, the root walked and the floor missed – byte-identical
to the census lane’s finding for the same read. The registry is closed and in bijection with the
exported assessments, so a lane cannot arrive without a floor.
The checks leaf is still a pure leaf
The module imports nothing, names Date nowhere, reaches no monotonic clock, and holds no date
literal and no numeric literal large enough to be a duration in milliseconds – including in
exponential, hex, BigInt and product-of-literals notation. Every freshness and silence comparison
rides on a now and a window supplied by the caller, and no kind admits a key by which a node could
assert its own liveness, freshness, consultation or audit status.
59/04 · The audit face
One command over every instrument
aof work audit [scope] [--json] [--strict] is registered on the same command core work:doctor
sits on, with a derived route and a CLI↔registry bijection, and runs every lane in the audit’s lane
registry: the census, the evidence re-run and the registry checks. The report says which lanes ran
and what each of them read. A scope narrows what is audited and is named in the report; a scope that
matches nothing produces an empty report rather than an error.
--strict is the door into the gate, and it promotes nothing
work:audit exits 0 on an error finding without the flag and 0 on a warning even under it, gating
only on --strict and an error. That is deliberately not work:doctor’s policy, which gates on
an error either way and promotes a warning under --strict: the two tables differ in exactly the
cells error/no-flag and warn/--strict. The finding set is identical with and without the flag
on both commands, and --json’s healthy agrees with the exit code.
Bad news is addressed to the reference-owner and never to the audited loop
Every finding carries about (the instrument) and to (who hears). The addressee is computed: the
instrument resolves to the loops that own it, each owner’s target-setting source is the audience,
and any candidate that is itself an owner is subtracted. Over the shipped registry no finding’s
addressee is a loop that owns the instrument the finding is about, and addressing does not vary with
severity – severity says how bad, addressing says who hears.
An escalation channel the audited loop cannot absorb
A finding whose code is in the escalating set reaches the auditor’s declared escalation: actor as a
second addressee, with the owner’s copy still present. Where no reference-owner resolves, the
finding escalates rather than being dropped. The bypass terminates at an actor whose ground: is
exogenous, never at another loop.
A day-one auditor, shipped and admissible
src/bundle/loops/instrument-audit.md is the framework’s own auditor, written in 59/00’s grammar.
Every pointer in its audits: resolves to a file, a registered command or a declared node and none
is a work item; its measurement is a registered command and cites no document as its authority; it
declares no reporting edge to anything in its own audits:; and the shipped registry produces zero
gating findings. Its cadence is declared (event:per-milestone) and nothing schedules it – the audit
is runnable on demand and the record says so rather than implying a scheduler that does not exist.
Every declared limit is stated in the face, in one shape across all lanes
A lane’s limit – the sentence saying what this run could not see – is one record
(src/work-audit/reads.mjs: LIMIT_KEYS, limitRecord, limitDeclarationProblems), complete by
construction, refused at construction and again at lane assembly. The human face renders every
declared limit with its own text, attributed to the lane and the sweep it qualifies; the two faces
carry the same limits; and a clean lane still states its limit, which is the case the limit exists
for.
58 · Supervising loops
The loop graph has an ABOVE, and it is queryable
Before this milestone the registry was a flat set of loops with edges between peers; “who may change
this loop’s target” had no answer a command could give. It now does, at every declared loop, through
one edge kind (target-setting) whose admissible sources are frozen at three – a slower loop, an
actor, or a frozen-rule anchor. The hierarchy ACD always had as documents is now an edge set the
checks walk and the face renders. The seven day-one edges are m58/01/the seven ownership edges; the
vocabulary that admits them is m58/00/every declared loop has a named owner.
Three of milestone 52’s six checks stopped describing and started refusing
reference-ownership, actuator-arbitration and timescale are the checks 52 shipped that had
nothing to decide over: no ownership edges existed, no arbiter kind existed, and six of seven loops
have no clock. All three now gate at error severity – m58/02/the structural codes stop the run –
and the reason they can is that 58/01’s records gave them something true to be true about. A check
that reports and a check that refuses are different artifacts, and this milestone is where three
crossed over.
The ordering that made the gate safe to switch on
The promotion to error waited for the records: measured over this repository’s own registry, the
sequence is 39 findings → 36 after 58/01 → 32 after 58/02, 0 errors throughout. Promoting first
would have delivered fifteen error-severity findings through aof:validate’s hard loop lane for work
that was merely unfinished. A gate that arrives red for unfinished work is a gate somebody switches
off, and this milestone’s own ordering edge is the reason this one arrives green.
Supervision is computed from records, never self-declared
No node kind admits a key by which a node asserts its own supervision, layer authority or arbitration
– no supervised-by, no arbitrated-by, no independence – and the checks module that decides all
of it still imports nothing. Every supervision fact in this system is derived from what records
declare about their own fields, which is what makes the graph auditable rather than aspirational.
A node kind that records a trade-off and cannot act on it
arbiter is the fifth kind, and the load-bearing part is an absence: it admits no actuator, no
measurement, no cadence and no ground, so a node resolving a conflict between loops has no
vocabulary in which to say it also pulls one of the actuators it arbitrates. The refusal costs no new
code – the loader’s existing loop-key-not-admitted-for-kind fires – and no new finding code.
Enforced end to end: m58/00/an arbiter has no vocabulary, cleared only on an arbiter by
m58/02/only an arbiter clears a shared actuator, and visible as its own glyph by
m58/03/every declared kind renders as its own shape.
The registry’s citations are now read by something
The registry’s entire value is that an authored edge declares itself authored and a discovered one
cites the artifact it was read from. Until this milestone closed, nothing read those citations –
52/ADR-013 had routed prose-body line citations out of its census as not-black-box – and twelve of
fifteen defining-line claims were wrong, by up to 355 lines, with two records inside one diff giving
different lines for the same export. FF-5810 now asserts in-range and defining-line over
src/bundle/loops/** on every build. This is a milestone-level fact rather than 58/01’s, because the
gap it closes predates 58 and spans every record 52, 55 and 57 shipped.
Four kinds of failure this milestone had to survive, and did
Six architecture defects found at contract-authoring, five partition defects found by the developer sweep, one false acceptance criterion refused by QA, and two blocker findings at the verify gate. All ruled or repaired before accept, each minuted at the § it amends. The count is the outcome: a milestone that declares ten controls and seven ADRs generates defects at a rate its own review stages have to be able to absorb, and this one measured that they can.
58/00 · The supervision vocabulary
arbiter is a declarable node kind
NODE_KINDS is five frozen literals – loop, actor, anchor, watcher, arbiter – and
ENDPOINT_SCHEMES admits arbiter: as an endpoint, so a record whose whole purpose is to resolve a
conflict has a kind of its own rather than being spelled as a loop that happens not to run.
An arbiter must say which conflict it resolves, in what order, and for how long an adjustment stands
resolves, priority and dwell are required for the kind. resolves follows counter’s rule
exactly – one branch, not two copies – admitting a non-empty phrase and refusing every
SENTINEL_TOKENS member and every RESERVED_FIELD_PREFIXES entry, so resolves: prose:…,
resolves: config:… and resolves: unknown are each loop-bad-value, while controlled keeps its
distinct pointer-or-phrase rule. dwell admits exactly cycles:<n≥1> and none, and not unknown.
An arbiter has no vocabulary in which to say it also acts
ADMITTED_KEYS.arbiter omits actuator, measurement, cadence and ground. An arbiter that
claims to pull one of the actuators it arbitrates over is refused by the loader’s existing
loop-key-not-admitted-for-kind – no new code and no new finding code.
A loop declares the timescale it runs at, and only a loop may
layer: is an optional scalar on kind: loop alone, taking the frozen three-member enum
operational / management / governance. It is optional by design: with no layer: declared
anywhere, milestone 52’s timescale behaviour is byte-for-byte what it was.
The ordering the checks compare has one home, and it is the loader
src/work-loops.mjs owns the layer→rank and cadence-trigger→scope-rank maps and hands the ranks over
already computed on the parsed fields. src/work-loops-checks.mjs spells no layer name and no trigger
token, so nothing derives a duration from either.
The widening deleted nothing
All fourteen records shipped before this milestone parse with zero new findings, measured against the
signature milestone 57 froze: the same codes in the same counts (12 loop-field-prose-only, 6
loop-owner-unknown, 18 in total, no error anywhere), and every record keeps the kind it had –
seven loops, two actors, two anchors, three watchers.
A registry fixture copies a subset closed under its own endpoints
test/support/registry-fixture.mjs is the one helper that copies records out of src/bundle/loops/
into a temp registry, and it transitively adds every record a copied record’s endpoint names. No
fixture it builds reports loop-graph-dangling-endpoint, and no test file reaches the shipped
registry by any other route.
The six check ids have one authority
CHECK_IDS and src/work-doctor-loop-ready.mjs’s COMPOSED_CHECK_IDS are asserted identical in
members and order, so a seventh check cannot be added to the checks module while doctor silently
scores six. The loader is never asked what the checks are called.
58/01 · The reference hierarchy and the arbiter
Every declared loop has a named owner for its reference
All seven kind: loop records in src/bundle/loops/ carry an inbound target-setting edge, and
loop-unowned-reference reports zero over the installed registry where it reported five on the day
this story opened. Revising a loop’s target is now a cycle with a named owner rather than an edit
somebody makes.
The seven ownership edges, and what each one is
actor:operator sets the references of loop:autonomous-cascade, loop:mesh-assignment-reclaim,
loop:retrospective-memory-ingest and arbiter:speed-thoroughness-autonomy; actor:product-owner
sets loop:verify-triage-accept’s; loop:autonomous-cascade sets those of loop:build-to-green and
loop:review-fix-rereview – the slower loop above the faster one, which is the structural move the
milestone exists to make; and anchor:run-lifecycle-policy sets loop:run-resilience’s.
An authored edge says in its own record that it was authored
Every record declaring an edge this milestone decided states so in its own body and offers no citation for the relation it decided, while separating out the discovered fact underneath it. Every record declaring an edge the repository already stated names the artifact it was read from. Which kind an edge is can be answered by reading one record, without opening another file.
The one reference no cycle revises is carried by an anchor
anchor:run-lifecycle-policy has ground: frozen-rule and observes:
module:src/run-store.mjs#isLegalTransition, so the authority behind loop:run-resilience’s reference
is a thing that resolves rather than a sentence written into a loop record to fill a gap. It is the
first frozen-rule anchor the registry carries.
Every loop declares the timescale it runs at
Seven declared layers – one governance, two management, four operational – each corroborated by
its cadence’s scope rank, with at most one uncorroborated and that one named and defended in its own
record. This is what made 52’s timescale check able to decide anything: six of the seven loops have no
clock, so before the layer axis the check had nothing to compare.
One arbiter owns the standing speed-versus-thoroughness-versus-autonomy trade-off
arbiter:speed-thoroughness-autonomy resolves the named conflict, vetoes all four contenders for the
three shared agent actuators, declares a priority that is a permutation of exactly that veto set, and
a dwell. Every config: endpoint of its parameter-tuning is cited as a ceiling: pointer by one
of the loops it vetoes. loop-shared-actuator-unarbitrated reports zero where it reported three.
The records ship as bundle assets and land in a project
All 16 loop records are registered in src/bundle/manifest.json, and aof work update installs them:
.aof/loops/ holds 16 records byte-identical to src/bundle/loops/, with 16 matching entries in
.aof/aof.lock.json. A record that lives only in the source tree is in no project’s registry, and the
gate 58/02 turns on reads the installed copy.
Every citation a shipped record writes resolves
59 in-repo path citations are in range, and every `<symbol>` at `<module>:<line>` claim names the
line that symbol is actually exported on – asserted by FF-5810 over src/bundle/loops/** on both
legs, each probed red. The registry’s value is that an authored edge is honest and a discovered one
cites; a citation nothing checked was the half that had already rotted.
58/02 · Layer separation, arbitration and the gate
The timescale check decides on the layer where the clocks cannot
checkTimescale compares declared layer ranks whenever both ends of a target-setting edge carry
one, and falls back to the clock only where they do not. A supervisor at no slower layer is
loop-layer-inversion; one reaching more than a single boundary down is loop-layer-skipped. Over
this repository’s registry the check now decides seven loops across three layers, where before this
story it had reported nothing since the day it shipped.
The layer axis is additive over the cadence axis
With no layer: declared anywhere, the check’s output over the closed cadence cross-product, the
ratio-3 boundary, the four single-node fixtures and the non-target-setting case is identical to what
milestone 52 shipped. Both behaviours are asserted by one authority: FF-5802 extends 52’s own
comparability guard rather than standing a sibling beside it.
Nothing derives a duration from a trigger or a layer
Where neither axis can answer – no clock at both ends, no layer at both ends – the finding is
loop-timescale-not-comparable at warn, an honest absence rather than a fabricated conversion. The
layer→rank and trigger→scope-rank maps exist only in src/work-loops.mjs; src/work-loops-checks.mjs
contains no LAYER_VALUES or EVENT_TRIGGERS member and receives every rank already computed on the
parsed fields.
The separation ratio is a frozen literal that no configuration can lower
MIN_SEPARATION_RATIO === 3, exported so a control can assert the number rather than grep for a
digit, and resolved by no config key. A project cannot lower it to 1 and thereby declare a supervisor
that runs as fast as what it supervises.
Only an arbiter clears a shared actuator
checkActuatorArbitration clears a contended actuator only on a non-contending node whose kind is
arbiter. A vetoing actor, loop, anchor or watcher no longer clears it – the finding names the
pretender and its kind. Every arbiter’s priority must be a permutation of its own veto endpoint
set, with no duplicate, extra or omission, and no arbiter may declare target-setting: the kind that
records the trade-off is the kind that cannot act on it.
An arbiter is a node the checks can see
isGraphNode accepts every member of NODE_KINDS, so a kind: arbiter record is a member of the
graph all four structural traversals walk. This is what took loop-shared-actuator-unarbitrated from
3 to 0 over the shipped registry, and it carried the two grounding movements with it. A sixth kind
added to the loader without being admitted here fails CI instead of being silently filtered out of
every check.
A target-setting edge from an inadmissible source is reported, not accepted
Ownership resolves only from a slower loop, an actor, or an anchor whose ground: is frozen-rule.
Anything else is loop-target-setting-not-admitted, and a loop with no inbound edge at all is
loop-unowned-reference.
The structural codes stop the run
GATING_CODES is a frozen thirteen-member set – 57’s five plus this milestone’s eight – and severity
is derived from membership in it rather than from any hardcoded constant. Nothing inherited moved:
every grounding, anchor and watcher-census code and all seventeen loader codes retain the severity
they had. The two codes that report a preference (loop-layer-skipped) or an honest inability to
decide (loop-timescale-not-comparable) stay warn.
The exit decision lives only on the face
src/work-loops-checks.mjs returns findings and decides nothing about the process; the face reads the
summary and chooses the exit. run() returns an identical result whether or not anyone is gating on
it. src/work.mjs is not edited and no seventh doctor lane exists.
Supervision is computed, never self-declared
No kind admits a key by which a node asserts its own supervision, layer authority or arbitration –
there is no supervised-by, arbitrated-by, dead-band or independence key in any admitted set,
and owner is admitted on kind: loop alone. The ownership, layer and arbitration requirements are
emitted as findings rather than required as keys. The checks module still imports nothing.
The gate turns on green
Measured over this repository’s installed .aof/loops/ at acceptance: 0 errors, 32 warnings, exit 0,
with reference-ownership, actuator-arbitration and timescale each running and reporting nothing.
58/03 · The supervision face
Reading a loop names the layer it runs at and the node that sets its reference
aof work loops show --id <loop> renders the declared layer and the node holding the inbound
target-setting edge on the same line as the node itself – measured live over this repository’s
installed registry: loop:build-to-green · loop · Build executable work to green · layer operational ·
reference set by loop:autonomous-cascade.
An absence is stated in words where it is a defect, and nowhere else
A loop nobody supervises and a loop that declares no layer each say so; a kind that has no layer axis renders no layer at all and is named only when something sets its reference. The face does not print an empty field to keep its columns even.
owner: and the reference-setter stay two different facts
The face never conflates who is accountable for a loop with who sets its target, a loop pointing a
target-setting edge at itself is not reported as its own supervisor, and a setter declared by a
kind that may not set one is still named rather than quietly dropped – the face reports what the
registry declares, including where the registry is wrong.
Two setters are both named, in id order, and the two faces agree
Where more than one node sets a reference the face lists every one of them deterministically; the machine face is a shape a caller can depend on; the human and machine faces carry the same answer node by node; and reading one node agrees with reading the whole registry.
Every declared kind renders as its own shape
renderLoopGraph draws five distinct glyphs for five declared kinds – ["…"] for a loop, (["…"])
for an actor, (("…")) for an anchor, {{"…"}} for a watcher, {"…"} for an arbiter – where before
this story an anchor, a watcher and an arbiter were the same picture as each other and the same
picture as a dangling reference. The glyph table lives inside the exported pure renderer, so the glyph
set can be asserted without standing up a workspace.
The fallback for an endpoint nobody declared is untouched
[/"…"/] still renders command:, config:, module: and dangling loop: endpoints, and it is
also what a record whose kind: the vocabulary does not admit falls to – so an unadmitted kind
borrows no declared kind’s glyph. No declared kind’s shape equals it.
The sixth kind fails CI until somebody gives it a shape
FF-5808 asserts glyph-to-kind parity in both directions, and its second leg is the one that makes
the guard real: a sixth kind handed the fallback shape would still yield six distinct shapes across
six kinds, so cardinality alone is satisfied by the exact collision this control repairs. The two legs
together mean the next kind cannot be added without a glyph of its own.
The picture is deterministic
The same registry renders byte-identically twice over, in a second process, and whatever order the records were discovered in. Node keys and edge lines are unchanged by the new shapes.
57 · Paired loops
Every optimizing loop aof runs is watched, and an unwatched one stops the run
aof work loops validate over .aof/loops/ reports 14 records including 3 watchers, 0 errors, 39 warnings and exit 0, with zero loop-unpaired-optimizer – where the same command over the same registry reported three before this milestone.
The pairing is a fact about aof’s own tree, not a rule aof ships to other repositories
The three watcher records are both bundle members and installed records: .aof/loops/ carries them byte-identically to their src/bundle/loops/ sources, so the framework satisfies the rule it enforces, and FF-5313 fails if a shipped loop record is ever left uninstalled.
A counter-metric that reports nothing reports that it measured nothing
Both counter-metric commands refuse a number rather than inventing one – work:counters returns status: "unmeasurable" with no count key at all, and work:ratchet returns ratchet-base-unresolved with no legs computed and exit 1. Two stories arrived at the same refusal independently, so a watcher that has never observed anything cannot satisfy the pairing gate with a zero.
Day one carries no judges
All three shipped watchers declare determinism: counter, and loop-watcher-is-judge – a permanent, non-gating census – reports zero on the shipped table.
Independence cannot be declared by anything, including the framework itself
No node kind admits an independence key and no loop may name its own watcher, in the shipped grammar and in every record installed: the property is computed from parsed records on four legs, each with its own finding code. See m57/01 for the legs.
Every structural invariant this milestone declared is enforced and has been seen to fail
Eight declared controls, all landed, none carrying pending, each red-probed – 49 probes, each restored byte-exactly with its control re-run green on the restored bytes. Two of the eight extend guards already in service rather than adding siblings.
The framework’s own build loop has a counter-metric that a machine produces
command:work:ratchet resolves to a registered route, and each watcher’s measurement pointer resolves to a registered command or an exported symbol – a declared counter that pointed at nothing would be FF-5707-red rather than merely unhelpful.
57/00 · The watcher node
kind: watcher node class
The loop registry admits a fourth node kind beside loop, actor and anchor, requiring id, kind, title, counter, determinism and measurement, and carrying the same five edge keys as every other node.
A watcher has no vocabulary for acting
ADMITTED_KEYS.watcher omits actuator, and with it controlled, ceiling and optimizing; a watcher declaring any of them is refused by the loader’s existing loop-key-not-admitted-for-kind – no new check, no new finding code, no special case.
determinism: – two literals, and no default
DETERMINISM_VALUES is counter and judge, a frozen set in src/work-loops.mjs; any other value is loop-bad-value and an absent one is loop-missing-field that leaves the key off the parsed node, so a record that does not say how its number is produced is incomplete rather than assumed.
counter: is a phrase, and only a phrase
A watcher’s counter: admits free prose alone – the three pointer schemes, prose:, the three sentinel tokens and the empty string are each loop-bad-value – so the quantity being counted is named in words while the instrument that produces it stays in measurement:.
The pairing is declared by the watcher, outbound, on the edge that already existed
EDGE_KEYS is still 52’s five literals: the pairing is a monitoring edge on the watcher naming the loop it watches. A loop carrying a watcher: key is refused as loop-unknown-key, and loading a watcher’s edge synthesises no reverse field or edge on the watched loop.
The eleven records shipped before the widening parse unchanged
Every record milestones 52 and 55 delivered loads with a finding set identical file-for-file, code-for-code and key-for-key to its pre-widening signature – 11 nodes, 18 findings, zero new.
FF-5701, armed
test/arch/acd-watcher-taxonomy-additive.test.mjs fails when NODE_KINDS drops a prior kind, when a record milestone 52 or 55 shipped stops parsing, when ADMITTED_KEYS.watcher gains actuator, when DETERMINISM_VALUES gains a third literal, and when determinism stops being required for the kind – each leg observed red and restored.
57/01 · Independence computed, and the gate
An optimizing loop with no watcher fails the run
loop-unpaired-optimizer is error, and aof work loops validate exits non-zero when any
error-severity finding is present. The check that has existed since milestone 52 now stops a run
instead of adding a line to it.
Watcher independence is a computed property of the records
Four independence legs read the parsed model alone and each carries its own code:
loop-watcher-shares-measurement when a watcher’s measurement pointers intersect the loop’s,
loop-watcher-shares-actuator when a judging watcher’s prose authority is also the watched loop’s
actuator, loop-counter-equals-controlled when a counter restates the loop’s controlled variable
after normalization, and loop-counter-not-deterministic when a record declaring determinism:
counter cites anything a machine cannot run. No node kind admits an independence key and no loop
may name its own watcher, so independence cannot be declared – only computed.
Every watcher that leans on a model is visible, and never gates
loop-watcher-is-judge reports every watcher declaring determinism: judge, independent or not, at
warn permanently. It is a census, not a gate: whether a metric admits a deterministic computation
is not decidable from a record.
Severity is a property of the code, and the exit lives only on the face
GATING_CODES is a frozen five-member set and finding() derives severity from membership in it
rather than from a hardcoded constant. src/work-loops-checks.mjs returns findings and decides no
exit code; loopsValidateCommand.cli.exit is the sole exit decision, and reading it leaves the run
result byte-identical. The pure check module still imports nothing.
Nothing inherited turned red
The five promoted codes are the only check codes whose severity moved. Every other inherited check
code remains warn and every loader code retains the warn/error severity it already had.
Measured on this repository at the gate: 0 errors and 39 warnings across nine inherited codes.
aof:validate runs the loop registry as its own deterministic step
The validate procedure runs aof work validate, then aof work loops validate, then
aof work doctor, in that fixed order. The loop gate is a separate workspace-wide step – not folded
into work validate, not a doctor lane – and its non-zero exit is surfaced to the operator. All
three installed runtime faces (.claude, .opencode, .codex) carry it.
57/02 · Examples rows in the parser
Every parsed scenario carries its Examples blocks
parseFeature returns a sixth key on every scenario – examples, an ordered list of { header, rows, line }, one entry per Examples:/Scenarios: block in the order the file declares them.
The row count is data rows, never the column header
A block’s rows is the number of pipe-delimited lines beneath its caption excluding the first, so an Outline is reported with exactly as many cases as a runner would execute – 1,217 blocks and 5,333 data rows across this tree, where the count was previously 0.
A block’s boundaries are the ones a reader sees
Blank lines and #-commented rows inside a table contribute nothing and do not close the block; a step, a tag, a docstring, or the next structural keyword does close it; and pipe-delimited lines inside a docstring are data, not a table.
An absent table is an empty list, never an absence
Every scenario carries an array – an Outline with no Examples: block and a plain Scenario both report [] – so a consumer counts a zero rather than branching on a missing key.
The five keys the parser already reported are byte-identical
Over all 803 feature files in the tree, name, outline, lane, verification and line, the tag resolution behind them, and the firstViolation/freeTextLines litmus verdict are deep-equal to the pre-story parser’s output, with examples the only difference; key order is name, outline, lane, verification, line, examples.
The parser’s three production consumers are unchanged
src/work.mjs, src/commands/tasks.mjs and src/work-doctor-rubric.mjs are unedited by this milestone and name the new key nowhere; executableScenariosOf still defines an executable scenario without reference to rows.
FF-5704, armed
test/arch/acd-feature-parse-examples-additive.test.mjs fails when the widening moves an existing scenario key, when it moves the litmus verdict without touching a key, when examples stops being an array on any scenario, and when any of the three consumers reads the new key – each leg observed red and restored.
57/03 · The contract-integrity ratchet
aof work ratchet <ref> reports contract movement as four legs
A registered CLI command returns contract, closed-set, marker and compensating-assertion, each carrying fired / discharged / unclassified / clear and the findings behind it; --json and the rendering carry no precision claim of any kind.
The baseline is the item’s own record, and an unresolvable one produces nothing
The base commit is the first --first-parent commit whose version of the item’s record document carries status: in-progress; a shallow clone, an unborn ref or a record never committed in-progress yields ratchet-base-unresolved, exit 1, no legs computed and no comparison against HEAD~1. --base <commit> overrides it and is recorded in the output as supplied.
An acceptance criterion is a scenario or an Examples row, counted item-wide
Leg (a) compares @executable scenarios plus Examples rows across all of the item’s task features as one total, so deleting a row shrinks the contract and moving a scenario between task files does not.
A closed-set assertion may not be relaxed on a file that existed at the base
Leg (b) fires on closed-to-open only, over files present at the base commit; a closed-set assertion added to the same file in the same change clears it as leg (d); anything the frozen vocabulary cannot classify is reported ratchet-unclassified and counted as neither fired nor clear.
A skip, only or todo added to a pre-existing test is a fire
Leg (c) fires only when the marker is added and only when the test existed at the base commit; a marker inside a comment is not a marker, a removed marker is not a fire, and a newly authored todo is not a retreat.
Discharge requires an authority the optimizer could not have written
A fired leg clears only against an ADR that (i) is cited by the artifact as it stood at the base commit, (ii) names the owning item – 57/ADR-007, m57/ admitted, a bare ADR-007 never – and (iii) resolves as a heading in that item’s ARCHITECTURE.md at the base commit. The owning ref is built from the numbered path segments between the work dir and the register-bearing ancestor, so a milestone register is 57 and a story-level one is 57/03.
The justification comment is structurally outside the inputs
Citations are harvested from the artifact’s base text, so a comment written with a weakening does not exist in the text the ratchet reads. The prohibition holds by construction rather than by a rule the code is trusted to follow.
The engine is pure and every repository read is at the command boundary
src/work-ratchet.mjs imports exactly ./feature-parse.mjs and ./work-doctor-rubric.mjs and reaches no filesystem, git, process or clock; the diff, both trees’ feature texts, the owning item’s register at the base commit and its ref all arrive as parameters assembled by src/commands/ratchet.mjs.
FF-5705, armed across three legs
test/arch/acd-ratchet-pure-and-discharge-scoped.test.mjs fails when the engine gains a repository read, when the boundary stops handing it a completed observation, when the no-base refusal stops exiting non-zero, when citations are harvested from head instead of base, when a bare ADR id is admitted, and when an unresolved owner stops blocking discharge – each leg observed red and restored.
57/04 · Escape and intervention counters
aof work counters <ref> reports both counter-metrics for one item and its direct children
A registered CLI command returns { ref, escape, intervention } for a milestone (itself plus its stories) or for a single story, rendered as two lines and available verbatim under --json.
A finding raised after an item was accepted is an escape, attributed to the item it escaped from
The escape counter compares each kind: "raw" feedback entry’s at against the acceptance moment of items reading status: done, reports a per-item count with the escaping entries’ ids, and totals them across the scope it was given; classification records are not in the denominator.
Acceptance is read at day granularity and resolved conservatively
A date-only stamp is treated as T23:59:59.999Z, so a finding raised on the same day as acceptance is not an escape; full ISO instants keep their exact ordering.
An intervention is a retry, a resume, an exhausted attempt ceiling or a non-retryable failure
The intervention counter reads run records already on disk and classifies per run: a record carrying retryOf is one intervention – resume when the run it retries carries resumeAfter, otherwise retry – and a terminal failure that was never retried is attempts-exhausted (at or above the configured ceiling, or attempts_exhausted by name) or non-retryable-failure.
The count never exceeds the run total it is reported against
A retried run contributes exactly one intervention regardless of how its own attempt later terminates, so count, runs and rate are a coherent triple and rate is always in [0, 1].
A counter with no evidence reports that it cannot measure, and carries no count key at all
status: "unmeasurable" with a reason of accepted-items-absent, feedback-absent or runs-absent is returned instead of a number, and the result object has no count property – so a consumer cannot read a zero that was never measured. A genuine zero returns status: "measured" with count: 0, and the two are distinguishable without inspecting the number.
A partly measurable scope reports what it measured and names what it could not
measuredItems, unmeasuredItems and missing accompany every measured result, and the rendered line appends ; N item(s) unmeasured.
The arithmetic is a pure leaf and every record read is at the command boundary
src/work-counters.mjs imports nothing and reaches no filesystem, process or clock; src/commands/counters.mjs holds the only reads (readFeedbackRecords, readRuns, the record’s frontmatter) and no write API of any kind. Both properties are enforced by test/arch/acd-work-counters-read-only.test.mjs.
Nothing new is written to disk by either counter
No instrumentation was added, no record gained a field, and both counters are readers over records milestone 20 and the feedback path already produce.
57/05 · The pairing table
Every optimizing loop aof ships is watched
src/bundle/loops/ carries three kind: watcher records – watcher:build-to-green-watcher, watcher:review-fix-rereview-watcher and watcher:autonomous-cascade-watcher – one monitoring edge each at loop:build-to-green, loop:review-fix-rereview and loop:autonomous-cascade, the only three loops in the registry declaring optimizing: true.
The watchers install by the ordinary update path, and are framework-owned
The three are bundle.json asset members targeting .aof/loops/, rendered verbatim for both the claude and codex runtimes; each carries the # aof-generated: true marker, so aof work update classifies an operator-edited copy drift-warning and preserves it rather than overwriting it.
Every watcher’s number is produced by a machine, and the pointer resolves
All three declare determinism: counter with a single command: measurement pointer – command:work:ratchet on the build watcher, command:work:counters on the other two – each naming a route registered in the command registry. No watcher declares prose: measurement, and none is a judge.
The independence legs hold on the records rather than being asserted about them
Each watcher measures a different artifact from the loop it watches, counts a different quantity from that loop’s controlled:, and declares no actuator – the kind admits no such key. The shipped registry loads with zero error-severity findings while the inherited loop-owner-unknown and loop-field-prose-only warnings remain warnings.
aof’s own registry satisfies the rule aof ships
.aof/loops/ holds 14 records including the three watchers, byte-identical to their bundle sources. loadLoops over it reports 14 nodes, 3 watchers and zero error-severity findings.
A shipped loop record that is not installed is now a red control
test/arch/acd-registry-framework-owned.test.mjs (FF-5313) asserts every .aof/loops/ bundle member is present on disk and byte-identical to its src/bundle/loops/ source – the byte comparison the .aof/loops/*.md text eol=lf pin in .gitattributes was already written for.
55 · Anchors & the frozen set
aof’s own loop graph carries a measured groundedness verdict
aof work loops groundedness answers, for this repository, that 11 components stand at 5 anchored / 2
exogenous-only / 4 self-referential, with loop:autonomous-cascade, loop:retrospective-memory-ingest,
loop:review-fix-rereview and loop:verify-triage-accept named as anchored by nothing – the taxonomy
(m55/00) and the report (m55/01) joined into a reading of the real graph rather than of a fixture.
“Ungrounded” is a computed property of the declared graph, not a judgement about it
A component’s verdict is derived by flooding from every node bearing an admitted ground: value and
decomposing the graph 52 declared, so the answer changes only when the registry changes – no reviewer’s
reading enters it, and the same registry and checkout always produce the same verdict.
L3 executes, and the gate that opens it is computed from this milestone’s own measurements
Milestone 53’s locked rung is unlocked (m55/05) and admitted only on a Loop-Ready score of 100
together with a groundedness report carrying no self-referential and no stale component – so the
rung the ladder declares and the ground the loops settle against are one mechanism, not two.
This workspace has not earned L3, and the refusal says which half and why
aof work doctor reports 50% (5/10) with grounding, anchor-grounding, pairing,
reference-ownership and actuator-arbitration blocking, and four self-referential components stand
in the report – so unattended self-driving is open in the ladder and closed here, by measurement.
Every claim aof records is stamped or refused, and every human input is captured verbatim first
The two integrity rules are structural rather than emphasis: a claim without {node, run, commit, at}
is refused at the write seam and never back-filled (m55/02), and a capture path that accepts a
classification argument does not exist (m55/03) – each enforced by a control, not by prose.
The frozen set names its own anchors, so the milestone’s two halves close on each other
src/bundle/frozen-set.jsonc’s six members include anchors alongside locked-contract, litmus,
tag-vocabulary, gate-order and test-isolation (m55/04), so the taxonomy this milestone declares
is itself a frozen member compiled to the enforcement boundary aof owns.
Eight architectural controls, every one watched to fail in the form it shipped
FF-5501…FF-5508 are landed, registered in the runner’s own labelled milestone-55 blocks, green, and each carries a red probe performed against a real source edit restored byte-exactly afterwards – the two that act on a guard already in service (FF-5503 extends, FF-5508 supersedes) probed on the extension and the replacement rather than on the surviving legs.
55/00 · The anchor taxonomy
kind: anchor node class
The loop registry admits a third node kind beside loop and actor, requiring id, kind, title, ground and observes, and carrying the same five edge keys as every other node.
The six-member ground: taxonomy
GROUND_VALUES is process-exit, build-stamp, landed-commit, live-soak, frozen-rule, exogenous – a frozen set in src/work-loops.mjs, admitted on kind: actor and kind: anchor and on no kind: loop node at any value.
observes: – the registry’s one no-prose field
An anchor’s authority parses through the pointer grammar alone: a module:, command: or config: pointer is admitted, and prose:, unknown and free text are each a loop-bad-value refusal rather than the declared gap they remain on every other key.
The nine milestone-52 records still parse byte-unchanged
Every record the previous milestone delivered loads with zero error-severity findings after the widening, and actor:operator’s ground: exogenous is now a member of the taxonomy rather than its only value.
Two framework anchors, delivered through the bundle
anchor:rubric-process-exit (ground: process-exit, observing src/commands/grade.mjs#reportObservation) and anchor:run-liveness (ground: live-soak, observing src/run-store.mjs#isStale) ship as bundle assets for both runtimes, install byte-identical to source into .aof/loops/, and declare three data-feed edges between them – each edge cited in the record’s own body.
FF-5501, armed
test/arch/acd-anchor-taxonomy-additive.test.mjs fails when the enums drift from their literals, when a set stops being frozen, when ground becomes admissible on a loop, when observes accepts prose, and when any milestone-52 record stops parsing.
55/01 · The groundedness report
Grounding is seeded by ground:, on any host permitted to carry one
analyseGrounding floods from every node bearing an admitted ground: value – kind: anchor and kind: actor alike – rather than from the single actor + exogenous predicate milestone 52 shipped, and every grounded component carries the groundClasses that supported it.
A fourth verdict, stale, distinct from both its neighbours
A component’s verdict is one of anchored, exogenous-only, self-referential or stale, and an anchor that resolved once and does not now reports stale – a different token from a component that never had an anchor, distinguishable without reading any message text.
A loop nothing anchors is named, not merely absent from a list
Every kind: loop node with no inbound data-feed edge from a kind: anchor node appears in unanchoredLoops and raises a loop-anchor-absent finding naming it; the count is reported beside the names. In this repository that is four – loop:autonomous-cascade, loop:retrospective-memory-ingest, loop:review-fix-rereview, loop:verify-triage-accept – against 11 components at 5 anchored / 2 exogenous-only / 4 self-referential.
work:loops-groundedness – a registered command with a frozen --json document
The report is reachable as aof work loops groundedness --json and in-process through invokeRegistered("work:loops-groundedness"), returning {source, present, state, components, unanchoredLoops, authorities, findings, summary}, byte-identical across runs on an unchanged registry and repository. It has no board route: loops-groundedness sits in BOARD_DEFERRED beside loops-show / loops-graph / loops-validate, so the loop family opens no /api/work door.
The three pointer schemes are resolved, not merely parsed
resolveAnchorAuthorities answers {pointer, resolved} per anchor: a module: pointer resolves when the named symbol is genuinely exported from a file inside the owning root, a command: pointer when the registry registers that id, a config: pointer when the workspace config declares that dotted key – and each returns false rather than throwing when it does not.
The checks stayed pure, and the guard that says so was widened to cover it
src/work-loops-checks.mjs imports nothing and performs no filesystem, process or clock read; resolution arrives as a map argument built at the command boundary. 52’s purity guard was extended to cover the resolution map rather than joined by a sibling.
The verdict is returned, never written
Producing the report writes nothing to disk – no verdict, no resolution result and no staleness record is persisted by this story.
FF-5502 and FF-5503, armed
test/arch/acd-anchor-grounding-seed.test.mjs fails when the seed narrows back to a kind predicate, when the SCC decomposition body moves from its pinned digest, when the verdict set drifts from its four literals, or when a sixth edge key appears; the extended test/arch/acd-loop-checks-pure.test.mjs fails when any I/O re-enters the checks.
55/02 · Provenance at write time
The claim provenance envelope
Every recorded claim carries a four-key stamp – node, run, commit, at – compiled by
src/claim-provenance.mjs, a leaf module that imports nothing and reads no clock, filesystem,
process, git checkout, transcript, log, mtime or directory listing.
Refusal in place of back-fill, at the durable write seams
mintRun and recordAnchorReading validate the stamp before the store is read or written, so a claim
offered without a complete one is refused with claim-provenance-missing naming what was absent, and
no writer completes a partial stamp from anything it could have inferred.
Anchor readings that ride the run record
recordAnchorReading appends a reading to the run’s brief under the work item’s own runs/, so
readings accumulate rather than overwrite, no sidecar store exists, and nothing writes a reading into
the bundle-delivered .aof/loops registry.
One impure gathering edge
gatherClaimProvenance at the grade command is where the facts are collected; the compiler is handed
them, which is why the same observation always compiles to the same record.
55/03 · Raw capture before classification
A verbatim feedback ledger, written before anything classifies
Each capture appends the raw {kind, id, text, actor, refs, at} record to the item’s
FEEDBACK.ndjson before the human-readable STATE.md bullet is projected, and the ledger’s module
holds no writeFile, rename or truncate – a raw record cannot be opened for rewrite because
nothing that owns it can rewrite.
A capture path with no menu, at both doors
work:feedback accepts exactly ref, note, actor and refs in its command schema and in its CLI
flags, the bundled /aof:feedback command offers no AskUserQuestion, and an unknown field is
refused with feedback-classification-deferred before the item is resolved or anything is written.
Classification as a strictly later, separate record
A triage is appended as its own record referencing the raw one by id, so re-triage leaves the verbatim text byte-identical, an untriaged capture is complete on its own, and a classification cannot exist without a raw record to point at.
One production raw writer
Exactly two modules reach appendRawFeedback – the capture transition and the ledger itself – so
every capture path in the system, CLI or board, lands the same verbatim record first.
55/04 · The frozen set, compiled
The frozen set as a versioned declaration
src/bundle/frozen-set.jsonc declares six members – locked-contract, litmus, tag-vocabulary,
gate-order, test-isolation and anchors – each naming the enforcement point it protects, and it
installs into a consumer as .aof/frozen-set.jsonc.
A compiler from declaration to enforcement point
compileFrozenSet emits hook entries, permission denials and agent tool scopes that each carry their
declaring member’s id, so no aof-authored rule exists at any enforcement point without a declaration
behind it; a member that cannot reach its enforcement point refuses the compile atomically rather than
partially applying.
A surgical permissions merge
.claude/settings.json is merged array-wise through splicePermissions, with permissions removed
from the patch before the top-level spread, so every operator entry survives a compile with its value
and its position, and a compile that changes nothing writes nothing.
Tampering as a coded event, with the human’s way out intact
Drift on a frozen member emits frozen-set-tamper carrying the member id – distinct from the
pre-existing preference-drift warning – while an entry whose ownership marker has been removed is
neither edited, retracted, re-marked, nor reported as a tamper, and the report states that escape
where the person will see it.
The test-isolation guard as compiled output
The rule that blocks an unisolated test run is now the compiled output of the test-isolation member,
delivered as .claude/hooks/aof/guard-test-isolation.mjs, and it distinguishes an actual test
invocation from a read or from quoted prose.
55/05 · L3 unlocked
An L3 rung that executes
LOOP_LEVELS is ["L1","L2","L3"] and LOCKED_LOOP_LEVELS is empty; milestone 53’s L3 lock is
removed rather than narrowed, and the arch-test proxy that asserted no module could branch on L3 is
deleted rather than left passing on a premise that is now false.
Admission computed from earned facts alone
resolveLoopLevelGate admits L3 only on a Loop-Ready score equal to L3_SCORE_THRESHOLD = 100
together with a groundedness report carrying no self-referential and no stale component. No config
key, environment variable or flag anywhere in src/** admits it.
Both halves gathered through the command boundary
resolveInvocation invokes work:doctor and work:loops-groundedness through the registry before the
gate is consulted, so the loop reads the registry the way every other consumer does, and a refused
request spawns nothing and records nothing.
A refusal that names the failing half and its parts
A refused L3 request carries loop-level-gate with failingHalves, the score against its threshold
with every blocking check named, and the groundedness half’s components verbatim – machine-readable,
and a refusal rather than a crash.
The anchor check inside the readiness score
COMPOSED_CHECK_IDS carries anchor-grounding beside milestone 52’s five, composed from the same
report rather than re-derived, and an unreadable registry yields not-applicable – which does not admit
L3 – rather than a pass.
54 · Verification as a feedback loop
aof grades a work item, and the grade is a record rather than an exit code
aof work grade <ref> --run compiles a GradeRecord – verdict, codes, per-case counts, the runner’s
verbatim failures – from a report a declared runner emitted. The whole path refuses to mint a pass
from an absence: nothing yields pass without a parsed report, and the evidence floor is measured
against the cases that actually RAN. (The vocabularies, verdict rules and evidence floor are stated
whole by m54/00; the declared-argv rubric, its single bounded spawn and its read-only bare face by
m54/01.)
The loop decides on deterministic evidence before it spends a model turn
This is the milestone-level fact none of the stories states alone. Before 54, a loop’s only judge of
“is this item correct” was a model review turn. There are now three deterministic rungs ahead of it –
work:validate, work:doctor, work:grade – in declared cost order, and the loop’s next act is
derived from their answers. A story whose suite is red re-drives without a review turn being spent
on it. (The ladder and its admitted scope/severity: m54/02. The verdict-derived routing:
m54/03.)
A red gate hands the maker what went wrong, instead of the fact that something did
The grade’s record rides the run it re-drove and reaches the next session through 70/04’s existing
fix transport, and a loop that exhausts its cap halts carrying the accumulated record of what it
could not close rather than an empty list. (Stated whole by m54/03.)
An unconfigured repository is unchanged, byte for byte
Every capability above is inert where no work.rubric is declared: the LoopState, every report
line, every driver input, the driver’s spawn argv and every run record are indistinguishable from the
pre-54 shell. rubric-unconfigured is a named indeterminate that proceeds exactly as before, and
no code path maps it to a pass or to a halt. This is what makes the milestone adoptable rather than
a migration.
aof declares what it does not know, and refuses to guess it
The rubric is the project’s declaration – an argv, a report format, an evidence floor – and aof runs
precisely what was declared. It never infers a runner, never derives a case identity or status from a
runner’s free text, and reports unjoined rather than guessing a pairing. (The emitted-not-inferred
rule and the @executable→case join: m54/04.)
Ten fitness functions, each observed failing
FF-5401 … FF-5410 are declared, resolve to files on disk, and every one has a red probe recorded in
VERIFICATION.md against a planted defect. aof work doctor 54 reports zero control-unresolved
and zero missing red probes at any severity.
54/00 · The grade record – green is positive evidence
The grade record
src/work-grade.mjs compiles a GradeRecord – {ref, verdict, codes, runner, report, cases, failures, gradedAt} – whose key set is exact on every branch and whose JSON round-trips.
A closed verdict triple
GRADE_VERDICTS is a frozen exported ["pass", "fail", "indeterminate"], and no fourth verdict is reachable.
A frozen nine-code vocabulary with a producer for every member
GRADE_CODES is frozen, set-equal to ADR-005 §3’s nine, reported in its own declared order rather than the order observed, and every one of the nine is reachable through the real compiler from a committed fixture.
pass costs four pieces of positive evidence
A pass is reachable only when the declared report exists, parses in its declared format, enumerates named cases at or above the evidence floor, and every case carries a status; the exit status is read first and then discarded as insufficient – it can veto a pass and can never buy one.
The evidence floor measures cases that RAN
casesThatRan (total - skipped) is the single expression both the declared floor and the ratchet’s bar are measured on, so a report of nothing but skips is report-vacuous at indeterminate at every floor and with any history, while cases still reports the observed counts.
A ratchet scoped to the item, needing no configuration
The floor is raised by the highest cases-that-ran of the last recorded pass for the same ref, filtered by ref inside the compiler; only a pass raises it, and another item’s history moves it neither up nor down.
TAP normalisation proven against captured output
Both TAP dialects this repo produces normalise to the cases their producer actually emitted – ordinals and plan lines optional, # section headers and summary lines not cases, suites distinguished from tests by the producer’s own type field, failure messages carried verbatim – and each supported format is backed by a committed capture carrying a provenance line, never by a hand-written specimen.
Two armed fitness functions
acd-grade-green-needs-evidence (FF-5402) and acd-grade-record-envelope (FF-5403) are registered in the runner and each has been observed failing against a planted defect.
54/01 · The declared rubric
The rubric is a declaration in its own home
work.rubric is a config subtree of its own – command (an argv array), optional args.ref, env,
and report {format, path, floor} – read by declaredRubric and by nothing else.
work.controls.runners answers its unchanged question over its unchanged readers; neither key is
derived from the other, and removing either leaves the other’s answer identical.
The declared argv runs element for element, with no shell anywhere
rubricSpawnOptions passes shell: false, so the array reaches the child unsplit, unjoined and
unre-quoted, and an argument carrying shell metacharacters is one argument with its characters
verbatim – no redirection, no pipeline, no second command.
A present-but-unusable declaration halts, and is never mistaken for an absent one
A work.rubric whose command is a string, an empty array, a non-program, or absent yields
indeterminate / runner-spawn-failed and launches nothing – never rubric-unconfigured, the one
indeterminate that proceeds. The two are distinguishable by code alone, without reading a message,
and no tenth code was coined for either.
An unconfigured repo is an honest no-op at every door
No work.rubric yields indeterminate / rubric-unconfigured naming work.rubric as the key to
set, with runner and report null and the observed case counts zero rather than absent. It is
never read as pass and never as fail, through the read face, through --run, for a story or a
milestone, and on a repeat invocation.
Scope is the project’s choice, and a whole-suite run says so
An args.ref is appended to the argv exactly once, followed by the item’s own ref. Absent, the plan
carries no scope argument and reports whole: true beside scope: null – a reader can tell the
whole-suite run was the declaration’s choice rather than aof’s.
aof work grade <ref> – a read face that executes nothing
The bare verb reports the planned argv, its cwd, the report format, the absolute report path, the
floor and the deadline, plus the last recorded grade, and spawns nothing; the declared report path is
neither created, read nor deleted by planning. --run is the only door to execution. A
never-recorded grade reads as no grade recorded yet and is never reported as pass.
The command is registered once, CLI-reachable, and served nowhere
work:grade appears exactly once in the registry with a cli adapter and no launcher seam, is
reachable as aof work grade, and is a documented BOARD_DEFERRED member with its reason recorded
beside it – no /api/work/grade route exists, so a board page load launches no process. board-ui,
ui/ and src/bundle/commands/** are unedited.
One bounded, fully-captured spawn per --run
stdio: ["ignore", "pipe", "pipe"] – a runner that reads stdin gets EOF rather than hanging, and
both streams are captured, so a runner whose failures go to stderr (this repo’s own shape) cannot
read as an all-green report. The exit status is read before the report is: a non-zero exit is never
overridden by a green report. launched is 1 for a run that passes, fails or times out, and no retry
is attempted inside a single grade.
The deadline force-kills, and is resolved rather than chosen
timeout + killSignal: "SIGKILL" bound the child; a run that reaches it yields indeterminate /
runner-timeout reporting the deadline that bound it and the duration observed, with nothing from
that run left alive. The value comes from 69/ADR-001’s single home src/loop-bounds.mjs via
work.loop.startToCloseMs – 30 minutes by default, the declared value when one is set, the default
again when the declaration is not a number. No second home for the bound exists in src/**, and
work.dispatch.concurrency and work.autonomous.maxAttempts resolve exactly where they did.
Re-entrancy is refused structurally
Every spawn sets AOF_GRADE_RUNNING in the child’s environment – the only variable aof contributes
that the project did not declare – and a --run that finds it already set refuses with
runner-spawn-failed before any other check, launching nothing. The invoking process’s own
environment is unchanged, so a later --run from it is not refused. A rubric that invokes aof cannot
fork a grader tree however deep it is attempted.
The child inherits its environment, with the declaration overlaid
The ambient environment reaches the runner; the declared env overrides same-named ambient
variables; everything else stays visible. What is forbidden is aof inventing a variable the project
did not ask for, not the child having a PATH.
Five armed fitness functions
acd-grade-never-imports-the-suite (FF-5401), acd-grade-unconfigured-is-additive (FF-5404),
acd-grade-read-face-never-executes (FF-5405), acd-grade-bounded-single-spawn (FF-5406) and the
extension of acd-controls-never-execute (FF-5407) are registered and each has been observed failing
against planted defects – eleven across the five, with FF-5406 re-probed at accept on the accepted
bytes.
54/02 · Fitness in the gate
A declared five-row cost ladder
GATE_ORDER is a frozen five-row declaration – drive continue, gate work:validate,
gate work:doctor, gate work:grade, drive verify – in strictly increasing cost, and every gate
row names a command the registry resolves.
The fitness half of the gate, wired
invokeGateLadder invokes work:doctor after work:validate, so an item whose declared controls do
not resolve is stopped by a deterministic check before any model turn is spent.
Each rung short-circuits every rung after it
A work:validate that returns findings returns from the ladder without invoking the doctor, and the
ladder’s answer is its own envelope – { gate, findings }, naming the rung that answered – never one
rung’s result wearing another rung’s findings.
The doctor rung runs at the driven item’s own scope
It is invoked as { scope: ref }, exactly as work:validate already was, so an un-authored register
anywhere else in wiki/work cannot stop a loop that is not driving it.
An admitted code set derived, never restated
DOCTOR_GATE_CODES is Object.freeze(CONTROL_FINDING_CODES.filter(…)) over 66’s frozen array minus
its two verification-* members and control-runner-unchecked; no admitted code appears as a
literal, so neither verification code can enter the gate at any severity and a ninth control code
cannot silently join it.
Severity is taken, never re-derived
The gate admits severity === "error" and nothing else; the loop shell neither calls nor
re-implements severityFor, does not import the acceptance horizon, and reads loopReady nowhere in
the ladder.
Two armed fitness functions
acd-doctor-gate-scope-and-severity (FF-5410) is registered and has been observed failing against a
planted defect, and FF-5409’s GATE_ORDER clause on acd-loop-probe-contract has been observed
failing against two.
54/03 · Feedback rides the re-drive
The verdict decides the act
gradeRoute() is the single derivation from a grade to a loop act – pass proceeds, fail
re-drives, indeterminate halts, and rubric-unconfigured is the one named exception that proceeds
as today. The routing reads the record’s verdict; a fail carrying no failures re-drives and an
indeterminate carrying no codes halts, neither of which depends on the record having detail.
The grade rides the run it re-drove
brief.grade is written through the same transition seam that already writes brief.loop, so a
fail verdict’s record lands durably on the run record of the re-drive it caused, and the failures
reach the next session through 70/04’s existing pendingFixes → ctx.loopDrive.fix →
## REVIEW FINDINGS transport. No second transport exists.
The driven row carries the grade
drivenRow() emits the verdict, the code list and the observed case counts per drive, attributed by
run id rather than by position. LoopState’s ten top-level keys and actShape()’s whitelist are
byte-for-byte what they were.
A cap-exhausted halt reports what it could not close
Both cap-exhausted sites – the gate-decision halt and the pre-drive loop-cycle-cap halt – report
accumulatedRecord(), the union of the grades carried by the loop’s own runs keyed by loopRunId,
with the exhausting cycle’s grade carried inline because it has no successor run to ride.
The union survives a retried build attempt
accumulatedRecord() contributes one entry per distinct grade: a run whose retryOf lineage already
contributed the same gradedAt is skipped, so a cycle whose build attempt was retried is counted
once rather than twice.
An unconfigured repository is byte-identical
With no work.rubric declared, the loop’s LoopState, every report line, every driver input, the
driver’s spawn argv and every run record are indistinguishable from the pre-54 shell – measured by
neutralising the whole grade path and diffing full observable traces, not asserted.
FF-5409’s second clause is armed
acd-loop-probe-contract now drives a real loop to a grade-indeterminate halt and asserts the stop
and its producer before the key-set shape it guards, and has been observed failing against two
planted defects.
54/04 · Scenario traceability
A declared @executable → case join
src/work-doctor-rubric.mjs pairs an emitted case with an @executable scenario when the case’s name
CONTAINS the scenario’s name, over scenarios the one feature parser resolved – @manual and @uat
scenarios are never offered to the join, however their names read.
A miss is reported, never guessed
A case naming no scenario is case-unjoined; a scenario named by no case is scenario-unjoined; both
report at warn and move no verdict, and no module on the grade path carries a levenshtein, edit
distance, fuzzy, similarity or best-match fallback.
The honest no-op names the key that would activate it
With no work.rubric.report declared, the lane emits rubric-join-unchecked once per item that
declares at least one @executable scenario, naming work.rubric.report; an item declaring no such
scenario gets no notice at all, and no unjoined finding is ever emitted from a report nobody read.
A pure work:doctor lane that runs nothing
rubricTraceabilityGroup is a horizon-scoped CHECK_GROUPS member reading the last report as
snapshot text, exactly as the controls lane reads its runner texts; it spawns nothing, and it joined
the scanned work-doctor* family the day it was written.
The milestone-15 traceability gap filled outside the god-node
The join lives in a new lane leaf; validateWork and its 256 dependents are untouched,
src/work-doctor-controls.mjs is not edited, and src/feature-parse.mjs is read and not edited – it
remains the one feature parser.
A frozen three-code vocabulary of its own
RUBRIC_FINDING_CODES is frozen at case-unjoined, scenario-unjoined and rubric-join-unchecked,
and RUBRIC_REPORT_CONFIG_KEY is the single spelling of the key its message names.
One armed fitness function
acd-grade-subject-is-emitted (FF-5408) is registered and has been observed failing against two
planted defects – a prose classifier and a fuzzy fallback.
53 · The loop as a CLI artifact
aof work loop <ref|range> – the code-owned loop shell
The loop’s sequencing, phase dispatch, gate, bounded retry and stop conditions are code in
src/commands/loop.mjs over the pure engine src/work-loop.mjs; no prose document decides them.
- Never driven for real – every claim about the shell rests on fixtures and the injected spawn seam; it has not driven a milestone end to end under observation (F-12, waived).
- Foreground only – the launcher body owns the terminal for the whole run; nothing resumes it unattended.
The registered work:loop command is a read-only probe
work:loop’s registered run returns a ten-key LoopState and never spawns, mints or writes; the
executing form is the cli.launch body, which --json and dryRun resolve before reaching.
- The probe/launcher split is the machine face – a caller that reaches the registry gets the probe; only the CLI launcher path executes.
Three atomic phase drivers – work:drive-refine|continue|verify
Each spawns one session running that phase’s existing prompt through
src/agent-session-driver.mjs, watches the transcript to completion and gates; the three ACD phase
prompts are byte-unchanged at their pinned addresses.
- The prompts stay the agent node – the CLI owns the shell only; no phase logic moved into code.
The PTY session driver has one home
src/agent-session-driver.mjs holds the seventeen frozen names, re-exported by
src/mesh-worker-execution.mjs so all pre-existing importers bind the same values by identity; the
sink is measurably smaller and carries a shrink-only line ratchet.
- The re-export is the compatibility surface – dependents resolve through the sink, so a future extraction must keep following the chain rather than typing a second filename.
The ladder ships L1 and L2; L3 is locked
LOOP_LEVELS is the frozen pair ["L1","L2"] and LOCKED_LOOP_LEVELS.L3 refuses with a coded
refusal before any spawn or mint, naming milestone 55 as its unlock.
- L1 writes nothing – a full L1 drive reports one act per in-scope item through the report channel and moves no byte, adds no path and mints no run record.
Loop state rides the run record
A loop’s declaration is a seven-key brief.loop on the run record and round-trips unchanged through
work:run-status; --resume reads it back. There is no loop store and no command-side file write.
- Observability is the run store’s face – loop state reaches the board through the same surface as every other run, never a side channel.
Loop-Ready scores on aof work doctor, registry-optional
aof work doctor reports a Loop-Ready percentage that composes milestone 52’s structural checks when
a loop registry is declared and falls back to its own base checks when none is. This repo reports
56% (5/9) – clears L1, blocking on grounding, pairing, reference-ownership and actuator-arbitration.
- The scorer reaches the registry only through a deferred command invocation – it holds no
import of the loop-registry module and sits outside
CHECK_GROUPS. - A repo with no registry still scores – an absent registry removes five rows from the denominator rather than failing the check.
The loop registry ships in the bundle at .aof/loops/
Nine framework loop records install into a consumer repo’s .aof/loops/ as verbatim, framework-owned
bundle assets, each carrying the # aof-generated: stamp; work loops show and work loops validate
read them from workspace.aofDir, the sole production home.
- Consumer edits are preserved, not overwritten – an edited record produces a drift warning and
keeps its bytes;
--forcerestores the shipped ones. - The retired
wiki/work/loops/home is gone – nothing reads a work-stream registry path.
/aof:autonomous hands its range to the shell
src/bundle/commands/autonomous.md names aof work loop <range> --level L2 as its body and carries
none of the seven loop-shell tokens it owned before; --solo and --ship survive, re-anchored on the
shell’s report. The door keeps its id, file, namespace, argument hints and every caller.
- The prose loop is gone from the body, not the door – one authored source still renders to both the Claude command and the mapped Codex skill.
Thirteen architectural controls enforce the above, and each has been watched to fail
FF-5301…FF-5313 are landed, registered in the runner’s own labelled milestone-53 blocks, green, and each was red-probed at accept by mutating its own subject.
- Non-vacuity is measured, not assumed – every probe produced a failure message naming the invariant it broke.
52 · Loop registry & the loop graph
The loop registry
wiki/work/loops/ holds nine markdown records – seven loops and two actors (actor:operator,
actor:product-owner, loop:autonomous-cascade, loop:build-to-green,
loop:mesh-assignment-reclaim, loop:retrospective-memory-ingest, loop:review-fix-rereview,
loop:run-resilience, loop:verify-triage-accept) – each declaring its controlled variable, reference,
measurement, actuator, cadence, ceiling and owner in frontmatter.
The loop model and its loader
src/work-loops.mjs reads the registry into plain data with a frozen schema, a closed value grammar
(phrase, module:, command:, config:, prose:, the three sentinels, periodic: and event:
cadences) and sixteen loader finding codes; it resolves periodic:<n><unit> to milliseconds and
reports declared gaps as warn and schema violations as error.
The closed edge vocabulary
Five edge types – data-feed, target-setting, monitoring, veto, parameter-tuning – are declared
on the records themselves. depends is unchanged and remains the item-level edge; no loop record
carries one.
The five structural checks
src/work-loops-checks.mjs computes groundedness over strongly-connected components, optimizer pairing,
reference ownership, shared-actuator arbitration and timescale comparability as pure model-in/findings-out
functions. It imports nothing, parses no declared value, reads no clock, and emits eight check codes in a
frozen order.
work:loops show | graph | validate
Three registered commands with frozen --json envelopes. show prints the roster (--id filters),
validate runs the loader lane then the five checks and reports summary.checks with a per-check
{ran, findings}, and graph renders deterministic Mermaid – byte-identical across processes, with
loop, actor and endpoint carrying distinct glyphs. All three report an absent registry as present:
false and exit 0.
The day-one report
On the registry as it stands the milestone’s own machinery reports 0 errors and 40 warnings:
12 loop-field-prose-only, 7 loop-graph-ungrounded-component, 6 loop-owner-unknown,
5 loop-unowned-reference, 3 loop-unpaired-optimizer, 3 loop-shared-actuator-unarbitrated,
2 loop-graph-grounded-exogenous-only and 2 loop-ceiling-uncapped. Exactly two edges are declared in
the whole registry, both target-setting. The three loops marked optimizing: true –
build-to-green, review-fix-rereview, autonomous-cascade – are all unpaired, because no record
declares a monitoring edge at all.
The instruments
Nine fitness functions (FF-5201…FF-5209, 18 legs in test/arch/acd-loop-*.test.mjs) hold the structure –
that the registry is not an item type, the module import boundary, the thirteen frozen vocabularies, the
real registry parsing, check purity and determinism, timescale domain, command-surface route-only,
Mermaid byte-freezing and the finding envelope. Six behavioural suites (139 cases over
test/support/loop-registry-fixture.mjs) hold the behaviour, and
test/work-loops-coverage-ledger.test.mjs re-derives the feature-to-suite traceability on every run –
323 scenarios and 461 rows across 28 tables, each with a deciding assertion or an exclusion whose
pointer is looked up in the gate module rather than transcribed.
50 · Session launcher – start a session on a node, bound to a repo
The session-spawn wire kind
kind:"session-spawn" is a named directive on the mesh stream with its own frame builder, ack frame
and worker receive lane, carrying no lifecycle phase and no work item.
A second named write route on the fleet face
POST /api/mesh/session accepts {nodeId, workspaceId, itemRef?} behind same-origin +
application/json admission, mints a crypto.randomUUID() session id and answers it in the 200; the
fleet face’s write allowlist is exactly two named entries, and the two fitness functions that pin it
assert the count rather than a pattern.
Cross-process dispatch over the shipped relay
The fleet face in aof mesh ui reaches the directiveTargets registry owned by aof mesh serve
over the existing loopback relay bridge; session-spawn is a third named lane on that bridge beside
terminal-input and terminal-resume.
Worker-side PTY spawn and session registration
A worker receiving the directive opens a PTY in the chosen workspace – the operator’s default shell, not an assistant CLI – registers it through milestone 48’s session index, bridges its output onto the terminal frame producer, and tears it down on stop with no orphaned shell and no armed timer.
A coded outcome lane for a spawn the 200 could not answer for
GET /api/mesh/session-outcome?nodeId=&sessionId= answers {state, code, at} for every tuple and
never a 404; a dispatch the router cannot route, or one that throws mid-send, becomes a synthesised
refusal on that same lane rather than a success.
The new-session affordance on the terminals home
A New session ▾ trigger sits in the surface slot at every breakpoint and opens a bounded panel with
Node / Repo / Item (optional); the picker annotates every node the payload carries and filters none,
and the route – not the picker – issues refusals.
An operator-facing failure vocabulary owned by the browser
Fourteen refusal codes render as operator sentences from the UI’s own REFUSAL_MAP, each naming the
machine the fault is about, with a stated fallback for a code the map does not know.
relaying as the session record’s seventh key
The session projection emits relaying on every live session as a strict record.relaying === true,
so a record written before the key existed projects false rather than absent.
49 · The terminals home
/ is the terminals home
The application’s root route is a routed surface inside the shell’s crash containment that renders the
live terminals of the fleet; ui/src/app/Landing.tsx no longer exists, SHELL_RENDERED_ROUTES is
["not-found"], and the four legacy query addresses (?mode=fleet|board|assets|wat) rewrite to their
paths by replaceState, preserving every other parameter and the fragment byte-for-byte.
The grid of live panes
Every addressable session in the mesh renders as a tile carrying node, repo, work item where one
exists, agent state and a live terminal; tiles are ordered (nodeId, repo, sessionId) in the browser,
and each subscribed tile constructs a real ws://…/ws/terminal-view?nodeId=…&sessionId=… socket
through the product’s own mount chain.
The pane is a fourth host of the one terminal control
HOST_GRID_PANE = "grid-pane" declares all eight affordances – including each absence with its
reason – and ui/src/home/session-mount.mjs is the single author of a grid pane’s posture, which is
derived from the feed axis and fails closed to a labelled read-only.
Typeable panes on the fleet origin
A grid pane whose session can accept input is POSTURE_INTERACTIVE and types through m42’s existing
tuple-bound seam; entry stays content-blind and byte-bounded by MAX_TERMINAL_INPUT_BYTES, delivery
stays session-exact through liveSessionInputs, and a pane can only type into the session its own
socket names.
Invariant 4, amended rather than deleted
acd-fleet-terminal-input-constrained part 1 is now a surface→posture-home table covering all three
surfaces with per-surface floors and discovery-by-sweep; parts 2 and 3 are untouched, every prior
assertion survives verbatim, three are added, and no exemption list of any shape exists.
The feed axis – a pane says whether anything will ever arrive on it
Pane truth is composed from a feed axis derived from wire fields the fleet already polls, beside m46’s
subscription axis, with the precedence fixed in one place; a session that is addressable but has no
producer renders idle with a top-left no live output line and opens no socket.
A bounded live-socket ceiling
MAX_LIVE_PANES = 16 is arbitrated by one pure function over the whole row set, taking the
currently-subscribed set as an argument; priority allocates free slots and never evicts, a pane beyond
the cap is unsubscribed rather than refused, and retained tiles are counted against the cap rather than
hidden from it.
Layout as a filter, persisted per operator
A per-origin browser preference behind one pure module that takes storage as an argument filters the live index; it is never a source of rows, adds no route, no mutation and nothing on the wire.
needs-input reaches the wire
projectAssignment appends code under sessionId’s own guard and WorkAssignment declares
code?: string, so the agent state the worker already produced survives the hop to the fleet face –
assignment-scoped, with no new reader and no rendering leaked into ui/src/fleet/api.ts.
One repo, said once
The current-work line groups on the raw repo string and prints each distinct repo once with a count –
working · aof ×2, demo (session) – in the JS formatter and the Rust view-model alike, with a raw
U+00D7 sign in both shipped sources and a captured fixture that makes the cross-language gate red when
either half diverges.
One live region for the whole grid
The terminals home announces state through a single polite region whose role follows the host’s own
hostAnnouncesState declaration; the gate counts implicit role="status" regions beside explicit
aria-live ones.
The pulse honours reduced motion
One unlayered @media (prefers-reduced-motion: reduce) block silences .animate-pulse beside the
.aof-pending the stylesheet already named, so all twelve animate-pulse sites across ui/ stop
animating under the preference while every state stays distinguishable by word and colour.
Claude sessions reach the index
The distributed bundle wires the three session-lifecycle events for Claude, so a workspace that runs
aof work update records its sessions into the index instead of leaving the terminals home empty
everywhere but this repo.
The harness can drive a grid
The test harness mounts N controls from a caller-supplied entry with a per-pane driver, declares the
shell on the bundle the control reads, moves real focus and dispatches real keystrokes; an entry must
bundle the real control unless the caller declares terminals: false, and declaring it withholds the
pane driver entirely.
48 · Routable session identity
The session id of record
A session record is keyed by (nodeId, workspaceId, assistant, sessionId) and carries the assistant’s own
id in a frozen ordered seven; the id is read through the ordered ladder --session → payload.session_id
→ env.CLAUDE_SESSION_ID and is null when no channel supplies one – never generated, never normalised,
stored byte-identical.
One live session, one record
Two concurrent sessions on the same (node, workspace, assistant) are two files on disk, and ending one
leaves the other’s record and its presence entry intact.
The orphan reaper
A TTL-expired session record is removed from disk by its owning node at the startSession/pingSession
write seam, under the same isStale predicate the rest of the mesh uses; a reap fault degrades loudly and
does not fail the start or ping.
The session on the presence wire
readLiveSessions projects every live session to the frozen ordered six
{ sessionId, workspaceId, repo, assistant, lastPingAt, workspaceHasRun }, ui/src/fleet/api.ts’s
PresenceSession declares exactly those six, and the control stream remains an entry-level pass-through
with no per-field whitelist.
A complete wire: run subsumption moved to the formatter
The presence producer no longer drops a session whose workspace has a run – every live session reaches the
wire stamped with the fact workspaceHasRun, and fleetCurrentWorkLines applies the display rule at the
render, strictly on workspaceHasRun === true. What the fleet draws is byte-identical to what it drew
before, on both the JS and Rust surfaces.
The fleet-side session index
buildSessionIndex in global-mesh-query.mjs answers “what live sessions exist across the mesh” as an
(nodeId, sessionId) lookup plus a deterministically ordered array – a pure, rebuildable projection that
performs no I/O, reads no clock, stores nothing, and returns null on every miss. It is served as the
additive top-level sessions key on the global status payload, and a duplicate (nodeId, sessionId) is
resolved by a total order (latest lastPingAt, then workspaceId/repo/assistant), never collapsed by
filesystem arrival order.
Attribution without a second authority
workItem is derived onto a session at the index alone, joined on (target_node_id, session_id); no
session record carries a work-item field and no session write path reads global_assignments. A session
with no assignment is first-class: workItem: null, present and never dropped.
A session index that is live on the real fleet
A deployed node publishes the six-key entry with a real routable id, and aof mesh status --json on the
control node returns it – measured on a three-node fleet, not only in fixtures.
47 · /fleet with a repo filter
The repo filter
/fleet narrows to one workspace via ?repo=<workspaceId>, applied at one seam above the region
fan-out, and every collection on the GlobalMeshStatus wire is either narrowed there or declared
machine-wide with its reason.
Filter and scope composing by intersection
?scope= and ?repo= are two independent questions answered together: scope narrows at the server,
repo narrows the result at the client, and the composition is an intersection taken per collection.
The partial intersection as a third answer
A filter whose work is out of scope but whose machines are not renders populated – work regions at
0 of N, the node region naming the machines that carry the repo, and a notice stating the cause in
words.
Five empty states, told apart by their cause
empty, unknown-filter, out-of-scope, filtered-empty and empty-mesh are separate renders selected by
one predicate; the out-of-scope state exists because “zero workspace rows survived” cannot otherwise
distinguish the scope excluded it from the mesh does not have it.
A filter that survives the address bar
The narrowing is a deep link that survives refresh, poll and a second browser profile, and one
popstate listener re-derives both narrowings from the address, so Back and Forward re-narrow the
page.
The filter control and its banner
A disclosure trigger in the shell’s surface slot occupying a fixed 150px slot at sm and above, and a
“filtered by” chip row hoisted above the page’s state ternary so it renders in the populated and empty
states alike.
Region 5’s relief under a filter
A filtered milestone card drops the workspace-name column every card would otherwise repeat, and the attention cluster’s yield order is a discrete ladder keyed to cluster arity: the drill-in and the secondary token surrender their words whole to pinned glyphs, and the assignment target is the last element to yield.
The board drill-in that opens a board
The drill-in resolves through GET /api/mesh/board-url, which refuses a workspace whose checkout is
gone rather than answering 200 over an empty board; the local-shape boards branch unreachable since
m34 is deleted rather than repaired.
A slot that degrades by truncating rather than by overprinting
The shell’s surface-bar slot is flex-nowrap with shrinkable occupants, so a row that cannot fit
yields its one designated element instead of wrapping into the chrome above and the content below.
45 · UI app shell & path routing
Real URL paths
/, /fleet, /board and /config are the four addresses of the application; the render-root
?mode= ternary no longer exists. One frozen table in ui/src/app/routes.mjs is the only thing that
maps an address to a surface, and acd-ui-single-route-table is the only place that fact lives.
A shared application shell
Every surface mounts inside <Shell>, which renders five named regions (notice rail, top bar, surface
bar, content, overlay), publishes its measured height as --aof-shell-chrome-height, and owns one
z-ladder, one skip link, one banner and one <main>. Chrome measures 48px at 1280 and exactly 88px
at 768 and below; at the desktop app’s 760×520 window the content region is exactly 432px.
Navigation between the surfaces
A four-item nav, driven by the route table, is present on every route and on every origin; the active
surface is identifiable from its rule and weight with colour removed. At 390 the nav collapses to a
disclosure labelled with the active surface – or Surfaces ▾ where no route matched.
Chrome that stays put while a surface scrolls
The chrome block is pinned, so the top bar and the navigation remain on screen through a page scroll
on content:page routes. DESIGN GAP D1’s page-level horizontal clamp is overflow-x: clip on html
and body – it clamps without establishing a scrollport, so the pin resolves against the document.
A history fallback on both static origins
A client-side path that is deep-linked or refreshed renders instead of 404ing, on the board and
config-editor origins as well as the fleet’s. src/static-serve.mjs is the single home of the
traversal guard, the MIME table and the fallback predicate; the guard runs before the predicate
unconditionally, so a refused path is a 404 and never the shell.
Paths as the only advertised address
Every producer that hands an operator a URL – the board, fleet and config-editor launchers (announce
line and --json probe), GET /api/mesh/board-url, the desktop tray’s compiled constant, and the
in-app cross-links – emits an ADR-002 path. No surface URL in ui/, src/ or app/desktop/ contains
mode=, and acd-no-surface-mode-url-literal sweeps a closed route-path vocabulary over 295 files so
a fifth producer cannot appear unseen.
Every legacy address still opens what it always opened
A ?mode= URL is translated once, client-side, as a replaceState onto its canonical path, with the
query and fragment carried through unaltered. Back returns to wherever the operator was before, never
to the legacy form. The bare / on the board origin now renders the shell landing; it previously
rendered the config editor, and it is the one URL whose meaning changed.
A surface that fails takes down only itself
A surface that throws while rendering is contained at the shell boundary and rendered as the shell’s
failed state, with the chrome, the navigation and a retry intact. A surface reached on an origin
that cannot serve its API degrades through its own error state: the config editor states the failure,
names the command that opens it on its own origin, and offers a retry.
43 · Mesh artifact authority
The exclusive item lock
An item whose execution scope is covered by an active assignment is not assignable again, and the check
sits in front of transitionRunStart – the one seam all four mint doors route through – so work next
and run-start honour it as well as the continue/refine/verify doors.
The cache as the authority for item state
work_items is no longer wholesale-deleted and rebuilt on every propagation tick; authority is by
authorship and door, never by timestamp, and syncedAt is provenance for display and staleness
only. A worker-authored row survives the control node’s republish tick indefinitely.
Write-triggered artifact sync
A PostToolUse hook on Write|Edit|NotebookEdit enqueues the exact path the agent wrote, and the
worker daemon batches the wire send; the periodic re-scan remains as the reconciliation backstop for
files a Bash command touched. The manifest carries ARCHITECTURE.md and tasks/*.feature, which the
previous four-name whitelist excluded.
Staleness that marks rather than evicts
Cached rows carry reportedBy and syncedAt; past the configured window a row renders a stale badge
and an operator-initiated Resync that asks the owning node to push a fresh copy. Nothing is ever
evicted – after settle the cache is one of only two copies of that work, the other being the pushed
branch.
Gate-time propagation
A dispatch advances a continuing item’s branch to the assignment’s pinned base at the worker’s reuse
door, by merge, before the agent starts. An item whose line exists only on the remote is adopted as a
local head and takes that same door rather than being forked. A conflicting advance aborts the merge,
refuses with assignment-gate-propagation-conflict, and leaves the branch at its prior tip with no
half-merged tree; the outcome and both commits ride the node log channel.
The cache as the read surface
list, find, next, resolve (and the doc / tasks / feedback commands that sit on it),
run-start, run-status, work-doctor, the notion sync/associate pair, promote-gap-to-chore,
mesh-heartbeat and memory/local-indexing answer cache-first with a disk fallback, and say which
side answered. A worker reading its own checkout still reads that checkout – the boundary is pinned by
positive assertion on the protected function itself.
39 · Delivery Memory
The OUTCOME.md record doc
The aof bundle ships a milestone OUTCOME.md template (## Delivered / ## Assumptions / ## Gaps) at both src/bundle/templates/milestone/OUTCOME.md and .aof/templates/work/milestone/OUTCOME.md, LF-pinned and carrying the leading bundle marker; aof:verify instantiates and authors it at Accept, and recordDoc never resolves to it – it is an additional artifact, never an item’s identity record doc.
Delivery is recallable over the ADRs
parseOutcome turns each ## Delivered ### heading into a capability record and each ## Gaps ### heading into a gap record through the shared buildRecords, so delivery records reach both the local and graphify backends from one edit; a capability-intent recall (“what provides X / is X built”) returns the capability record ahead of verbose keyword-dense ADRs via a bounded TYPE_BOOST_CAPABILITY (0.1, strictly below TITLE_BOOST_PER_TERM 0.6) plus an --area delivery hard pre-filter.
Gaps are schedulable debt
A gap record carries an open | discharged lifecycle in the reused MemoryRecord.status field and its discharge condition in searchable text; aof work memory recall --status open returns only open debt (status is now a scope flag in work-memory.mjs and a scope field in local-retrieval.mjs), and aof work promote-gap (backed by the new aof work insert-chore seam) scaffolds a top-level chore whose ## Definition of Done is seeded from an open gap’s discharge condition and which back-references the originating gap.
The dangling-declaration fitness function
test/arch/acd-outcome-declared-field-has-producer.test.mjs computes the MEMORY_RECORD_FIELDS entries with zero producer write-site among the record parsers and fails red on any non-empty result, catching a declared record-format field with no writer (the warnings_delivered shape) whether or not an OUTCOME.md declared it; a planted producerless field trips the same detector, so the check is proven able to fail.
38 · Cross-machine worker execution & session presence
Assistant-session presence
A node with a live coding-assistant session reads working · <repo> (session) in the fleet, fed by an assistant-agnostic aof session start|ping|end CLI writing one record per (node, workspace, assistant) tuple.
- Hooks are wired per workspace – the record exists only where
.claude/settings.jsoncalls thesessionverbs; an editor that reports nothing staysidle. - Identity is derived from the hook’s
cwd– the payload carries no aof concepts, so the workspace and repo are resolved from the directory the assistant is open on. - The installed binary is the one that must carry the verb – a hook wired against an older installed build refuses with
session-arg-missing-workspaceand writes no record.
TTL liveness (never a stuck working)
A session that dies without an end self-expires after 120 seconds (DEFAULT_SESSION_TTL_SECONDS) through the same isStale predicate presence staleness uses, leaving the node idle with the record still on disk.
- Liveness is a ping cadence, not an event pair – a session pinged less often than the TTL reads as idle even while the assistant is open.
Cross-workspace presence aggregation
Presence is assembled over every workspace registered to the node in the machine-wide store (global_node_workspaces joined to global_workspace_descriptors), so a node working two repos shows both and a daemon launched from a non-repo directory still reports every repo’s work.
- Each row’s work dir resolves against its own
project_root– an absolute descriptor path, never the reader’sprocess.cwd(); a workspace whose resolved dir is absent is skipped loudly, not silently. - A run subsumes only its own workspace’s session –
activeRunscarries no workspace attribution, so subsumption happens in the publisher where the attribution exists, and the wire record is already reconciled.
Presence carried verbatim across the fabric (ingestion)
A remote node’s five-key presence record survives ingestion on the control node – applyPresenceFrame carries sessions through unchanged in ADR-001 key order, with the worker as the single TTL-filtering authority.
- The sender is the filtering authority – the control re-derives nothing and holds no session records of its own; a dead worker’s whole record is gated stale.
Worker repo checkout on demand
A worker assigned work for a repo it does not have clones it from config.mesh.repo.cloneUrl into the scoped meshCheckoutPath(workspaceId), registers the workspace, and falls through to the unchanged worktree-and-run flow – no manual pre-setup on that machine.
- The clone URL is fleet-shared and committed – it is read from the assigned workspace’s own config, never from the worker’s request frame.
- The checkout root is a single seam – a resolved path that escapes the mesh-checkouts root is refused rather than traversed.
- Git’s ambient credential helper is disabled for the clone –
-c credential.helper=plusGIT_TERMINAL_PROMPT=0, without which a configured OS helper wins overGIT_ASKPASSand persists a token to the keychain.
Automatic clone-credential mint
The control node mints each clone credential on demand from a configured provider – a GitHub App installation token, short-lived, contents:read, scoped to exactly the assigned repo – and delivers it to the worker over the already-open stream at the moment it hits a clone miss.
- The App private key never leaves the control node – only the minted token crosses the mesh, and neither is written to logs.
- The mint is scoped to the assignment’s own workspace – never the requester’s claimed one – and refuses for any assignment not in an active state.
- The credential is pull-only – nothing persists it to
.git/config, process env, or disk on the worker.
Per-org credential scoping
Each org’s clone credential is minted by that org’s own GitHub App – appId, installationId and private key resolved per assigned workspace, with keys coexisting as distinct files under the code-enforced <meshRoot>/credentials/github-app-<appId>.pem.
- Every org’s config names its own App identity – a workspace with no well-formed App identity produces a loud coded mint failure, not a fallback to another org’s App.
UI-driven assignment
A milestone or story is assigned to a worker node from the fleet/board UI through POST /api/mesh/assign – the read-only fleet face’s single mutation route – with no CLI touched, and the affordance acknowledges the call with a held Sent state plus one silent re-load that lands the assignment chip.
- The wire carries
{ref, nodeId, workspaceId}, all required – the item’s own workspace is resolved through the sanctioned status→projectRoot→loadWorkspace seam, and a missing id is a coded refusal rather than a fallback to the daemon’s own project dir. - The target workspace must be local to the control node – a non-local or unknown workspace is refused (
workspace-not-local/workspace-not-found). - The acknowledgment reports the CALL, not the outcome – a dispatch can be
Sentand then fail; the assignment’s own state arrives on the poll. - The assignment carries a validated lifecycle phase – refine, continue or verify, chosen at dispatch.
Terminal-driven worker execution
A worker runs assigned work as interactive claude in a PTY on its own subscription, driven by whole directive commands the control node types into that one session – claude -p is gone from the worker’s execution path, so the driver can be asked a question and the run bills to the subscription.
claudemust be authenticated in a real login session on the worker – a daemon started over SSH has no session and burns the run.- The worker clears the folder-trust dialog and runs
--permission-mode auto– a directive typed before the TUI is ready, or submitted with a line-feed instead of a carriage-return, is never executed. - Completion is detected from the interactive session, not from process exit – the run’s terminal state comes from the session’s own signal.
Worker terminal streaming into the fleet
The worker’s live PTY bytes ride the frozen relay envelope as an opaque terminal-frame routed by (nodeId, sessionId), and the control node’s fleet renders them in a read-only mirror at the /ws/terminal-view carve-out.
- Each leg rides the bind it fits – worker→control over the fabric, control→browser over loopback; the relay broker binds loopback only and is unreachable off-host.
- The mirror is read-only in fact – no keystroke path back, and neither bridge nor mirror writes a durable record.
- The join key is reported while the run is live – the session id reaches the fleet before the run reaches a terminal state, or there is nothing to watch.
Durable worker pushback
Work done on a worker commits to a real named branch and is pushed home before the assignment’s worktree is force-removed, so the output survives the done transition and arrives as a reviewable branch.
- The write credential is a second, distinct mint –
contents:write, separate from the read-scoped clone token. aof mesh recover-pushrecovers the stalled case – a stranded worktree on a terminal or stalled assignment is pushed home under control-node direction.
Worker-verified memory syncback
Knowledge verified on a worker reaches the control node’s memory over git – the pushed markdown is pulled and re-ingested with aof work memory ingest; no index or knowledge payload crosses the mesh.
- The graphify index is a local cache – derived output stays gitignored and is rebuilt on each node from the committed markdown.