aof Agent-Centric Delivery – a local CLI and agent team that refine, build, review and verify work from Markdown records in the repository.

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

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 routeFindingscreates 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.mjssrc/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-warningaof 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 → readRunsprojectExecution – 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.

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 13ceil(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/failedneeds-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: nullnot 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-6101FF-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-5901FF-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 item57/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 pendingFixesctx.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.

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.

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 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 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.

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.

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 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.

/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.

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.

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: truebuild-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 --sessionpayload.session_idenv.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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.