Delivered
What aof provides today, stated per accepted item from that item’s own outcome record – 172 items, 1042 capabilities. Newest first; a milestone precedes its stories. Each line is product state, not intent.
- 129/01 · The mode and the engine decide
- 129/02 · The drive is a child
- 129/03 · The lane commits and merges home
- 128 · work memory joins the route table
- 127/01 · One enumerator, three roots
- 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
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.
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/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.
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.