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 – 389 items, 1728 capabilities. Newest first; a milestone precedes its stories. Each line is product state, not intent.

190 · A tree is tested once per change, and every sign-off door reads that run’s record instead of running it again

A sign-off is evidenced by a recorded run, never a repeat of one

Every door that asks for tests gets an answer the command recorded against the tree’s content, and a repeat on unchanged content launches nothing: the build terminator, the review lanes, aof:review, aof:verify and a UAT sweep. The stories hold each capability whole: m190/00 (the record), m190/02 (the gate’s record and resume) and m190/03 (the doors’ prose).

A milestone closes on one whole-tree gate run, which may run on a hosted runner

190 itself closed on one aof work regression-gate 190 run: green, sharded, in 15.7 min on a clean ubuntu-latest checkout through gate.yml. Its row was committed home unchanged. A stopped gate resumes rather than restarting (m190/02).

Narrowing is positive and the project declares what it knows

--scope impacted narrows only on what it knows (m190/01). An existing project is told what its work.test declaration leaves out, and /aof:map-tests declares it and proves it by replay (m190/04). This repository declares its roots, suffixes, exclusions and resume templates.

190/00 · A repeat run is answered from its record

Every aof test run leaves one record keyed to its content

aof test takes a content key before it launches (the tree including uncommitted changes, wiki/work included) and files one record under <git common dir>/aof/test-records/ holding the key, scope, selection, result and what widened. packages/work/src/testing/record.mjs is its only writer, and the agent supplies no field of it (FF-19001, FF-19004).

A repeat on the same content is answered from its record

A run whose key and selection a stored record covers is answered with answered from the run of <instant> (tree <id>) and launches nothing. A red record answers only the identical selection, and --fresh forces a launch.

Callers asking together make one run

A caller whose selection an in-flight run covers waits on that run’s lock instead of launching its own. The lock and the record are placed by write-then-link, so a reader never sees half of one.

No door decides from a test record

No status, doctor, loop, audit or bundle door imports the record store (FF-19002). The record answers aof test, and the regression gate’s own row still decides the accept door.

190/01 · impacted narrows on what it knows

Package-owned suites are suites, and only runnable ones are selected

work.test.roots and suiteSuffixes admit the package and app test/ directories, and the selector takes a suite only when it is also a member of the whole suite the runner runs. Every registered suite is selectable (FF-19005).

Dependents are followed to the end

A changed module selects every suite that reaches it transitively through the code graph, not only its direct importers.

A new file narrows only when its place is known

A new suite selects itself (suite-itself), a new module imported by a changed file inherits its importer’s suites (imported-by-changed), and a declared path not yet written contributes nothing (not-yet-written). Any other file the graph does not know widens the run to every suite.

Documents narrow only through a declared coupling

A changed document or data file narrows only through work.test.couplings. This repo declares none, so every changed document still widens.

The narrowing reasons are a closed set

Every selected suite carries one of the five frozen reasons of ADR-003 §5, and a widened result carries none (FF-19003). A narrowed selection of nothing launches nothing.

190/02 · A gate that did not finish resumes

A stopped gate resumes on the same content

aof work regression-gate <NN> --resume re-runs only the units a not-finished run left: its killed units, its unstarted units and the whole of any killed packed unit. It skips finished lanes, and appends one row for the whole, reading resumed from <instant>: n of m units re-run.

A resume refuses what is not a resume

A resume is refused with regression-gate-resume-tree-changed when the content key differs from the stopped run’s, and with regression-gate-nothing-to-resume when the newest row is not not-finished. A gate that declares no reportArgs/filesArgs refuses with regression-gate-resume-undeclared.

The gate tells its program what to do through declared templates

work.test.gate carries reportArgs ({report}), filesArgs ({files}) and noLanesArgs beside jobsArgs and budgetArgs. scripts/test-sharded.mjs plans only the files a --files list names, and reports units, lanes, notFinished and notStarted.

Every gate run is also a test record

The gate writes an ADR-001 record through 190/00’s writer, keyed after its row is appended, with its sharded report attached. A later aof test --scope all on that content is answered from it (measured live: launched: false).

190/03 · The doors stop prescribing repeat runs

Each door asks aof test once and reads an answer from the record as evidence

The aof:continue build terminator and review lanes, aof:review’s test step and aof:verify now ask aof test at their scope, rather than telling each party to run the suite again. Each says that an answer from the record is the command’s own evidence. Reviewers judge the diff and do not re-run.

A UAT session sweeps once

The aof:verify UAT path and the UAT command and templates run one aof test --scope all for the whole session, answered from the newest gate run’s record when the content has not changed.

A stopped gate’s repair is the resume

verify.md names aof work regression-gate <NN> --resume as a not-finished row’s repair, and re-running or speeding up the gate as the repair when the content has changed. Never a code repair.

The prose names no tiers and one deliberate repeat

No command names a unit, integration or end-to-end tier (ADR-004). --fresh is named once, in <build_terminator>, for a suspected flake.

Every runtime render carries it

The Claude, Codex, OpenCode and .agents renders and .aof/templates/ match the bundle (aof work update: 0 drift).

190/04 · An existing project is told what its test declaration leaves out, and one skill declares it

aof project doctor names what a test declaration leaves out

A test-layout family checks a config’s fit to its tree: test-layout-suites, test-layout-couplings and test-layout-resume. Each is ok or a warning naming /aof:map-tests, and none is ever an error. An empty whole suite is never ok.

aof work update prints the same warnings

After its Updated ACD: line, work:update prints one line per test-layout-* warning, from the same function as the doctor, and prints nothing when the family is all ok.

A commit’s selection is replayed without running anything

aof test --scope impacted --commit <rev> --plan selects from the files that commit changed and stops. It launches nothing, writes no record and is answered from none. --commit without --plan is refused.

One answer to “what is a suite”

A suite is a file under a declared root that ends in a declared suffix and that work.test.exclude ([{ paths, why }]) does not name. select.mjs decides it, and the doctor, the selector and aof test’s whole suite all ask it. A declared suffix list reshapes the whole suite. With none declared, the whole suite keeps its pre-190 pair.

/aof:map-tests declares the layout and proves it by replay

The bundle command does the following, writing work.test.* and nothing else; --dry-run writes nothing:

This repo’s declaration accounts for its native tests

work.test.exclude names the 64 native node:test files under packages/*/test and apps/*/test, with the reason, so test-layout-suites and test-layout-resume are ok here and the whole suite stays at 1,273.

186 · Core names no integration package outside the registry

The registry is the only core importer of any integration package

packages/core/src/integrations.mjs is the only module under packages/core/src that imports an @aof/integration-* package. FF-16901 (acd-integration-registry-one-home) reports any other importer, Notion included, with no named exception.

185 · Notion’s effects, routing and store metadata reach core through the contribution

Integration reactors come from the registry

application/bindings/effects/table.mjs builds every registered integration’s effects-journal reactors from its contribution’s effects member. Every integration’s local reactors run before mesh projection and every integration reactor after publication, so Notion’s sidecar remap and board sync keep their cascade positions.

Integration routing comes from the registry

application/bindings/integrations/routing.mjs merges each contribution’s routing({ readRouting }) members over the one .aof/integrations.json reader, and application.work.integrations.routing keeps its keys (resolveNotionRouting, isPageId, classifyParent, asBoardsRegistry, RoutingError).

Integration store classification comes from the registry

effects/stores.mjs merges each contribution’s storeMetadata tables and file stores into the one store classification, so notion-sidecar stays a fact written only by the Notion mapping module.

The contribution’s optional members are one recorded shape

169/ADR-002’s amendment records the contribution as { id, name, assets?, services?, commands, effects?, routing?, storeMetadata? }, read generically by core. Hyperagent carries none of the optional members added for Notion.

184 · Notion’s verbs reach core through one contribution

Notion registered in the one integration registry

packages/core/src/integrations.mjs registers @aof/integration-notion/contribution before Hyperagent, and aof work integrations notion sync-work / associate reach the command registry through integrationContributions(), in the same registration order as before.

The Notion package composes itself

createNotionServices in packages/integration-notion/src/services.mjs builds the sidecar mapping, the apply layer, the ntn CLI seam and the sync body from collaborators core injects. No core module under application/bindings/notion/ or bindings/commands/notion-* exists.

One core binding builds every integration’s services

application/bindings/integrations/services.mjs builds each registered contribution’s services once per application. It hands the same collaborators to every integration’s verbs, and application.integrations.notion exposes the built mapping, sync, CLI and sync-work services under its existing keys.

183 · Global issue log

Local issue capture

AOF provides issues log, issues list and issues remove from any directory, storing timestamped issue records with origin and optional details in the configured global AOF home.

182 · Renumber work items

Explicit renumbering

A live numbered item can move to a vacant number through aof work renumber, with a read-only preview, unchanged lifecycle status and no sibling shifts.

Reference and memory continuity

Current work-document references and linked stream stores receive the new identity; the configured memory corpus is rebuilt. Dated migration history lets recall accounting join original observations to moved lessons while allowing later reuse of a number.

Observable recovery

Held items and invalid destinations are refused before mutation. Ordinary document-write failures roll back, and a post-move memory failure returns the successful move with a nonzero exit and a recovery command.

181 · Unattended loops survive declines

Procedure completion independent of acceptance

The shared Codex and Claude phase contract treats a recorded negative verdict with routed findings as procedure completion, while retaining genuine procedure failures.

Durable developer handback

Both shipped verify procedures return a declined story from in-review to in-progress through the status command, and the loop selects continue while preserving the review-gate route for genuinely in-review stories.

Rubric command preflight

Doctor reports unusable declared rubric commands and loop launch rejects them before sessions start, using grade’s existing command predicate without changing runner-spawn-failed grading or shell behavior.

180 · One memory node in the config

Every memory setting has one home

Backend, corpora, the code graph’s sharing and each engine’s settings live under the one memory node in the schema, the reader, the update, init and the config editor alike (m180/01/the-one-memory-node-in-the-schema, m180/01/the-one-reader, m180/02/the-update-migrates-the-old-shape, m180/03/the-memory-section), and no surface writes the old spelling (m180/02/init-writes-only-the-new-shape-and-selects-local, m180/03/the-editor-never-writes-a-legacy-key).

A second engine is one block and one enum value

A later engine’s settings are one block under memory.backends, named by a value of the backend enum the schema admits, so adding one creates no root key and needs no second migration.

Egress is a reported classification, not a key

No key under memory configures egress. aof memory status and aof graph build report it, and a new project’s local default sends nothing to a hosted model.

180/01 · The memory node holds backend, corpora, graph and per-engine settings, and one module reads it in either spelling

The one memory node in the schema

$defs/memory in schemas/aof.schema.json declares backend, corpora (items enum equal to KNOWLEDGE_CORPORA, with work, imports and code required), graph (the code corpus’s share and remote, with the never-shared sentence), and backends.graphify.extractionBackend (enum equal to GRAPHIFY_BACKENDS), and no egress property at any depth (FF-18002).

The one reader

readMemoryConfig(config) in packages/knowledge/src/memory-config.mjs is the only module that reads corpora, sources, graphify, backends or graph off the memory node, or graph off the root config (FF-18001). It answers the new spelling or the old, lists each old key with its replacement under legacy, and refuses a setting spelled both ways with memory-config-conflict.

Corpora absent means today’s set

An absent memory.corpora serves work, imports and code, and operator is the only corpus a project can add. A list that omits an always-served corpus is refused with invalid-memory-corpora.

The faces print the new paths

aof memory status prints a corpora: line and one legacy: line per old key, config-inspect reports the memory node under its new paths, and aof project doctor shows one memory-config-legacy warning row whenever an old key is present.

180/02 · aof work update rewrites an old-shape memory config to the new node, and aof work init writes only the new shape

The update migrates the old shape

aof work update rewrites memory.sources, memory.graphify and a root graph into memory.corpora, memory.backends.graphify and memory.graph, reports one memory: line per move, and leaves every other key of the project config unchanged. A setting spelled both ways is left exactly as written, for the operator to choose.

Init writes only the new shape and selects local

aof work init-config writes a memory node in the new shape with memory.backend: "local", and the /aof:init command and its renders say that local is the default a new project gets and that it sends nothing to a hosted model.

180/03 · The config editor has a Memory section that shows the node in the new shape and saves only the new shape

The Memory section

The config editor’s project scope has a fifth section, Memory, that shows the memory node already rewritten to the new shape, lists each old key and its replacement in a notice ending Saving writes the new shape., and keeps its Saved Memory message after the save’s refresh.

The editor never writes a legacy key

A saved Memory node that carries an old spelling is refused with memory-config-legacy-write, and a save that is accepted drops the superseded root graph through replaceMemoryNode.

179 · One knowledge engine behind both memory faces

One engine seam behind both memory faces

aof memory (with its aof work memory alias), aof graph build, aof graph impact and the three code-graph readers reach the work records and the code graph only through the one knowledge-engine port (m179/01/the-knowledge-engine-port), so the code graph has one reader (m179/04/the-code-graph-has-one-reader) and every face answers as it did before the port (m179/02/graph-impact-is-relations-paths, m179/03/aof-memory-dispatches-through-the-port).

A second engine plugs in as one declaring module

A later engine joins aof by exporting corpora and the methods it declares (m179/01/the-capability-declaration), and a corpus it does not serve is reported as corpus-unserved rather than answered by whichever backend is present (m179/01/the-corpus-unserved-miss).

179/01 · The knowledge-engine port answers four verbs and routes each by a backend’s declared corpora

The knowledge-engine port

createKnowledgeEngine({ backends }) in packages/knowledge/src/engine.mjs answers exactly ingest, recall, relations and status, and routes each (corpus, verb) pair to the one backend that declared it, calling a record backend’s reindex as its ingest.

The capability declaration

Every backend composed into the port exports corpora, a map from a corpus in KNOWLEDGE_CORPORA (work, code, operator, imports) to verbs in ENGINE_VERBS, and construction throws a TypeError naming the backend and entry for a value outside the vocabulary, a declared verb with no method, or a pair two backends both claim.

The corpus-unserved miss

A verb asked of a corpus no backend declares answers { served: false, code: "corpus-unserved", corpus, verb, backends } and calls no backend.

179/02 · graphify serves the code corpus behind the port, and aof graph build and impact answer through it unchanged

graphify serves the code corpus

packages/knowledge/src/graphify-code-backend.mjs declares code: ["ingest", "relations", "status"] and is the one module that drives graphify for, and reads the edges of, graphify-out/graph.json.

graph build is ingest(“code”)

aof graph build answers the same BuildResult through the port’s ingest("code"), keeps 174’s share around it, and writes a self-ignoring graphify-out/.gitignore (*) on every build, so the built graph leaves nothing untracked.

graph impact is relations(paths)

aof graph impact answers byte for byte as before through the port’s relations, reading the edges once per call, and an artifact that does not parse refuses with a coded graph-unreadable 424 instead of escaping as a raw SyntaxError.

One degrade path for the code corpus

With the graphify binary absent graph build answers graphify-missing and creates no graphify-out/, and with the artifact absent graph impact answers no-graph, both from the code backend.

179/03 · The record corpora sit behind the port, and aof memory answers byte for byte as it does today

Every record backend declares the record corpora

Every backend in BACKEND_REGISTRY exports exactly { name, corpora, recall, reindex, status } and declares work, operator and imports from the one RECORD_CORPORA, never code or relations, and none declares them too, so memory off answers served-and-empty rather than corpus-unserved.

aof memory dispatches through the port

recall, recurrence, ingest, reindex and status reach the configured backend only through createKnowledgeEngine, and both aof memory and aof work memory print the same bytes as before the port for every argv row of the outline.

179/04 · Test selection, contract derivation and seam liveness ask relations instead of parsing the graph

The three readers ask relations

Impacted-test selection, story-contract derivation and the seam-liveness audit each take a relations function bound in core (bindings/code-relations.mjs) and report the same widenings, limits and refusals as before, with selectSuites and deriveStoryContract now async.

The code graph has one reader

Across packages/*/src, readGraph, normalizeGraph and computeImpact are imported only by the graphify code backend, their own modules, graphify.mjs and the graphify memory backend, and FF-17904 refuses any other importer.

A driver-free relations port

createCodeRelations composes relations over the code backend’s read half alone, so the audit family’s construction reaches no graphify driver and no process-starting tool store.

178 · Memory is its own command

Memory is a top-level concern end to end

Memory’s config (memory, 05/ADR-002), its command (aof memory, m178/01/the-aof-memory-route) and every bundle prompt’s recall step (m178/02/bundle-assets-spell-aof-memory) now name the same top-level concern, and aof work memory stays a byte-identical alias (m178/01/the-work-memory-alias).

A prompt-driven recall leads to the PRD behind a record

A recall made by any bundle prompt (refine, continue, verify, retrospective, shatter, in Claude Code, Codex, opencode or Cowork) returns work records that name the planning document their item was framed from (m178/03/records-carry-a-resolved-origin), while recall still searches only the work stream and wiki/planning is not indexed.

178/01 · aof memory is the command and aof work memory its alias

The aof memory route

aof memory <verb> is a registered command at route ["memory"], answering recall, record-recall, recurrence, ingest, reindex and status through the one core path runMemoryVerb.

The work memory alias

work:memory at route ["work", "memory"] is a second registered door whose input, run, argv, render and json are the canonical command’s own function objects, so both spellings print the same bytes for every verb and projection.

Help and hints spell aof memory

aof --help lists memory as a family of its own, and the usage text, the stale-store hint, import-milestone’s reindex warning and the work-command examples all name aof memory.

178/02 · Every bundle prompt, skill and doc spells aof memory

Bundle assets spell aof memory

No file under packages/*/assets/ spells aof work memory in any whitespace form, and FF-17802 refuses one that does.

Rendered runtimes carry the new spelling

.claude/, .agents/, .opencode/, .codex/ and .aof/loops/ are rendered from the migrated assets, and the work install renders all three runtimes, claude, codex and opencode.

Docs spell aof memory

The root README’s memory block and command reference, the schema’s memory.backend description and the knowledge README name aof memory, and milestone 05’s delivered-milestones row keeps its old spelling as history.

The learning-edge control reads the new spelling

acd-learning-edge-reaches-every-cut parses aof memory <verb> invocations in the prompts.

178/03 · A recalled work record names the planning document its item came from

Records carry a resolved origin

Every MemoryRecord carries origin, a list of project-root-relative, forward-slashed paths that each name an existing file. It is [] when there is none, and a story without its own origin: takes its top-level driver’s.

Origin resolution

A declared origin: value is tried relative to the project root, then to the record doc’s folder, then to that folder as it stood before archiving. A value naming no file is dropped and counted in aof memory status as origin.unresolved (6 on the live stream).

Recall shows origin

--block appends ` · origin ; ` before the source, the text view adds a `↳ origin` line, and `--json` carries the field. A record with `origin: []` renders byte for byte as before.

Index version 4

INDEX_VERSION and GRAPHIFY_INDEX_VERSION are 4 in lockstep, and a version-3 store reports stale until aof memory ingest rebuilds it.

175 · aof on macOS

A Mac is a supervised single-node control node

This MacBook goes from clone to a running fleet with the deploy rules’ commands alone (m175/04/the-deploy-rules-carry-a-macos-section). The menu-bar app runs both daemons in the login session and returns at login with no terminal (m175/02/the-desktop-verbs-on-darwin), Quit leaves no aof process (m175/00/quit-reaps-the-whole-tree), and a supervised claude authenticates in the daemons’ environment (m175/03/the-darwin-preflight).

A launch folder is not a workspace anywhere in the mesh layer

A folder with no aof config of its own, such as launchd’s cwd /, is not synced, not read for presence and not watched (hasOwnWorkspaceConfig, F-11, F-14). mesh serve --serve --workspace is honoured, the fleet UI resolves identity through resolveWorkspaceId, and repo availability reads the pinned checkout (TECH_DEBT 4 A–D, item 21).

175/00 · Quit reaps the whole tree on macOS

Quit reaps the whole tree

On macOS every supervised daemon leads its own process group, and Quit, SIGTERM and a quit Apple Event each TERM every group, wait 5 s and KILL what is left, so no aof process outlives the app (FF-17501).

The menu-bar menu

A left-click on the menu-bar icon opens the menu, which stays open: it is replaced only when its items change, and the icon only when its glyph does. Windows still shows the window on a left-click.

The window on a Mac

The window keeps the native traffic lights over its own title bar, and the red light hides it to the menu bar. The node row shows the hostname whole where it fits, with the role badge and this node chip in one centred group (DESIGN M3, M4).

175/01 · install-local builds the app on macOS

The app bundle is built and placed

node scripts/install-local.mjs --desktop on darwin builds the release app, assembles an ad-hoc-signed aof-mesh-desktop.app (executable AOF Desktop, LSUIElement) and places it in ~/.aof/bin beside the payload.

The install leaves the shell alone

No shell profile is edited and ensureOnPath is gone, so the npm-linked aof stays first on PATH, while ~/.aof/bin/aof --version answers payload <buildId>.

175/02 · The desktop verbs install, run and stop the app on macOS

The desktop verbs on darwin

install places the .app. run starts it through launchd in the login session, reloading a loaded-but-stopped job from its plist and refusing over SSH when no LaunchAgent exists. stop sends SIGTERM and waits up to 20 s, never force-killing.

Login autostart

install --autostart writes the LaunchAgent com.aof.mesh.desktop (RunAtLoad, Aqua session, KeepAlive false, the installing shell’s PATH), and Login Items lists it as “AOF Desktop”.

175/03 · The preflight checks the Mac

The darwin preflight

On macOS the preflight adds macos-command-line-tools, rust-toolchain, app-not-quarantined and daemon-claude-authenticated, and a check with nothing to probe reports --.

The daemons’ claude is probed in their environment

daemon-claude-authenticated runs claude auth status with the LaunchAgent’s PATH and launchd’s HOME, USER and LOGNAME alone, so it answers as a supervised run would (FF-12607).

175/04 · The Mac runbook and acceptance

The deploy rules carry a macOS section

.claude/rules/build-deploy-restart.md takes a Mac from clone to a supervised fleet: the build prerequisites, install-local --desktop, the control-node nomination with fabric direct on 127.0.0.1, aof mesh identity, autostart, deploy, restart, verification at the source, and the gotchas with the crash recovery.

The desktop README and ROADMAP

apps/desktop/README.md names the macOS prerequisites and the .app, and ROADMAP lists macOS crash containment as deferred.

174 · A built graph is shared through the project’s own remote

A commit’s codebase graph on the project’s own remote

A project’s remote can hold one graph bundle per commit, as a blob behind refs/aof/graph/v1/<full commit sha>. A plain git clone never downloads it.

aof graph build takes the published graph on a miss

With graph.share: "git-refs", a whole-root code build with no current local graph first fetches the graph published for HEAD from graph.remote (default origin). It keeps that graph, with no graphify run, only when its inputs match the tree. Otherwise it rebuilds as before, and an unusable remote is a warning, never a failed build.

Clean default-branch builds are published once, and the remote keeps the newest 20

After a build, the graph is published only when it was built at HEAD from a clean tree and HEAD is on the remote default branch’s tip, which must be present locally. Any other build names its reason and pushes nothing. The first publish of a commit wins, nothing is force-pushed, and every successful publish prunes refs to the newest 20 first-parent default-branch commits.

aof graph fetch, aof graph publish and aof graph prune

Each half runs by hand, whether or not sharing is on. fetch lays out the published graph through graph:import’s own staged swap and refusals. publish and prune apply the build’s rules and report what they did or why not. Unlike the build, each verb exits non-zero on a remote it cannot reach.

Only the codebase graph rides the transport

graph-share.mjs is the one module that names refs/aof/graph/. No memory module reaches it, and it reaches no memory or mesh module, so the memory work graph and index are never fetched or published (FF-17401). No push to the namespace forces, and every remote call runs with GIT_TERMINAL_PROMPT=0 (FF-17402).

172 · Excalidraw phase 3 – the architect conversation streams into the editor, through a designed UI

The architect conversation is an editor surface with no terminal

A question or reply at a shape reaches a headless session on the board’s server (m172/00), its answer is written into that shape’s thread in the one conversation panel as it is produced (m172/01, m172/02), and the diagram surface renders no terminal and holds no code to type into one (m172/03); the operator approved it live on 2026-10-28, which closes F-171-04 (VERIFICATION ## User sign-off).

The conversation surfaces have a committed design baseline

mocks/conversation*.png (accepted 2026-10-27) are the conformance source of truth for the panel, the canvas ask, a proposal and the session state, with DESIGN.md stating the accepted fallback wherever the shipped surface cannot yet say what the mock says (F-172-09, F-172-10).

172/00 · The architect answers in a headless session

A headless conversation session in @aof/execution

conversation-session.mjs runs one stream-json claude per key, one turn at a time: a delivery during a turn is queued in order, a session with no subscriber and no turn in flight ends after ten minutes (stdin closed, then the tree), and one pure reader turns its stdout into session, turn, text, activity and turn-end events, each naming the request it answers.

The launch never bypasses permissions

The session launches with --permission-prompts none and an explicit --allowedTools list, with an argument vector and no shell, at the refine phase’s model and effort, and with the session-attachment keys scrubbed by the same predicate the loop’s driver uses (FF-17201).

The board delivers to the architect’s session and streams its turn

POST /api/diagram/conversation (admitted like every board write) starts, sends to, or queues on the diagram’s session; /api/diagram/events forwards each conversation event as an SSE conversation event, a snapshot first; with no claude to launch the route answers 503 architect-session-unavailable.

One home for the architect’s vocabulary

packages/diagrams/src/review-session.mjs is the only place the /aof:diagram-review invocation and the session’s tools (Read, Grep, Glob, Write, Bash(aof diagram scene:*)) are spelled; diagram:scene --action session and the editor’s copy-the-command fallback both read it (FF-17202).

172/01 · Every conversation in one panel

One panel holds every conversation on a diagram

ConversationPanel.tsx beside the canvas is the one place a thread, its replies and its proposal are read; the review list, the note bar and the thread popover are gone, and a note pin or the canvas ask opens its thread in the panel.

One pure model of threads

conversation-model.mjs (which absorbed note-threads.mjs) derives each thread, its subject, its state and the proposal it holds from the review records alone, with one clockOf time formatter.

172/02 · The answer streams into its thread

The answer is written into its thread as it is produced

conversation-stream.mjs reduces the stream’s conversation events into the session’s state and each request’s live turn, and the panel draws that turn inside its thread until the architect’s recorded turn for the same request replaces it; the thread follows the text while it streams, until the operator scrolls up.

Asking and replying reach the server’s session

Ask, a reply, Review with the architect, Ask again and a first draft POST to /api/diagram/conversation; Save as note posts nothing; no editor action opens or types into a terminal, and an unavailable session shows the command to copy.

Publish says it published

The publish answer carries whether the figure’s own ADR shows it, and the editor’s notice reads “Diagram published.”, adding only that the architect links the figure when the ADR does not show it yet; no link block is handed to the operator (F-172-16).

172/03 · The diagram surface has no terminal

The terminal control carries no diagram seam

apps/ui/src/diagram/dock-mount.mjs is gone, and the terminal control no longer takes laterInputs, onLaterInput or onSessionReady; the mount command’s own readiness typing (typeCommand, the outbox) stays for the board’s dock.

The surface table lists only surfaces that render a terminal

The fleet’s terminal-input census no longer carries a diagram dock row, and every control that counted the removed lanes and lines reads the smaller tree.

172/04 · A right-click leaves the drawing true

A right-click is checked to leave the drawing as it was

The diagram editor’s browser smoke drives six right-click gestures over 171’s ADR-003-push-stream scene at device pixel ratio 2, asserts each opened its own element’s menu, and compares the drawing and the saved scene after each; F-171-03 did not reproduce on the overhauled editor and is ruled out.

171 · Excalidraw phase 2 – the editor follows the architect live and is easier to drive

A conversation about a shape without leaving the editor

The architect is asked about a shape at the shape (m171/00), its answer arrives in the open editor as it is written (m171/02), the note it answers is a thread pinned beside that shape (m171/05), and a reply typed there reaches the same live session and is answered in the same thread – run live by the operator on 2026-10-25 (171 VERIFICATION, User sign-off).

The editor reads the architect’s records only through the scene command

Every editor surface this milestone added – the stream, the pins, the threads, the palette – reads diagram state through diagram:scene and the review records it owns; the board face names no diagram path and imports nothing of @aof/diagrams (FF-17101).

171/00 · Ask the architect from the canvas

Ask architect at the selection

A selected shape, or several, shows an “Ask architect” action on the canvas above the box enclosing them, and a question asked there is recorded as a request naming the selection’s ids and delivered exactly as the bar’s ask is (askArchitect in editor-state.mjs).

One placement home for on-canvas affordances

Every on-canvas affordance is a component of the one overlay layer (CanvasOverlay.tsx), placed only through canvas-anchor.mjs, the one module that converts scene coordinates to the screen (FF-17102).

171/01 · Find a library item by name

Find in libraries

The editor’s sidebar has a Find tab, beside the SDK’s own, that filters every item of the global and project library roots by item or library name (a library in both roots is searched once, from the project) and inserts a clicked or dragged result through the SDK’s own library drop, saved as an aof:lib node.

171/02 · Review records are pushed

The diagram events stream

GET /api/diagram/events is one “look now” stream per open diagram: it says only that the review records or the scene moved, with the stamp that moved, and carries no record; the board face learns what to watch from diagram:scene --action stamp, re-stamps on a change, every 30 s as a safety, and every 2 s when no watch starts.

The editor follows the stream

An answer or a proposal written by the architect appears in the open editor’s panel without a reload (~210 ms measured); a draft saved elsewhere is taken in place by a clean canvas and offered as Reload to a dirty one; the editor’s own writes never read as changed elsewhere, and the five-second poll runs only while the stream is down.

171/03 · Drafts in the house style

The palette from the style guide

house-style.mjs reads the semantic-roles table of the guide work.diagrams.style names (its light column, hex or rgba) into a palette, and there is none without paper and ink; diagram:scene read and context carry it.

Proposals and hand-drawn shapes start in the house style

propose fills fill:paper, stroke:ink, color:ink into every added node that has no style and is not a library item, before the diff, so the record and the preview say it; the editor seeds the SDK’s current-item defaults from the palette, so a shape drawn by hand starts in it and a stored shape keeps its own colours.

171/04 · A note reaches the open session

Delivery to the open session

A note or a question added while the architect’s session runs in the dock is typed into that session and run after its current answer, as its own command; with no session running, Ask architect starts a keyed new one that takes the question and then the waiting notes, and a note otherwise waits for a review session – the surface’s one deliver says which happened.

What a live terminal session is typed

The dock types its command only once the TUI’s own readiness holds (bracketed paste on, then something drawn), or at a 30 s cap, and every input – the command and each later input – goes through one serial outbox as one bracketed paste followed, after a 900 ms settle, by its Enter as a separate write.

One home for the TUI input contract

@aof/contracts/tui-ready is the single home of the readiness gate and the paste framing, read by the loop’s session driver and screen and by the browser’s dock (FF-13801 re-homed there).

171/05 · A note is a thread beside its shape

A note is a thread

A request record carries a thread of turns: reply appends the operator’s turn and reopens the note (a proposed one too, its proposal untouched), respond appends the architect’s and still sets answer, and an older record reads as the thread its request and answer imply; the architect’s context carries the whole thread, oldest first.

Threads beside their shapes

A note on a shape is a pin at that shape’s corner – one pin per shape, counting its notes, coloured by whose turn is last – and choosing it opens the thread beside the shape, where a reply is recorded and delivered to the running session through story 04’s one deliver.

170 · Excalidraw phase 1

An ADR’s diagram is a refinement loop between the operator and the architect

On a project with work.diagrams on, an ADR’s figure opens in an editor where the operator draws, uses project libraries and leaves notes, and the architect answers or proposes edits that apply only on the operator’s accept. Its parts are stated whole in the stories: the store and loop (m170/00), the package pair (m170/01), the opt-out (m170/02), the route (m170/03), the first draft (m170/04), notes (m170/05), the batch review (m170/06), one format (m170/08) and libraries (m170/09).

The loop runs on the Windows control node

The editor, store and review records work on the platform the operator uses. They were verified there on payload f43d5121, with a real model, a real AWS library and a published figure (ADR-010’s own diagram).

170/00 · The native draft store and the review loop

Scene store with optimistic revisions

diagram:scene saves an ADR’s draft as a canonical .excalidraw file under the diagram’s exclusive .lock, and a save against a stale revision is refused as a conflict.

Review loop

Requests, answers, proposals, accept and reject are aof diagram scene actions. Each is kept as a review record beside the diagram, and on every platform a record is created by rename() without replacing an existing id.

170/01 · Diagramming is a package pair

@aof/diagrams and @aof/diagrams-ui

Diagramming lives in two workspace packages, the node half (layout, generator registry, diagram:* verbs, doctor lane) and the browser half (editor, pure state, SDK, fonts), each reached through one contribution or one dynamic import.

One-way dependencies

@aof/work imports nothing of either package, which FF-17001 (acd-diagrams-package-one-way) enforces.

170/02 · The opt-out seam

One capability read

aof diagram capability and GET /api/diagram/capability answer { enabled, editor, generator } through the one reader of work.diagrams, and the editor follows the generator switch.

The opt-out holds at every door

With diagrams off, the board shows no Edit draft and fetches none of the editor. Every write action of diagram:scene refuses with diagram-disabled. A directly opened /diagram imports no editor and says the project’s diagrams are off, or, on a server that does not serve them (the fleet), points to the project’s board.

170/03 · The editor is a route

/diagram?ref=&file=

The editor is a routed surface, listed under no nav item. It is addressed by query, so it survives a reload and can be linked from the board’s Edit draft.

The architect’s dock beside the canvas

The surface binds its own dock (diagram/dock-mount.mjs, a fourth posture home beside fleet, board and home), so the architect’s session sits next to the drawing.

170/04 · A first draft from Mermaid

A first draft on request

An empty diagram offers “Request a first draft from the ADR”. It records a request and opens the architect’s dock session, and the architect proposes the whole diagram in the Mermaid profile.

Drawn by Excalidraw’s converter, in the drawing font

Accepting a first draft draws it with @excalidraw/mermaid-to-excalidraw, after loading the hand-drawn font for the proposal’s text, so labels are measured and wrapped in the font that paints them.

170/05 · Notes without a session

Notes that start nothing

Add note records a review request on the selection, or on the whole diagram, without launching a session. The review panel lists every open note, however old, and the operator can withdraw one.

170/06 · One command examines every note

Batch review

/aof:diagram-review <ref> <file> with no request id examines every open note in one session, recording an answer or a proposal per note. The single-id form still answers one.

Records leave the batch when acted on

A respond marks a note answered and a propose marks it proposed. A withdrawn or answered note refuses both, so a second batch never answers it again.

170/07 · The served-path smoke

A real-browser check of the served editor

test/smoke/diagram-editor.smoke.mjs serves two throwaway projects (diagrams on, diagrams off) with the real aof work ui. In installed Chrome it drives the board link, first draft, library insertion, freehand, notes, batch review, accept and publish, with the real CLI standing in for the architect.

170/08 · One format

The scene is the store, Mermaid the interface

The architect reads an ADR’s diagram only as the aof Mermaid profile projected from the stored scene (scene-view), and a scene whose view the profile could not read back is refused at save, naming the node.

Element-level accept

A proposal compiles in the editor into element operations. Accept refuses as stale, by Mermaid id, anything it touches that a person changed since, and refuses any update or addition outside what the proposal touches.

Publish from the canvas

Publish writes the editor’s SVG, and the PNG when work.diagrams owes one, where the ADR’s block links them, and answers the block to paste.

170/09 · Project libraries

Libraries used as they are, from two roots

Every .excalidrawlib in the global home’s diagrams/libraries/ (shared sets) and in the project’s .aof/diagrams/libraries/ (its own) loads into the editor’s Library panel. A project file shadows a global one of the same name, and each root has its own 8 MB cap. A real set (AWS Architecture Icons, 249 items) loads.

Insertions keep their provenance

An item a person drags in reads as %% aof:lib <id> <library>/"<item>". The architect places an item by name, and an edge proposed to a hand-placed item binds to an anchor that compile adds in the item’s own group.

169 · Hyperagent plans the work

Planning can be handed to Hyperagent end to end

A project turns on work.integrations.hyperagent, uploads the planner package once to the Hyperagent account, and from then on /aof:hyperplan takes a request to a draft pull request whose branch aof work validate passes. Measured on 2026-10-23 with draft PR #18. The parts: the verbs and skills (m169/00), the integration seam (m169/01), the aof-home package (m169/02) and its single zip upload (m169/03).

Hyperagent is an integration package, and core names none outside one registry

Everything Hyperagent ships (verbs, /aof:hyperplan, planner files) lives in packages/integration-hyperagent and reaches core through one contribution read by packages/core/src/integrations.mjs. The model knows exactly claude, codex and opencode (FF-16901, FF-16902). A new integration is one package plus one registry line.

169/00 · Hyperagent plans the work

/aof:hyperplan delegates a planning step to Hyperagent

From Claude Code, OpenCode (/aof:hyperplan) or Codex ($aof-hyperplan), a planning request is composed into a deterministic brief, sent to the configured Hyperagent agent over its MCP endpoint, followed to a result block, and its plan branch checked. Questions are relayed and never answered for the operator.

The four deterministic verbs

aof work integrations hyperagent brief | result | check | doctor compose the brief (refusing an unknown mode or a missing request), read the AOF-PLAN-RESULT block back, validate a plan branch in a temporary checkout using the repository’s rename history (your index, HEAD and worktree list are left untouched), and report the setup. A long request’s plan branch is cut at a whole word.

Planning skills written for an agent with no checkout

aof-prd and full rewrites of shatter, refine, add-milestone, add-story, add-task and validate each carry the Hyperagent execution contract: reach the repository through GitHub, write only an aof/plan/<slug> branch, end in a draft pull request, never simulate aof.

A live plan run delivers a validating draft pull request

Measured on 2026-10-23: one plan request delivered draft PR #18 (a PRD, its research, three backlog milestones and two spikes, all inside the planning lane), and check reports aof work validate: PASS over its branch.

169/01 · Integration packages are self-contained

Integration contribution seam

An integration package exports one contribution (id, name, assets, commands) and reaches core only through packages/core/src/integrations.mjs, the one core module that may import an integration package (FF-16901; Notion is the one named exception).

Integration-owned bundle members

readDescriptor() appends each contribution’s assets/bundle.json members to core’s and reads their text from the owning package, both in dev and payload installs (the package’s assets/) and in a packaged binary (bundle/packages/<name>/ in the sidecar). A member id that core already declares is refused at load.

The integration’s render follows work.integrations.<id>

An integration’s resources and assets render only when the project config has work.integrations.<id> (an empty object is enough) and are removed as aof-managed files on the next aof work update once it is gone; core’s members are never gated.

Hyperagent is an integration, not a runtime

The model knows exactly claude, codex and opencode; --runtime hyperagent is an unsupported runtime, and packages/core/assets holds no Hyperagent command, skill, planner instructions or variant (FF-16902).

The plan-branch check runs on Windows

aof work integrations hyperagent check extracts a plan branch into a temporary checkout with a forward-slash --prefix, validates it there and removes it.

169/02 · The Hyperagent package lives in the aof home

The Hyperagent planner package

aof work integrations hyperagent package [--dry-run] writes AGENT.md and the seven skills/aof-*/SKILL.md to <aof home>/hyperagent/ (~/.aof, or AOF_GLOBAL_HOME). The files are byte-identical to the package’s own. It owns that directory whole, writes nothing in the project, records no lock entry, needs no work.integrations block, and prints the upload steps without claiming an upload.

A project renders /aof:hyperplan alone

With work.integrations.hyperagent present, aof work init|update renders /aof:hyperplan (Claude and OpenCode command, Codex skill) and nothing under .hyperagent/. A lock that recorded the earlier .hyperagent/ render loses those files on the next update.

The doctor reads the package where the verb writes it

aof work integrations hyperagent doctor reports the planner package as missing, stale (it differs from the installed aof’s files) or current. When it is not current, a hint names the package verb. “ready” requires current.

The fragment declares what core never renders

Hyperagent’s assets/bundle.json carries packageFiles, the eight files its own verb ships, beside its one member hyperplan. The bundle-membership control counts them as declared.

169/03 · The Hyperagent package carries one zip

One upload for the seven skills

aof work integrations hyperagent package also writes <aof home>/hyperagent/aof-hyperagent-skills.zip. It holds <id>/SKILL.md for each of the seven planning skills and nothing else, uses the same bytes on every run, and the hand-over names it first, with each loose SKILL.md as the fallback.

The doctor reads the zip

aof work integrations hyperagent doctor reports the package missing when the zip is absent and stale when its bytes differ from the zip the installed aof would write.

168 · A prototype builds something fast

A prototype is the lightest work item in the stream

prototype is a sixth top-level item type alongside the governed five. It is numbered in the same stream as milestones, spikes and chores, but it carries no stories, tasks, .feature, criteria, ARCHITECTURE or VERIFICATION. Nothing schedules it or waits on it, and its whole record is PROTOTYPE.md and STATE.md (m168/00).

One verb runs a prototype end to end, in the operator’s session

aof work prototype and /aof:prototype start, continue (taking a new ask), stop and complete a prototype on the current branch. No branch, worktree, subagent, review lane, commit, run record or accept door is involved (m168/01, FF-16804).

Every stop is a checkpoint on record

The wants keep their history (additions dated, drops struck through, never deleted). Each stop, done or abandon copies the hand-off into ## Log, so a prototype’s full state at each checkpoint can be recovered without a plan written in advance (168/ADR-004 §4).

A prototype is visible wherever governed work is

aof work find, status, validate, doctor, aof:explain, aof:add-diagram and the board all resolve a prototype. The board shows it in the existing lanes and counts it in no milestone tally (m168/00, m168/02, m168/03).

168/00 · A prototype is a work item

A prototype resolves like any work item

A NN_prototype_<slug> folder in wiki/work (or the backlog) is a top-level work item that aof work find, status, list and doctor resolve. It is neither a driver nor a depend target, so aof work next never offers one and no depends: may name one (FF-16802).

A prototype has its own four status words

A prototype’s status is active, paused, done or abandoned. Its edges are active ⇄ paused, either one → done | abandoned, and done and abandoned reopen to active. The governed five words and their order are unchanged, and aof work status refuses a governed word on a prototype as status-edge-not-applicable, naming the legal moves, and refuses a prototype word on a governed item.

Validate holds a prototype to its two files

aof work validate checks a prototype’s frontmatter, the ## Intent, ## Wants and ## Shortcuts sections of PROTOTYPE.md (Intent and Wants filled past the template), the ## Hand-off and ## Log sections of STATE.md, and that it carries no depends: or parent:. The doctor’s lifecycle lanes skip it, so a well-formed prototype is clean at every status word (FF-16803).

One type-to-record-doc map

RECORD_DOC_BY_TYPE in packages/work/src/identity.mjs is the only map from an item type to its record doc (prototype → PROTOTYPE.md among them), and records, doctor coherence, the doctor snapshot and the ratchet all read it (FF-16801).

The prototype templates ship in the bundle

.aof/templates/work/prototype/ holds PROTOTYPE.md and STATE.md, declared in bundle.json as template prototype and rendered by aof work update.

168/01 · One verb builds a prototype

aof work prototype carries the four moves

aof work prototype "<ask>" mints the next stream number through runInsertTopLevel’s append form and writes PROTOTYPE.md (Intent and first Want from the ask) and STATE.md at active. <ref> ["<new ask>"] appends a dated Want and logs it, and reopens a paused, done or abandoned prototype. --stop, --done and --abandon copy the hand-off into ## Log under a dated line and set paused, done or abandoned. --log appends one line.

The verb refuses what it cannot write faithfully

Two moves in one call, or a new ask beside a move flag, are refused as prototype-one-move. A stop or complete over the template’s unfilled hand-off is refused as prototype-handoff-missing, and a ref that is not a prototype as prototype-not-a-prototype. The verb never commits, mints no run record and imports no git, dispatch, worktree or lane module (FF-16804).

/aof:prototype builds in the operator’s own session

The bundle command (and its codex workflow.md section) calls the verb for every move. It spawns no agent, makes no branch, worktree, review lane or commit, keeps ## Shortcuts current, and when the checkout is on main says so once and then builds. It ships to every runtime add-chore ships to, through aof work update.

Explain names a prototype by its intent

aof:explain describes a prototype from its ## Intent.

168/02 · A prototype draws its diagrams

Diagram plan and export take a prototype

On a prototype, aof diagram plan and aof diagram export take a diagram name in place of an ADR id. The brief is the prose under ### <name> in PROTOTYPE.md ## Diagrams, and the files land in the prototype’s diagrams/ named by that name. An unknown name is refused as diagram-name-unknown, --slug as diagram-slug-not-applicable, and a heading with no prose as diagram-brief-missing. The prototype paths live in packages/work/src/diagrams/layout.mjs beside the ADR paths (FF-13302).

A prototype diagram can be redrawn

Exporting a name that is already drawn overwrites its files, with no dated copy; diagrams/ holds only the current picture.

/aof:add-diagram takes a prototype ref

aof:add-diagram <prototype> "<what to draw>" writes a new ### <name> brief, draws it and pastes its link under the brief. aof:add-diagram <prototype> draws every brief with no link, and aof:add-diagram <prototype> <name> redraws that one. The ADR path is unchanged.

168/03 · The board shows a prototype

A prototype card sits in an existing lane

The board places a prototype in an existing status lane: active in in-progress, paused in not-started, and done and abandoned in done. The mapping is laneStatus in apps/ui/src/board/model.ts, which the card’s status ring also reads. The card is tagged prototype · <status> and shows the prototype’s own status word.

Prototypes are counted nowhere

No milestone tally, story count, active-milestone count or blocked-gate count includes a prototype, and a milestone focus shows none.

Opening a prototype shows its two documents

A prototype card opens on PROTOTYPE.md with a STATE.md tab and no other tabs. PROTOTYPE is in the one artifact set (packages/work/src/artifacts.mjs), so work:doc serves it and a worker streams it.

167 · The daemon stays within a resource budget

The serving daemon holds its idle budget on a tree as large as aof’s

On the Windows control node and in Linux, mesh serve idles under the 2% budget over a 1,800-doc tree with the motivating faults planted, and both daemons hold the 10% viewer-open budget with a viewer polling. This is proven by the soak (m167/04), which is one unit of the whole-tree regression gate. What makes it hold at the milestone level is the four stories together: a tree that has not changed is not walked (m167/07), not parsed (m167/01), not re-walked for runs (m167/06), and a permanent fault is not retried (m167/03).

A daemon’s cost is visible, attributed and watched from outside

What a daemon spends its time on is named by cause and workspace in aof mesh status --json and aof work doctor (m167/00), and an idle view asks almost nothing of it (m167/02). The desktop shows a sustained overrun and never acts on it (m167/05). The motivating 17-hour burn would now show in all three.

167/00 · The daemon says what it costs

Each daemon meters its own cost by cause

packages/foundation/src/cost-meter.mjs is the one reader of process.cpuUsage and eventLoopUtilization in packages/ (FF-16701). It counts work against a closed cause set, labelled by workspace, route and X-AOF-Client client, and mesh serve and mesh ui each write a snapshot to meshDir/cost/ every 15 s and once more at a clean stop.

Every degrade line names its workspace

A degrade raised while reading a workspace carries its workspace label, and every report is counted through the meter even when the line itself is throttled.

The daemon budget has one home

The idle budget (2% of a core) and the viewer-open budget (10%) are declared only in cost-meter.mjs (FF-16702); a fleet or board request makes a daemon viewer-open for 30 s.

aof mesh status and aof work doctor say what each daemon costs

mesh status --json carries a cost block per daemon (share, state, budget, top causes), its text report prints one cost line per daemon, and a snapshot older than 45 s reads not-reporting. The daemon-cost doctor lane warns on a daemon over its budget, naming its top cause, or on one that stopped reporting. Its warns never fail validate.

167/01 · An unchanged work tree costs a stat walk

A read of an unchanged tree is answered from a memo

packages/work/src/discovery.mjs remembers one tree read per resolved work dir, keyed by the stat digest and the tree’s directory shape. A second read of an unchanged tree opens no record doc, and an edited status or a new item (even one with no record doc yet) is answered on the next read. Both hot readers go through it: buildStreamView and the projection store’s readStreamSnapshot port. A memo answer counts as cache-hit labelled memo, and a parse as full-disk-read.

Concurrent reads of one tree share one parse

Two callers at the same moment, such as the presence tick and a board poll, get one parse. A shared read that fails is never remembered, and reads of two different trees do not wait on each other.

The stat digest has one home

workTreeStatDigest lives in discovery.mjs. The sync pass and the read memo compute the same digest for a tree, and the sync pass’s stat gate is byte-identical to 164/ADR-002’s.

167/02 · An idle view costs almost nothing

The polled routes answer 304 when nothing changed

/api/work/list and /api/mesh/status send an ETag and answer 304 to a matching If-None-Match; an error answer carries no tag. The tag is two FNV-1a lanes plus the length, computed in packages/server/src/static-serve.mjs, and the fleet’s tag ignores diagnostics.generatedAt.

Every UI poll is scheduled through one cadence

apps/ui/src/lib/poll-cadence.mjs schedules every interval poll in apps/ui/src (FF-16703). It makes no request while the window is hidden, polls at once when the window is shown, doubles while answers are unchanged up to 15 s, and returns to the 5 s base on a change, a user action, or a pending assign. The clock ticks and the session launcher’s outcome wait are exempt. While a Resync watches, every board poll stays at the base.

167/03 · A permanent fault is said once and backed off

A permanent sync failure is said once and backed off

The launcher holds a closed permanent-code set (event-id-conflict, workdir-missing, not-a-directory, no-descriptor). A workspace failing on one gets one warning, then counted sync-pass/backoff skips, with the gap doubling from one tick to a 1-hour cap. The backoff ends when the workspace’s stat digest moves or a pass succeeds, and a restart says the failure once more. Any other code is retried on the next tick and warned about as before.

One registry resolve per tick

The sync pass and the presence aggregation share one resolveTickWorkspaces result in capturePropagation, so a gone folder is reported once a tick at most.

A gone registration is removed at once

A registration whose folder is missing (workdir-missing) or is not a directory is removed from this node’s rows on the first tick, with one line naming the workspace and its path, by global-node-registry.mjs. A no-descriptor registration stays registered and backed off. Another node’s rows are never touched, a registry that cannot be read removes nothing, and the next aof command in the folder registers it again.

167/04 · The budget is proven by a soak test

The real daemons are soaked against the budget

test/mesh/budget/daemon-budget-soak.test.mjs runs the real mesh serve --serve and mesh ui over a generated 1,800-doc project, under their own temp global home and on free ports, with the motivating faults planted: an empty cache, an event-id-conflict and five gone registrations. After a 15 s warm-up it measures a 30 s idle phase and a 30 s phase with a viewer polling. Each daemon’s CPU-seconds per phase, read off its own snapshots, is judged with the meter’s judge against DAEMON_BUDGET. A failure names the daemon, the phase, its share and its top cause.

The planted faults are named, and each is said once

aof mesh status --json names each planted fault with its workspace in the daemons’ cost faults, and the serve log holds one line per sync fault, not one per tick.

The soak is one unit of the regression gate

It is registered through scripts/test.mjs, so the whole-tree gate runs it. It binds neither :4181 nor :4182, and leaves the global home it was handed and the process table as it found them.

167/05 · The desktop shows a sustained overrun

The desktop judges a sustained overrun from the OS’s own sample

The supervisor reads each child’s cumulative CPU time by its process handle on every 3 s poll tick (cpu_sample.rs). The core crate’s guard.rs judges that share against the budget in the cost block of mesh status --json, over the budget’s own 5-minute window: 60% from t0 enters overrun at exactly 5:00, a 40 s spike never does, and the overrun ends once the window has slid past the load. With no budget nothing is judged, a not-reporting daemon is judged against the viewer-open budget, and a daemon the OS cannot sample is never judged.

An overrun is shown and logged, never acted on

An overrun turns the tray degraded and fills the footer’s notice slot. It writes one JSON line on entering and one on leaving to the shared degrade log, with proc: "desktop". A standing exit notice outranks the overrun, and the overrun is dropped while the window renders error. The guard has no path to stop, restart or kill a child (FF-16704).

167/06 · Local runs reach the store through the ledger

Local runs are published through the ledger

run.started and run.completed each carry a publish-run reactor (locus local, retried until done) beside publish-projection. It upserts the run’s current record into work_item_runs with this node as reporter, only for a workspace the propagation predicate allows; a run recorded before this change reaches the store through the reconciler’s backfill.

The presence tick reads what is running from the store

The tick takes this node’s running runs and their loops from work_item_runs in one query over its shared store handle (readStoreActivity), and reads no item’s runs/ folder. A loop’s lastActivityAt is the one per-running-run disk read (FF-16705).

167/07 · An idle tick walks nothing

The serving daemon watches each work tree it syncs

mesh serve holds one recursive watch per work dir that its sync pass covers, a backed-off one included. The tick arms before it walks, so a newly watched tree is walked once, by that tick. A lane or a checkout off the mesh is not watched. A registration that leaves the tick set loses its watch on that tick, a tick whose registry read failed changes no watch, and the daemon’s stop closes them all. Nothing else in aof arms a watch (FF-16706).

An unchanged watched tree costs no stat walk

statWorkTree (packages/work/src/discovery.mjs) answers a watched tree from its last complete walk until a watch event dirties it or that walk is 5 minutes old. So the stat gate, the backoff check and the presence read walk nothing on an idle tick. An event naming a record doc, a folder or nothing dirties the tree. One naming any other extension (a run record, a log) does not. An event during a walk makes the next ask walk again, and asks during a walk share it.

A tree that cannot be watched is walked as before, said once

A watch that cannot start, or errors later, is closed. Its tree is walked on every ask, and one work-tree-watch-unavailable degrade names the dir. It is not armed again in that process.

Every walk is a counted cause

stat-walk is in the cost meter’s closed cause set. In a process holding watches, every real walk is counted under it, labelled unwatched, first, changed or safety.

166 · A built graph travels to another machine

Graph export to one portable file

aof graph export <path> writes the codebase graph, graphify’s cache and the inputs stamp to one gzip bundle stamped with the source commit, build time, graphify version and project identity, and replaces an existing file only with --force.

Graph import into a clone of the same project

aof graph import <path> lays a bundle out as the clone’s graphify-out/, refuses a bundle from another project with no override, verifies every file before swapping it in whole, re-keys the carried cache to the clone’s line endings, and reports the graph’s age and how many commits it is behind HEAD.

A build that runs graphify only when its inputs changed

A whole-root code-only aof graph build . stamps what it read (graphify-out/aof-inputs.json, line-ending-neutral) and runs no graphify while no input, git-visible path, graphify version or graph.json has changed, on the building clone and on any clone that imported the bundle.

A code-only build’s own time budget

A whole-root code-only build defaults to a 10-minute graphify budget; every other graphify spawn keeps the 2-minute bound.

A WSL deploy that runs

scripts/deploy-wsl.sh writes the distro’s bin/aof.mjs as a forward to packages/core/bin/aof.mjs and completes, including the native reinstall.

165 · Planning runs in Cowork

Planning in Cowork on the same work tree

A repo’s planning can be done in Claude Cowork: the operator writes a PRD with the pinned pm-skills planner and runs /aof:shatter, which writes framed milestone SPECs and spikes, with their origin and slug depends edges, into the repo’s wiki/work/backlog/, and aof work validate and aof work promote from Claude Code or Codex then accept and schedule them (165/03 E5).

Three planning runtimes, one lock

aof planning init installs planning for claude, codex and cowork side by side, each recorded as its own entry with its own pinned sha, so adding one never disturbs another (m165/00, m165/03).

One planner commit everywhere

The planner Cowork runs is packaged from the same 40-hex pm-skills commit Claude Code installs, never from a floating ref (m165/02).

165/00 · The planning lock records each runtime

One planning entry per runtime

.aof/aof.lock.json holds planning: { runtimes: { <runtime>: <entry> } }, and a planning init for one runtime leaves every other runtime’s entry deep-equal and every non-planning section intact (FF-16501).

A second runtime needs no –force

A planning init for a runtime with no entry is added beside the existing ones, while a re-install of a recorded runtime is refused without --force, and --force re-pins only the runtime it names.

Legacy flat sections still read

A pre-165 flat planning section reads as its one runtime’s entry, and the next write converts it to the per-runtime shape.

165/01 · The planning skills render as a Cowork plugin

The aof Cowork plugin

renderPlanningPlugin() returns the aof plugin’s file map: .claude-plugin/plugin.json plus skills/shatter/SKILL.md and skills/explain/SKILL.md, each the one cowork preamble followed by its command’s body unchanged (FF-16502), and refine is not shipped.

One command-to-skill mapping for codex and cowork

commandAsSkill takes each runtime’s preamble from its mapping, and codex’s mapped skills render byte-identically to their pre-165 output.

Markdown-only without the CLI

A cowork skill probes for aof, skips each CLI step when it is absent and lists the skipped steps, and never commits.

A reproducible package

zipStored turns a file map into a stored (uncompressed) zip whose bytes depend only on the map’s contents, never its order, using only node: built-ins (FF-16503).

165/02 · The pinned planner is packaged for Cowork

Planner packages from the recorded commit only

packagePlanner({ sha }) packages pm-skills only from a tree whose HEAD equals the 40-hex sha, cloned over HTTPS at MARKETPLACE_REF with core.autocrlf=false and core.eol=lf, and refuses any other tree with a named reason (FF-16504).

Each recommended plugin (with the optional pm-market-research on request) is its own file map carrying the repository’s root LICENSE, and pm-ai-shipping is never packaged.

No clone left behind

The live clone goes to a temporary directory that is always removed, and a caller-injected tree is never removed.

165/03 · Planning init delivers Cowork

aof planning init –runtime cowork

aof planning init --runtime cowork writes aof.plugin and one package per recommended planner plugin under the aof home’s cowork/ (~/.aof, or AOF_GLOBAL_HOME), prints each package’s full path with the upload steps, and spawns nothing.

The project carries only its lock entry

A cowork run writes nothing in the project except .aof/aof.lock.json, whose cowork entry names every package relative to the aof home and records installConfirmed: false (FF-16506, FF-16507).

Cowork beside claude and codex

A cowork entry is added beside the other runtimes’ entries without --force, a re-install needs --force and re-pins cowork alone, and an explicit --scope with cowork is refused, writing nothing.

All or nothing

A cowork run whose clone or packaging fails writes no package and no lock entry, and a --dry-run resolves no sha, clones nothing, spawns nothing and writes nothing (FF-16505).

A skill finishes unvalidated without the CLI

Each packaged skill’s preamble says that without aof a check the procedure requires is not a failed one, so a run marks its report unvalidated and names the checks to run from Claude Code or Codex.

164 · Every project change reaches the global store

Local files are canonical for a node’s own items

A project’s own records are the source of truth on reads and on writes: the global store is a projection updated only from files, and it adds only what this disk does not hold – other projects, and work another machine owns (stories 00 and 02; m164/00 and m164/02 outcomes state each half).

One sync pass, three drivers

Every change to a record file reaches the store through one pass – detect against the observed baseline, mark in the existing effects journal, drain through publish-projection – run by every CLI command for its own workspace, by the daemon at start and by every daemon tick over the registered set, whether or not a verb raised an event (m164/00, m164/01).

No sibling ledger

Sync state lives only in journal.sqlite and projection.sqlite; no package source opens any other database under the global mesh root (FF-16403), and no module writes a record doc from a stored row (FF-16404).

An owed sync lands and is visible

An owed publish never gives up and the operator can read its lag in doctor (m164/03); a publish over a partial node roster leaves membership intact and stays owed (m164/01).

164/00 · A local change is noticed and owed

Changed-since-observed baseline

work_item_observations (projection store schema 11) holds this node’s fingerprint of each row it last wrote, per workspace, plus a stat digest under ref *; only a successful upsert advances it, and packages/work/src/local-change.mjs is the one home of rowFingerprint/locallyChanged (FF-16401).

A local change takes its row

A record whose projected row changed since this node observed it takes authorship of its row in the next publish, exactly as an operator verb’s ref does; an untouched record never overwrites another node’s report, and a prose-only edit claims nothing.

Workspace changes are owed in the effects journal

A noticed change appends one workspace.changed event (id from workspace and digest, collapse key ws:<id>) drained by the existing publish-projection reactor; newer marks supersede older owed ones, a verb’s event is never superseded, and the journal is schema 2.

Every aof command syncs its own workspace

The CLI face runs the sync pass for the command’s own propagating workspace before it sweeps owed effects, and a failing pass never changes the command’s own result.

A linked worktree never publishes

A linked git worktree of a mesh-published repository is refused by the propagation predicate (mesh-workspace-secondary-checkout), so only the repository’s own checkout publishes the workspace’s rows and root.

164/01 · The desktop app syncs every workspace and catches up when it starts

The daemon tick syncs every registered workspace

Every propagation tick runs the one sync pass over resolveTickWorkspaces – this node’s linked workspaces plus the launch cwd when it is a workspace – the same set presence reads; the cwd-only publish is gone (FF-16405), and each skip is a coded warning while the tick goes on.

The daemon catches up when it starts

startLauncher runs the same pass over the same set before its first tick, so a change made while the desktop app was off is in the store as soon as it starts (measured live on node-7297, 2026-10-19).

A torn node record never empties membership

A registry publish over a node roster with an unreadable record writes nothing and fails node-record-unreadable, so the last membership stands and the owed change retries until the record is readable.

164/02 · My own items read from my files

Own records answer from disk

aof work find, list and next answer a record this node reported from its local file; a cached row overlays a local record only when another node reported it and the file has not changed since this node observed it, decided by the one cacheAnswers predicate (FF-16402).

Doctor reads the way the stream does

aof work doctor’s overlay asks the same cacheAnswers, with this node identified by the id the publisher stamps rows with, so its coherence group and the stream view judge one identity; own rows no longer raise cache-status-divergence.

Store-only items are still listed

An item with no local folder (work another machine owns) is still listed from the store.

164/03 · Owed sync never gives up and shows how far behind it is

Retry until done, with backoff

A reactor may declare retry: "until-done" (anything else is refused when the reactor table is built); every publish-projection declares it, and a failed step waits min(15s × 2^attempts, 15 min) from its last attempt instead of stopping at the five-attempt ceiling, which every other reactor keeps.

Explicit retries skip the backoff

doctor --converge and the lane cleanup drain with backoff: false, so an operator retry pays a recovered store at once.

Doctor shows the sync lag

Where the workspace propagates, aof work doctor prints one sync: line per workspace (last success, owed steps with age, attempts and error, records changed since the last sync), raises sync-failing/sync-behind as warnings that never fail validate, and adds a sync array to --json only where it applies.

163 · The operator can read the loop’s open questions

aof work questions

aof work questions reads the ask files under loop-asks/ through readAsks, with no daemon round-trip. For each ref it prints the latest ask that is waiting or parked, oldest first. A row carries the ref, phase, runtime, asked instant and age, workspace name, full question, numbered choices and aof work answer <ref> "…". --json emits the rows. When nothing waits, it prints “No open questions.” and exits 0.

A stale ask offers no answer line

A row whose run is no longer running is marked stale, with no answer line, and names aof work loop <scope> --resume. The verb judges this through askRunStopped / runsBehindAsk in work-loop/ask.mjs, the same functions the loop’s sweepStaleAsks calls.

–all widens to every workspace on this node

--all lists the open local asks of every workspace on the node. It names each workspace from its descriptor, or by id when there is none, and reports liveness unknown when that workspace’s checkout cannot be loaded.

Worker questions in this workspace are listed

An assignment that a worker parked on a human in the current workspace is listed with its node. Where the question text never reached this machine, the row says so and points to the worker’s terminal. A local ask outranks a worker’s ask on the same ref.

One waiting-on-a-human rule

standingAsk in work-loop/ask.mjs is the only definition of “this run waits on a human”. aof work resume receives it injected (openLastAsk is gone), keeps its NEEDS YOUR ANSWER row, and points to aof work questions for the text.

The /aof:questions wrapper

packages/core/assets/commands/questions.md runs the verb and renders through aof work update into .claude/commands/aof/questions.md and its codex and opencode faces. work:questions is registered and routed as aof work questions.

162 · A running loop is visible in the UI

Loop sessions stream to their cards

A loop-driven Claude session streams its terminal live to its session card on the home and the fleet, from the control node and from a worker, each through its own node’s daemon over a loopback, token-gated ingress that stamps the daemon’s node id.

Loop panes are read-only and say why

A session row that names a driving loop mounts read-only on every surface; no keystroke is sent, and the read-only badge reads “read-only – a loop drives this session; answer it with aof work answer”.

Loop progress on the home and the fleet

The running loop’s line shows the current drive’s elapsed time and its last activity, turning to “quiet " in the warning tone once the drive has not heartbeated for ten minutes; the line, the threshold and the tone each have one home in `apps/ui/src/components/loop-line.mjs`.

Loop progress in the terminal

aof work loop prints <ref> – <elapsed>, <n> subagent(s) active, last activity <age> ago every five minutes while a drive runs, per lane in a wave, silenced by --quiet.

The home names a running loop

When a live node runs a loop whose session is not in the grid, the home’s empty state names the loop and says its session has not reported, or is not relaying, instead of the bundle sentence.

Node liveness follows the heartbeat

A node reads live while its presence heartbeat is inside the window, whether or not any workspace has republished since.

161 · The compiled wiki stays true

Every direct wiki page’s source citations resolve, and doctor keeps them resolving

All 12 direct wiki pages currently have zero wiki-source-unresolved or wiki-source-unreadable findings, scoped or unscoped. A later move or deletion that strands a citation in any of them becomes a doctor warning (m161/01 states the probe whole).

The memory page is held to the code it describes

wiki/memory.md now describes the delivered procedure (m161/02). Because every src/ path it cites is checked by the same probe, a module move that would stale the page now surfaces as a warning instead of drifting silently.

161/01 · Wiki source citations are checked by doctor

Wiki source citations are a doctor finding

aof work doctor, scoped or unscoped, reads every Markdown page directly inside <project root>/wiki. It reports each cited local src/ file that resolves to no current file through resolveCitedPath and the recorded rename history as one wiki-source-unresolved warning per page and file, naming the first occurrence’s line.

An incomplete scan says so

An unlistable wiki directory, an unreadable page, or a Markdown symlink pointing outside the project is a wiki-source-unreadable warning that claims nothing about that input’s citations. A project with no wiki/ directory reports nothing new.

Existence is a stat, never an import

A cited source file is checked for existence as a regular file inside the project. It is never imported or executed. A directory, an external URL, a glob family, a non-source path and a path that traverses outside the project never count as a resolving source citation.

161/02 · The memory wiki describes the delivered procedure

The memory page describes memory as delivered

wiki/memory.md describes the delivered memory procedure from milestones 148, 153 and 157, citing a resolving source or ADR for each part. It covers the three layers and eight record types, source locators and tags, episodic provenance, recall filters and --all, the six decision edges, recall observations, recurrence and promotion, accounting, and the order of retrospective evidence.

The page carries no live-store facts

The page names no active backend or record count for this repository. It sends readers to aof work memory status --json for both, and it lists no retired verb as supported.

160 · The whole-tree suite signs off in minutes

Every pull request signs off on the whole tree in under two minutes

Each pull request to main runs all 12,988 registered cases across 8 Linux runner shards, inside a 10-minute budget the workflow enforces; on PR #12 the suite wall is 1.8 minutes, and each file’s drift against main is reported beside it (m160/13 states the workflow whole).

A timeout is a not-finished unit, never a red

Wherever the suite runs – a sharded run on the control node, a gate, or a PR shard – a unit stopped by its own limit, the budget or a stop is reported not finished, and the gate’s door refuses a not-finished row as its own case with its own repair (m160/05).

The control node’s gate is a correctness gate

aof work regression-gate on the control node runs the whole tree with no budget, in a clean launch environment that dies with its session (m160/04), and its row is what the accept door reads; the budget lives with the pull request suite.

A test run is hermetic for the operator’s files

A test run writes neither the operator’s ~/.aof nor their ~/.claude.json (m160/12), and no test spawns the registry runner unless the runner is its subject (m160/11).

160/00 · The loop reads each git fact once per step

A loop step asks git each ref or topology question once

memoGitReads in packages/execution/src/worktrees.mjs answers a repeated rev-parse, symbolic-ref, show-ref, merge-base, rev-list or worktree list call from its first status-0 answer within one step. It forwards content and working-tree reads every time, clears itself on any other verb, and never keeps a failed read.

A step is one in-process run of git work

The wave gives a fresh memo to the ask-and-dispatch tick (and the dispatch calls it makes), to a lane’s prelude, to a lane’s commit, to a merge home, to a cleanup, and to the epoch re-mint. No memo outlives the step that opened it.

The loop’s commit names its own tip

recordCommitTip reads the sha from the commit’s own summary line, so committing through the loop’s port needs no second rev-parse HEAD. A lane already at its base is not advanced.

One lane cycle is held under a pinned call ceiling

FF-16001 pins one ready story’s lane cycle at 34 git calls through the port (37 unmemoised). The measured cases make 37% fewer git spawns (471 → 295 over three wave cases).

160/01 · Tests that are not about git run on a git model

A git model stands behind the loop’s port in tests

test/support/loop/git-fake.mjs models the 16 verbs the loop sends through its port. It refuses any verb or form it does not model, so a case can never pass on an answer the model invented.

The model is held to real git

FF-16002 runs one 49-step script against real git and against the model, and compares stdout, status and directory bytes after every step.

Loop suites run their non-git cases on the model

31 case sites in loop-command-wave, loop-command-reconcile and loop-command-stops run with git: "fake". Cases whose subject is git (a real merge home, a conflict repair, a test-side git log or merge-base) stay on real git. Alone, the three suites run 18–31% faster with about half the git calls.

The dispatch command takes the caller’s exec

packages/work-loop/src/commands/dispatch.mjs threads ctx.exec through admission, list, sweep and cleanup. Production leaves ctx.exec unset, so the CLI is unchanged.

160/02 · A focused run loads only what it names

A selection launches its own program

work.test.selectProgramArgs compiles beside args and replaces it only when a launch selects files. This repository declares ["scripts/test-shard.mjs"], so a focused aof test launches the light entry and never imports the registry. The gate’s toolchain drops the key.

The shard entry runs a named selection

node scripts/test-shard.mjs <suite>... [positions] runs every file it is named through the runner’s one loader (loadSelected in scripts/test-harness.mjs). It enters through the runner only for a selection holding a suite that imports the runner. A focused run of one small suite takes 3.4 s instead of 7.3 s.

The isolation guard knows the light entry

.claude/hooks/aof/guard-test-isolation.mjs treats scripts/test-shard.mjs as a test invocation, so an unisolated shard run is blocked like an unisolated runner.

160/03 · A child process compiles once and loads only its verb

The compile cache is on in every aof and test process

packages/core/bin/aof.mjs and scripts/test-harness.mjs enable Node’s compile cache before the application graph loads, and the bin reaches cli.mjs only by a dynamic import. On the control node the cache engages and hits, and it does not lower start-up (F-03).

aof --version loads 16 modules

The session hooks and an unused workspace binding left cli.mjs’s static imports, so a version check loads 16 file modules instead of 41.

A child’s module count is pinned, shrink-only

FF-16004 pins each measured route’s module count by equality: --version at 16, work find at 706. A story that adds a module to a route’s closure must move its row.

160/04 · The gate launches clean and dies with its session

The gate’s suite starts in a clean environment

cleanLaunchEnv drops every GIT_*, CLAUDE* and AOF_* variable (case-insensitively), keeping only AOF_GLOBAL_HOME, before the gate launches its suite. The gate’s own environment is untouched.

A stopped driven run stops its gate

The gate’s keep-alive reads its driven run’s record on each beat. When that run is no longer running, the gate aborts its launch and refuses with regression-gate-stopped, writing no row. Measured: 58 s from the settle to no process left.

The sharded runner stops when its stdin ends

The pool lives in scripts/test-sharded-pool.mjs. It watches stdin only when stdin is a pipe, and on its end or close it kills every in-flight unit and starts no more. A stopped run writes no timings.

160/05 · The runner enforces the budget, and a timeout is not-finished

The sharded gate is held to its budget

The gate passes work.test.gate.budgetMinutes through budgetArgs (this repository: 10, ["--budget-min", "{minutes}"]) and bounds its launch at the budget plus 2 minutes. At the budget the pool kills its in-flight units and starts no more. A serial run is held to neither.

A unit out of time is not finished

A unit the pool killed (by a stop, by the budget or at its own limit) names no failure, prints no not ok, and is never retried alone. The report prints # not finished - <unit> and # not started: <n> units.

not-finished is a row result the door refuses

GATE_RESULTS holds not-finished beside green, red and override, with gateResult as the one home of their precedence. aof work status <NN> done refuses a not-finished row as regression-gate-not-finished, and the verify prose routes such a row to a re-run or a faster suite, never to a code repair.

A killed unit cannot hold the pool open

Once a unit has exited, its output gets a bounded drain (10 s). After that the pool stops waiting, even if a process the unit started still holds the output.

160/06 · Citation history is read only when a citation misses

Citation history is a lazy source

readRenameMap answers a loader whose first load() reads git history once. Doctor’s snapshot, its wiki probe, validate’s reads check and tune all resolve each citation on disk first, and call load() only on a miss.

A stream whose citations resolve spawns no git for them

aof work validate 160 makes no git call (4 before). Doctor and validate report byte-identical --json findings to the eager build, and one wave case makes 82 git calls instead of 132.

160/07 · A test’s child aof runs from one bundle

A sharded run’s child aof starts from one bundle

scripts/test-sharded.mjs builds one ESM bundle of cli.mjs per run, into the run’s own directory, before the pool starts, and every unit’s child aof loads it through AOF_TEST_CLI_BUNDLE; aof work find 160 --json starts in a median 275 ms from it against 999 ms from source.

The bundle and the release share one recipe

CLI_BUNDLE_RECIPE in scripts/build-sea.mjs holds the platform, target and externals for both; the test bundle rewrites each module’s import.meta to its own source file’s URL, so assets, the manifest and git resolve exactly as from source.

The bundle is held to the source by a parity control

FF-16008 runs a fixed command set from the bundle and from the source and requires the same stdout, stderr and exit code; a control whose subject is the bin’s own loading strips the variable, and a focused run sets none.

160/08 · Tests spend git only on what they assert

The git model merges a lane home

test/support/loop/git-fake.mjs answers and writes merge --no-ff of a lane branch as real git does, refuses a merge in which both sides changed one path, and refuses any merge form it does not model; sixteen more loop cases run on it (12 wave, 4 reconcile).

A real-git fixture starts from a template

The lane, dispatch and mesh-worker fixtures copy a repository built once per process instead of running init and config per case; work-dispatch-lanes makes 1 init and 2 config calls where it made 88 and 176.

The loop suites spend fewer git calls

loop-command-wave makes 2,219 git calls (4,023 before) and loop-command-reconcile 1,160 (1,518), every case still green.

160/09 · Small suite files share a unit

Short suite files share a process

The sharded runner packs whole files estimated under --pack-seconds (15) into units of about --pack-target-seconds (60), largest first, never splitting a file; this repository’s plan is 339 units (68 packed, holding 964 files) where it was 1,235.

Every account stays per file

The shard entry prints # file <path> <seconds> <executed> for each file it runs, import time included, so timings, lost-case detection and a not-finished report still name files; a failed packed unit is retried alone as a unit.

160/10 · The audits over this repository read once

The source scanner is linear

stripComments and blankStringLiterals decide regex-or-division from the token before the slash, never by re-scanning the prefix; their output over every .mjs under test/, packages/ and scripts/ is byte-identical to the previous scanner’s, and blanking the test tree takes 977 ms (10,116 ms before).

The tune corpus lists the stream once

assembleCorpus lists the stream once per assembly and hands each item’s row to the snapshot read, so one assembly over this repository takes 0.7 s (12.6 s before) and produces a byte-identical corpus.

Memory ingest lists the stream as often for one item as for many

aof work memory ingest hands each milestone’s listing row to the snapshot read, so its episodic indexing no longer lists the stream once per milestone (a fixture of 12 milestones is listed as often as one of 1; before, 14 times against 3).

160/11 · A test that needs a suite run starts the shard entry

A test that needs one suite run starts the shard entry

core-workspace, notify-channels, discord-bot and site-build’s gate cases start scripts/test-shard.mjs, not scripts/test.mjs --only; FF-16013 holds every spawn of the registry runner under test/ to an allowlist of one entry, test-command-contract’s bounded runner() child.

The Pages gate runs its one suite through the shard entry

.github/workflows/pages.yml runs node scripts/test-shard.mjs test/arch/loop/acd-loop-document-current.test.mjs under a fresh AOF_GLOBAL_HOME.

160/12 · Tests keep their state their own

A test run trusts folders in a claude home of its own

The claude folder-trust writer resolves its home from options.homedir, then AOF_TEST_CLAUDE_HOME, then os.homedir(); scripts/test-harness.mjs gives every test process a temporary AOF_TEST_CLAUDE_HOME, removed at exit, and the sharded runner gives each unit its own home, so a test run neither reads nor writes the operator’s ~/.claude.json.

A store opens without rewriting a current schema

The global work projection store and the effects journal migrate only an absent or older schema; opening one at its current version executes no write statement, still applies its pragmas, and a newer version is still refused.

160/13 · The whole tree runs on every pull request

The whole tree runs on every pull request, inside its budget

.github/workflows/test.yml runs every registered case on each pull request to main and each push to main, as 8 shards of 4 workers on ubuntu-latest; the summary job fails the run for a red shard, a case no shard ran or two shards both ran, an unfinished unit, a missing report, or a suite wall over 10 minutes. On PR #12 the suite signs off in 1.8 minutes.

The runner takes one shard of the plan

scripts/test-sharded.mjs --shard <i>/<n> runs one balanced bin of the whole plan, counts only its own cases, and --report-json writes its verdict for the summary.

Each file’s drift against main is reported

scripts/test-ci-summary.mjs compares every file’s time with the timings main last saved to the Actions cache and lists the files past the threshold in the job summary; drift alone never fails a run.

The control-node gate keeps no budget

.aof/aof.config.json declares no work.test.gate.budgetMinutes, so aof work regression-gate on the control node is a correctness gate under its own deadline.

The whole tree runs on Linux

Every registered case is green on ubuntu-latest; install.ps1’s extraction, file-lock and registry rows are Windows-only and skip elsewhere, flagged.

159 · The operator’s auto-memory is an opt-in memory source

Operator procedure recallable beside the work stream

On a node whose config sets memory.sources: ["claude-auto-memory"], one recall answers from both the operator’s Claude auto-memory and the repository’s lessons, each record citing its own source; the capabilities are stated whole in m159/01’s outcome (opt-in source, operator lessons, rebuild-faithful recall).

Node-local by construction

Operator records exist only in the git-ignored local index of the node that holds the Claude home, never in graph extraction or mesh transport, and an unconfigured node’s index is exactly the work stream (FF-15901..03).

159/01 · Recall the operator’s local auto-memory beside work-stream lessons

Opt-in operator auto-memory source

memory.sources: ["claude-auto-memory"] makes the next whole-stream ingest add the selected project’s Claude auto-memory entries to the index; an absent or empty setting indexes exactly the work stream, with no external read.

Operator lessons from feedback and project entries

Each feedback or project entry becomes one read-only lesson (owner operator, near-miss, source type kept as a tag, whole body searchable, locator is the entry’s own path), while user, reference, MEMORY.md and malformed entries are skipped with a named diagnostic that never reveals contents.

Separate, rebuild-faithful recall

Operator and repository records of the same fact both appear, each citing its own source; edits, deletions and disabling are reflected on the next full rebuild, a scoped rebuild stays work-item-only, and neither graph extraction nor mesh transport ever sees the entries.

158 · Native permission approvals

Native automatic permission review

Codex-driven phases select and verify native automatic review, allowing approved task operations to continue without another operator permission answer.

Attributable review lifecycle

Run observations retain native review identity, status and interruption evidence without storing grants or treating approval as successful execution.

Fresh verification after historical permission failure

A fresh loop can verify completed work with the selected model while preserving its build and earlier non-retryable permission failure.

157 · Episodic memory is recallable

What happened on an item is recallable through the memory seam

After ingest, recall "" --item <ref> --area delivery --all returns the item’s findings, raw feedback and terminal runs, each resolvable to its source (m157/01). The m148 retrieval eval stays green with the episodic pool ranked beside lessons and ADRs.

Three workflow edges read that history before they act

Continue on attempt 2+, verify at finding triage and the retrospective’s evidence step each consume one retained recall, as described in m157/03. The two decision edges raise the declared decision-recall roster from m153’s four to six.

157/01 · Episodic records resolve to their sources

Three episodic record types

aof work memory ingest compiles finding, feedback and run records from existing sources, on both the local and graphify backends.

Every episodic record resolves to its source

A record’s source is a line locator (<path>:<line>) or a JSON locator (<path>[:<line>]#<pointer>). readRecordSourceValue returns the line or the JSON value it names, and a locator that would leave the work tree is refused before any read. Over the live stream on 2026-10-12, all 1,226 episodic records and all 30 stall locators resolved.

157/02 · Every consumer accounts for the episodic pool

Status counts an episodic layer

aof work memory status puts finding, feedback and run under the episodic layer of the layer map. Both backends report the same partition, and every count sums to recordCount.

Recall returns an item’s whole history

aof work memory recall … --all returns every record in scope and overrides --limit; without it the limit still applies. A failed run’s block line shows its failureReason, and a lesson’s block line is unchanged.

Lesson-only consumers stay lesson-only

Tune’s corpus counts each lesson once over an ingested stream, and lesson recurrence never matches an episodic record.

157/03 · A retry, a triage and a retrospective recall what happened before

A retried build recalls its prior attempt

On attempt 2 or later, the continue procedure ingests, then recalls "" --item <ref> --area delivery --all --json. Before the first code change, it names the prior run (by retryOf, else the latest terminal earlier attempt), that run’s failureReason and the story’s findings in the milestone STATE.md. It submits one continue-episodic / developer observation.

Finding triage recalls the stream’s prior findings

The verify procedure ingests, then recalls "<finding text>" --kind finding --block across the stream before each triage decision. It cites a prior finding only on matching cause and evidence, never closes the new finding, and submits one verify-finding / verifier observation on the item being verified.

The retrospective’s evidence comes through recall

The retrospective procedure refreshes observability, ingests, and gathers findings, feedback and terminal runs through one --all recall of the item. When memory is off it reports the evidence as unavailable and does not fall back to reading the raw ledgers.

156 · One loop selects an assistant per phase

Model-only phase routing

aof work loop and aof work drive take models and efforts per phase (--model [PHASE=]MODEL[:EFFORT]) and infer each phase’s assistant from the model; no runtime flag exists on either command.

One pinned mixed plan per loop

A loop records one execution plan naming each phase’s runtime, model and effort; refine, continue and verify run on their own assistants in one command, and review and repair inherit the continue selection.

Recovery keeps the recorded selection

Resume, retry, lane reopen and worker handoff run each phase on its recorded assistant, model and effort, refusing conflicting flags and never rewriting a recorded failure.

Pre-launch refusals

An unknown, ambiguous or unavailable model, an unsupported effort, or a missing/incompatible Codex asset is refused before any phase run is minted, naming the cause and remedy, with no fallback to another assistant.

Neutral phase settings in the editor

The configuration editor saves per-phase models and efforts without assistant selectors and preserves unrelated settings.

155 · One agent-mode setting governs every session

The agent-mode chain and its default have one home

@aof/contracts/agent-mode (packages/contracts/src/agent-mode.mjs) exports AGENT_MODE_DEFAULT = "solo", agentModeFromConfig (work.agents.mode if valid, else solo) and sessionAgentMode (loop key, else work.agents.mode, else solo; null for verify).

The loop’s drive follows work.agents.mode when no loop key is set

aof work drive refine|continue composes --solo or --orchestrated from sessionAgentMode. loop-bounds.mjs no longer carries LOOP_AGENT_MODE_DEFAULTS, and loopAgentModeFromConfig answers the loop key alone (or null).

Every role-spawning prompt defaults to solo

Refine, continue, review, assimilate-code, migrate and autonomous state that an unset work.agents.mode resolves to solo. --solo and --orchestrated still override for one run. Verify reads no mode.

The schema and renders state the one default and the chain

schemas/aof.schema.json, the manifest, the lock and the Claude, Codex and OpenCode renders agree with the source prompts.

A per-role map is reported inert under the effective solo mode

aof project validate emits model-map-inert-under-solo / effort-map-inert-under-solo whenever the effective mode is solo, including an unset work.agents.mode.

154 · Codex drives the AOF loop alongside Claude

Two assistants share one governed loop

AOF’s existing work loop can execute and recover native Claude and Codex phases using one retained runtime choice, assistant-specific assets and common work-item gates; existing projects retain Claude as their default.

154/00 · A shared session boundary preserves Claude execution

Runtime session boundary

The existing Claude execution driver and the Codex adapter share one normalized session interface while the existing loop retains sequencing and retry policy.

154/01 · Runtime and model choices survive resume

Durable execution choice

Runs and assignments retain the resolved runtime, profile, phase models and effort; resume rejects a conflicting selection and legacy records retain Claude behavior.

154/02 · Codex executes a bounded phase through App Server

Bounded native protocol

The admitted Codex App Server profile records native threads, turns and normalized results, persists blocking questions, and bounds protocol queues, deadlines and owned-process shutdown.

154/03 · Codex discovers correctly scoped native assets

Native discovery outputs

AOF renders Codex skills, role configuration, scoped guidance and supported settings into their native discovery locations without changing the Claude render target.

154/04 · Codex upgrades preserve operator-owned files

Ownership-safe native upgrades

Recorded ownership and drift checks govern Codex asset updates and legacy cleanup while preserving operator-owned settings, guidance and neighboring files.

154/05 · Shared workflows render assistant-specific instructions

Runtime-specific workflow assets

Bundled runtime variants and project overrides supply native procedures, role settings and explicit delegation rules while preserving shared scope, ownership and verification obligations.

154/06 · Codex drives and recovers the existing work loop

Native governed phases

The existing loop drives Codex phases with driver-owned run identity, durable question delivery, pinned execution settings, bounded warm fixes and operator cancellation.

154/07 · Workers retain the selected runtime and assets

Durable worker handoff

Worker assignments retain their execution envelope across reconnect, prepare lock-owned native assets and start the lane view through the guarded status transition while retaining one primary-owned run.

154/08 · Codex activity and usage are reported honestly

Attributable native observations

Codex activity and token counters are joined to captured run, thread and turn identities; unavailable monetary cost and unsupported transcript measurements remain explicitly unavailable.

154/09 · Codex projects can choose an explicit memory backend

Explicit memory dependencies

Configuration and inspection expose the memory backend and its extraction dependency, including the local backend that does not launch Claude.

154/10 · The config editor explains effective assistant settings

Execution configuration editor

The configuration editor validates and persists runtime and scoped model settings, shows their effective selection, and retains keyboard and responsive behavior without executing the work loop.

154/11 · Live acceptance proves Codex and preserves Claude

Reproducible native acceptance envelope

The runtime verification fixture and recorded live evidence distinguish preparation, scripted transport, actual model execution, recovery and final acceptance for the admitted Codex profile and existing Claude driver.

153 · Memory closes the loop

Recall to recurrence is one traceable chain

At milestone level, the system can now answer whether a stored lesson was surfaced, whether the decision it informed honoured it, and whether the mistake came back. The chain has four links:

An operator traced one lesson end to end, from ledger and source alone, at the @uat sign-off.

153/01 · Recalls have complete verdict records

Recall observations in the item ledger

aof work memory record-recall <ref> --input <file> appends one kind: recall row to the item’s FEEDBACK.ndjson. The row carries the query, the scope, and an explicit honoured | departed | irrelevant verdict for each returned record. A resubmission of the same id is idempotent (replayed: true), and a conflicting one changes no byte.

Raw feedback stays raw

The finding-escape counter and every other ledger consumer select their partition explicitly. Recall rows never enter the raw-feedback denominator (FF-15301), and aof work feedback refuses recall fields.

brief is retired

aof work memory brief is an unknown verb, refused before any backend is resolved. Help no longer lists it.

153/02 · Lessons name recurrence and promotion

Forward lesson metadata

From each retrospective’s promotionFrom watermark onward, a lesson’s meta line carries **Promoted to:** control:<id> | rule:<path> | prompt:<path> | none and an optional **Recurs:** <item>/R<n>. Both are held by the memory-vocabulary validate rule and indexed into the lesson’s status and tags. Lessons below the watermark are never rewritten.

Calibrated recurrence matching

aof work memory recurrence "<text>" --exclude <item>/R<n> --json scores the text against every indexed lesson, across kinds. It names qualifying only when a score is strictly above threshold 0.23 (calibration 153.02-1). The result always carries the threshold and the calibration version. The matcher shares no code with retrieval, so base ranking is unchanged (FF-15302).

153/03 · Memory reports effectiveness and health

Effectiveness accounting

aof work memory status --json reports an effectiveness block. It gives surfaced, honoured, departed, irrelevant and recurred counts per record type, and per lesson with --lessons. It is built from the ledgers’ recall rows and the retrospectives’ Recurs stamps, and it flags corrupt or unresolved evidence in diagnostics and complete.

Recurring and aged-unpromoted doctor probes

aof work doctor reports memory-lesson-recurs for a lesson named in a later Recurs. It reports memory-lesson-unpromoted-aged once an unpromoted lesson has seen 5 or more later accepted milestones (the operator’s choice, configurable). An unknown age gets its own finding. All three are advisory and never fail a normal doctor run.

153/04 · Workflow edges close their memory observations

Captured decision-recall edges

Four edges submit one record-recall observation each, with their own edge, actor and scope: shatter (shatter-po, owned by the first driver, never replayed per driver), refine architect and PO (refine-architect, refine-po), and continue (continue-developer, near-miss scope). The Claude, Codex and OpenCode procedures do this, and an empty recall is still submitted with records: []. FF-15303 holds the roster at exactly these four edges, with render parity.

Retrospective records recurrence and promotion

The retrospective procedure writes the promotionFrom watermark first. For each new lesson it runs memory recurrence before writing, stamps Recurs only on a qualifying match, gives every new lesson an honest Promoted to, writes no document for a clean pass, and ingests only after authoring.

153/05 · Tune proposes promoted prompt corrections

Memory evidence in the tune corpus

aof work tune reads each selected item’s recall observations and the retrospectives’ Recurs stamps. Each candidate shows surfaced, honoured, departed and recurred counts, with citations to its ledger rows and source lessons. Observations from one document do not count as a second source document.

Computable prompt patches

When a recurring lesson has a prompt:<repo-path> destination, the prompt-or-brief-revision proposal carries a concrete from/to patch. It appends a deterministic `` block that quotes the lesson. The proposal stays advisory with no applier. A block already in force is reported as already-in-force. Targets outside the repository, symlinked out of it, or unreadable are reported as findings before any content is read, and the run writes no byte (FF-6207).

152 · Promote shows what to promote next

Promote candidates listing

aof work promote --show-candidates (and --json) lists every backlog item a named promote would accept now. The order is most backlog items unblocked (directly or transitively), then oldest created:, then group path and slug. Each waiting item is listed after them with the gate’s own offenders. The command writes nothing.

Promote the head

aof work promote --next-item [--at <P>] [--yes] promotes the head of that listing through the same promoteRow path a named promote takes. It refuses promote-no-candidates, with nothing changed, when the list is empty.

Promote argument conflicts refuse early

A slug, --next-item and --show-candidates are mutually exclusive, --at/--yes do not combine with --show-candidates, and any flag-shaped argument the verb does not know refuses as promote-flag-conflict before the work tree is read.

/aof:promote offers both modes

The command drives --show-candidates --json and --next-item on the verb’s machine face and never chooses an item by reading the backlog itself.

Claude commands carry their argument hint

Every rendered .claude/commands/aof/*.md carries its asset’s argument-hint as one JSON-quoted frontmatter value. Codex carries it in its usage line, and OpenCode copies carry none because their frontmatter has no such field.

151 · aof:add-diagram draws the architecture diagrams a refine left undrawn

/aof:add-diagram draws an item’s undrawn ADR diagrams after refine

/aof:add-diagram <ref> [ADR-NNN] ships for Claude, Codex and OpenCode. With no ADR named, it lists every ADR whose ### Diagram brief has no diagrams/ link and draws each in turn. With an ADR named, it draws that one only, first writing its brief from the ADR’s own text when it has none and the item is not done. An item with nothing undrawn gets “nothing to draw” and no file changes.

It reuses refine’s diagram step unchanged

Each ADR goes through aof diagram plan → the plan’s instructions → aof diagram export → the session pastes the returned block under the brief. enabled: false, available: false, diagram-item-delivered and a PNG miss keep the meanings they have in refine. There is no new CLI verb, and refine is unchanged.

150 · aof:explain says what a work item is for, without writing anything

/aof:explain answers what work items are for

/aof:explain <ref…> [--verbose] ships for Claude, Codex and OpenCode. For each ref, in the order given, it prints a three-to-five-sentence purpose read from the record (a story’s user story, a milestone’s objective, a spike’s question, a chore’s intent, a uat’s scope). It marks backlog and archived items, reports a ref that matches nothing, lists the matches of an ambiguous one without explaining them, and reports an unwritten purpose as not written down. A milestone’s short answer counts its stories. --verbose adds scope, stories or tasks, depends: edges and what is still open.

Asking writes nothing

The command runs only aof work find, doc, list and tasks plus Read. It mints no run, moves no status, stamps no updated: and writes no file, and a live run leaves git status unchanged.

aof work find resolves a folder path

findWork resolves a query holding / or \ (that is not a pair or a span) as a folder path from the caller’s directory, with a trailing record doc naming its folder. Only an exact item folder matches, so wiki/work/backlog answers []. Every reader that resolves through findWork (aof work doc, aof work tasks, …) takes a path, and every form that resolved before answers as before.

149 · Manual mode: continue guides the operator who builds the work themselves

Manual continue

aof:continue <story|task> --manual starts the story through its run, runs the story’s tests once, prints a terminal-only guide (what is still red, the user story, each task’s scenarios, reads:/files: with why each matters, the tests and the gate command, the plan, and an order), spawns no builder, and hands off to aof:review <ref>.

Manual is one story, at every door

--manual is refused when combined with --solo/--orchestrated, refused for a milestone or span, and refused by the CLI door for a non-story ref (continue-manual-not-a-story) or a remote decision (continue-manual-remote). It is a per-run flag only, so the loop never composes it.

aof:review

/aof:review <ref> reviews a story the operator built. It runs the story’s tests, continue’s gate ladder, a blast-radius ranking and continue’s architect and QA lenses, fixes nothing, reports every finding to the operator, and closes its run done. With no Blocker it moves the story to in-review and names aof:verify <ref> next.

aof:code-review removed

The bundle has no code-review command, aof:autonomous has no --ship, and work.codeReview.autoComplete is gone. aof work update deletes the three code-review renders from a repository whose lock records them, and assimilate-code hands off to aof:verify.

148 · The memory corpus holds its vocabulary

Every lesson is reachable by the kind it states

Across the whole corpus – milestone and story retrospectives, live and archived – a lesson that states a kind is indexed under its enum word (m148/02, m148/03), a live lesson cannot be written outside the vocabulary (m148/04), and memory status shows what is left blank or non-enum per field (m148/05).

Signing off an item reads no other item’s state

The whole-tree suite asserts nothing about which items are archived, where a gate sits, the citations inside done records, or an archived milestone’s ledger; a citation sweep reads only the documents of items that are not done (F-148-06).

148/01 · The ranking is held by an eval

Retrieval eval over the live corpus

FF-14801 (test/arch/memory/acd-memory-retrieval-eval.test.mjs) builds the records in memory from the real wiki/work and holds 25 cited (query, scope, <item>/<id>) pairs within the first five of rankRecords, the five lines an agent’s --block shows. A red names every lost pair with its query, scope and the rank it was found at, and a record that no longer exists reds as gone.

148/02 · A lesson’s meta line is normalised on read

One home for the memory vocabulary

@aof/work/memory-vocabulary (packages/work/src/memory-vocabulary.mjs) is the only source that spells the meta-label grammar, the Kind, Area and Stage enums and the three gap statuses. FF-14802 holds that, and holds the retrospective prompt’s lists and the OUTCOME template’s status comment to the same words.

Meta values normalised on read

The one retrospective parser indexes a lesson’s Kind, Area and Stage, and a gap’s status, as the vocabulary word the written value starts with, and keeps the rest as a tag. A value outside the vocabulary is indexed as written, and a blank stays blank. Title, summary and text are unchanged. On the live corpus (489 lessons) non-enum Kind falls from 65 to 53, Stage from 88 to 15, and gap status from 12 spellings to 3.

Every record carries tags

Every MemoryRecord has a tags array ([] when there is none) holding the qualifiers stripped from kind, area, stage or gap status, in that order. INDEX_VERSION and GRAPHIFY_INDEX_VERSION are both 2.

A store built before version 2 is reported stale

Each backend’s status carries index: { version, current, stale }, and a stale store names aof work memory ingest as the fix.

148/03 · Story retrospectives are indexed

Story lessons are recallable

Every item’s RETROSPECTIVE.md is indexed, whatever the item’s type. A story’s lessons carry its ref as item (134/01), so they are cited as (m134/01), and --item 134 reaches them. On the live corpus the lesson count rises from 489 to 788, and every one of the 299 new lessons comes from a story folder.

148/04 · A live lesson’s meta line is held

Validate holds a live lesson’s meta line

aof work validate errors on a live item’s lesson when its Kind, Area or Stage does not start with a vocabulary word, or its Owner is blank. The finding names the file, the R<n>, the field, the value and the legal values. A qualifier after the word is legal, and a lesson with no meta line fails on all four fields.

Doctor flags an archived lesson’s meta line

A doctor lane gives one lesson-meta-archived warning per archived retrospective that holds a non-conforming lesson, naming the count and the ids. It never gates, and no archived retrospective is rewritten. On the live tree it reports 94 files.

The live stream conforms

Every lesson in a live retrospective passes the hold. The 29 lessons in 14 retrospectives that failed it now keep each written word as the qualifier after a vocabulary word, and 145 and 146 carry meta lines authored from their lessons’ text.

The retrospective prompt states the hold

Step 4 of /aof:retrospective says that validate holds Kind, Area and Stage to the listed words with a qualifier after the word, and that Owner must be present. Its lists are the vocabulary module’s (FF-14802), in the asset and in the rendered .claude, .opencode and .codex copies.

148/05 · Memory status reports conformance and layers

Status names every record type’s layer

aof work memory status, on both backends, lists every record type with its count and the layer it serves from RECORD_TYPE_LAYERS, the one partition of record types: semantic holds adr, capability, gap and summary, and procedural holds lesson. A type missing from the map is counted as unmapped and is still listed. The type counts sum to recordCount (FF-14803).

Status reports vocabulary conformance

status reports blank and non-enum counts for Kind, Area and Stage over lessons, blank Owner, and non-enum gap status. Every count is nested, so status gains no top-level number.

The block line shows tags

A tagged record’s --block line carries [t1; t2] between its title and its source. An untagged line is byte-identical to the line before this story.

147 · The loop hands a halt to a fresh session to fix, then resumes

Lane halts are handed to a repair session

A foreground aof work loop handles its three lane stops (lane-open-failed, lane-merge-refused, lane-merge-conflict) by writing a hand-over file under the aof home (mesh/loop-repairs/<runId>.json) and driving a fresh Claude session typed /aof:repair <ref> <file>. Every other stop still ends the loop. --no-repair or work.loop.repair: false turns the hand-over off.

One repair per halt, then the loop resumes or stops

The repair is a run on the halted ref, with brief.loop.phase: "repair" and the halt’s stop and producer in its brief. It is settled from the session’s outcome. When it ends done, the account prints Repaired <stop> at <ref> (run <id>) – resuming <scope>. and the loop re-enters its body in process, on the recorded models and efforts. Otherwise the original halt is re-printed with the repair’s facts appended to its Details, and the loop stops.

work:drive-repair and /aof:repair

Repair is the fourth drive phase, and its session resolves as continue. The /aof:repair bundle command renders for Claude, Codex and OpenCode. It fixes only the loop’s own records, never discards a commit, never touches the operator’s dirty paths or a delivered .feature, and never starts a loop.

The two known lane-halt causes are gone

runs/.heartbeats.ndjson queues are git-ignored, are never captured by the one commit verb (lane and mesh worker alike), and are untracked where an earlier commit tracked them. STATE.md under the work dir merges by union (wiki/work/.gitattributes), so two lanes’ build notes no longer conflict.

146 · A capture can skip the backlog

--in-stream on the five top-level add commands

aof:add-milestone, add-story (standalone), add-chore, add-spike and add-uat accept --in-stream anywhere in their arguments. With it, the capture is scaffolded under backlog/ and then aof work promote <slug> --json runs at once with no position, so the item lands numbered at the tail whatever work.intake says. A promote refusal is reported as a stop and leaves the item in the backlog.

The backlog intake is unchanged without the switch

Under work.intake: "backlog", a capture without --in-stream stays un-numbered in backlog/, and aof:promote <slug> is still the next step.

145 · A milestone’s loop plan can be drawn

The loop’s wave plan, replayed

planLoopWaves in packages/work/src/ready-wave.mjs replays a milestone through nextWork (throughReview) and partitionReadySetByDeclaredFiles. The result is waves with members, held members and their reasons (files-overlap, write-set-unknown, after-unknown), depends: edges, the lane bound, waitsForLane marks, built flags from each story’s real status, and one stated assumption.

aof diagram plan <ref> loop

The command writes execution/loop-plan.json for a refined milestone under work.loop.concurrency: refine_first and answers the diagram engine’s drawing instructions. It refuses with loop-not-a-milestone, loop-not-refine-first or loop-not-refined, writing nothing.

aof diagram export <ref> loop

The command writes execution/loop.png from the drawn execution/loop.html through the generator’s toSvg and the rasterizer, and keeps no SVG.

/aof:loop-diagram

A bundle command (claude, codex, opencode) that plans, stops on any refusal, draws loop.html in the operator’s session through the engine’s skill, then exports. Built stories are drawn in the diagram style guide’s green done role.

144 · The whole-tree test run signs off in minutes

The regression gate runs a declared whole-tree program

aof work regression-gate <ref> runs work.test.gate.args when it is declared, at scope all. This repo declares scripts/test-sharded.mjs. --jobs N expands work.test.gate.jobsArgs, and --serial runs the plain work.test program. Conflicting or invalid settings are refused before anything runs, and a malformed declaration is a red row naming work.test.gate.<key>.

A gate row says what was lost, what is not isolated, and how the run ran

The row’s detail is the failing cases, then not isolated: …, then <mode>[ --jobs N] · <min> min[ · over budget (<B> min)]. A green row carries a detail too, and still satisfies the door. After its verdict the gate prints the program’s # slowest files block and its Logs: path.

The sharded runner reports through one pure module

scripts/test-sharded-report.mjs owns the report and the exit decision. A case red twice, or a unit that ran fewer cases than it was assigned, is a column-0 not ok. A case red in the pool and green alone is # not isolated - <case> and does not fail the run unless --strict. The slowest-files block sums each file across its chunks into .tmp/test-timings.json.

143 · The loop starts from the backlog, refines a whole item in one pass, and runs each phase on the model the operator chose

One loop command from backlog idea to built work, on the operator’s models

aof work loop <backlog-slug> --refine whole-item --model refine=opus:xhigh --model verify=fable:high promotes the item, breaks it down and authors every contract in one refine session, then builds. Each phase’s session runs on its own model and effort, and each run’s declaration says which and from where. The parts are 143/00 (promotion), 143/01 (whole-item refine), 143/02 (the grammar) and 143/03 (the per-phase lend and record).

The loop’s run choices survive a resume together

The declaration a resume reads carries the scope’s origin (promotedFrom), the refine scope (refine) and the per-phase sessions (sessions). aof work loop <NN> --resume therefore continues a promoted item at its number, in the same refine mode, on the same flag choices, with no operator input.

143/00 · The loop promotes a backlog ref

A backlog slug is a loop scope

aof work loop <backlog-slug> resolves the scope with resolveItemExact. It promotes a backlog row through the registered work:promote alone (FF-14301), narrates Promoted <slug> → <NN>. and runs at the minted number. A promote refusal is the loop’s refusal, with nothing minted.

The declaration names where a promoted scope came from

Every run the loop mints carries promotedFrom: the slug, or null for a numeric scope. A declaration written without the key, by an older build or by a caller that does not pass it, reads back with promotedFrom: null.

The read-only doors never promote

--dry-run, an L1 report and a --json probe on a backlog slug answer wouldPromote and write nothing. --stop, --hand-off and --resume refuse a backlog slug with loop-backlog-ref-not-running.

143/01 · A whole-item refine in one drive

The refine scope is a loop setting with a per-run flag

work.loop.refine (per-story by default, or whole-item) lives in the loop’s bounds leaf as one frozen member list (FF-14302). aof work loop --refine overrides it per run. The resolved value is the declaration’s refine key, a resume inherits it, and every LoopState answer carries it.

Under whole-item the break-down drive is the whole cascade

For a milestone with no stories, the loop’s refine drive composes /aof:refine <ref> --solo --autonomous. --autonomous reaches the session through both drive seams (in-process and the child’s argv). A re-entered break-down refine keeps it, because the decision and the re-entry share the engine’s isWholeItemCascade. aof work drive refine <ref> --autonomous is the same drive from the CLI.

per-story is today’s loop

Under the default, every decision and every composed prompt is byte-identical to the pre-143 loop.

143/02 · One grammar for per-phase session choices

One grammar for --model and --thinking

parseSessionChoices in packages/execution/src/session-model.mjs reads --model [<phase>=][<model>][:<effort>] and --thinking [<phase>=]<effort>. It splits a model from its effort on the last : only when an effort spelling follows, so a Bedrock-style …-v1:0 id stays a model. An unknown phase, an unknown effort, an empty choice or two values for the same part of the same phase are refused (session-choice-unknown-phase, thinking-unknown-level, session-choice-empty, session-choice-conflict). A phased value beats an unphased one.

Each part of a phase’s session resolves on its own, and says where it came from

resolveSessionLaunch resolves the model and the effort separately: the flag, then work.agents.session, then the default (high, and no model). It reports modelSource beside effortSource. parseSessionChoices and resolveSessionLaunch are defined only in the session leaf (FF-14303).

A string flag can be given more than once

A CLI flag declared repeatable: true collects every occurrence into an array. An inline --flag=a=b value keeps everything after the first =, including in the two hand-rolled parsers in knowledge/src/memory.mjs and mesh/src/commands/session.mjs.

143/03 · The loop runs each phase on the chosen model

The loop takes a model and an effort per phase

aof work loop accepts a repeatable --model [<phase>=][<model>][:<effort>] and --thinking [<phase>=]<level>, parsed by 143/02’s grammar before anything is read or written. A refused choice stops the loop at its door with no declaration, no run record and no session.

The run record says what each phase ran on

The loop resolves all three phases once. Each run’s declaration carries sessions, one { model, modelSource, effort, effortSource } per phase, and every LoopState answer carries the table. thinking keeps 141’s meaning (the unphased level or null). Before the first drive the loop narrates Sessions: refine … ; continue … ; verify … ., and the dry-run probe carries the same table.

Each drive runs on its own phase’s choice

Each drive’s session gets its phase’s model and effort, read off the declaration by one engine function (sessionLendFor) at all three seams: in-process, the primary’s child, and a wave lane. A child gets them as --model / --thinking on its argv. aof work drive <phase> <ref> --model <choice> is the single-phase door.

A resume reruns on the recorded choices

aof work loop <ref> --resume re-applies the recorded flag choices and re-resolves the config and default entries. Any new session flag replaces the recorded flag set as a whole.

141 · Subagents and loops think at a chosen effort

One effort vocabulary

normalizeEffort() in src/session-model.mjs is the only reader of an effort level. It accepts low, medium, high, xhigh, max, reads extra-high as xhigh, and refuses anything else. Every door that takes a level calls it.

A driven session thinks at high unless told otherwise

resolveSessionLaunch answers effort + effortSource from --thinking, then work.agents.session.effort.<phase>, then high. Every spawned claude carries --effort <level> and none of the inherited effort variables. aof work drive <phase> --dry-run --json shows the effort it would launch at.

aof work loop –thinking reaches every drive

--thinking <level> on the loop overrides every phase for that run. It reaches sequential child drives, wave lanes and in-process drives, is narrated before the first drive, and is stored as the declaration’s tenth key, so --resume and a supervisor relaunch inherit it. An unknown level is refused as thinking-unknown-level before anything is written.

A role can pin its own effort

work.agents.effort (role → level) renders effort: after model: in .claude/agents/aof-*.md, for pinned roles only. An unpinned role carries no line and inherits its session’s effort. aof project validate checks the map the way it checks the model map.

An operator’s session in an aof project defaults to high

aof work update fills effortLevel: "high" into .claude/settings.json when the document has none. It never overwrites an existing value, and settings.claude.effortLevel in the project config still wins.

A phase command’s –thinking stops and names /effort

/aof:refine, /aof:continue and /aof:verify given --thinking <level> stop before any run or role. They print the /effort command to run and state that the session’s effort was not changed.

The run record shows the effort a session ran at

The spend ingest reads the record-level effort Claude Code writes on each assistant turn, so spend.effort names the level instead of "unknown".

140 · Refine runs solo unless told otherwise

A hand-run refine plays its roles inline unless configured otherwise

With work.agents.mode unset, /aof:refine resolves to solo and /aof:continue to orchestrated. A set key governs both commands, and --solo / --orchestrated override it for one run.

An orchestrated refine gives each story one QA agent

Under orchestrated mode, one aof-qa writes the Examples tables for all of a story’s tasks. The prompt forbids splitting that pass into one agent per task.

The loop composes a mode flag on every refine and continue it drives

LOOP_AGENT_MODE_DEFAULTS in src/loop-bounds.mjs is { refine: "solo", continue: "solo" }. aof work drive composes work.loop.agents.<phase>.mode when it is set and --solo when it is unset, and never reads work.agents.mode. verify still carries no flag.

The per-key loop resolvers still answer null for an unset key

The default is applied only at the phase level (loopAgentModeFromConfig), so the registry’s range probe and the tuner see an unchanged null.

This repository and the operator’s ten others follow the defaults

None of the 11 .aof/aof.config.json files pins a mode key. Each one’s rendered prompts state the new defaults, and a following aof work update --dry-run reads 0 updated and 0 drift-warning.

139 · Shatter lands its drivers in the backlog

Shatter frames into the backlog

aof:shatter writes every milestone and spike it frames un-numbered under backlog/[<group>/], each dependent carrying a backward slug depends: edge in PRD order. Under work.intake: "stream" (or no key), shatter then promotes each driver in PRD order through aof work promote, so every stream number is minted by the verb and none by the prompt.

Promotion enforces and resolves a backlog slug edge

aof work promote refuses promote-depends-backlog for an item whose depends: names a backlog slug. Promoting the target rewrites each entry naming its slug, in every other backlog row, to the minted ref: per entry, after the --at seam, and only for a slug unique in the backlog. The envelope reports the rewritten docs as rewired, a key present only when non-empty.

Validate checks backlog slug edges

aof work validate reports a slug entry on a backlog row that names no backlog item, adding the written-as number when the slug belongs to a stream item. It reports each backlog slug cycle once, at <work>/backlog, whatever the scope. Numeric entries on backlog rows stay unchecked.

One number/slug predicate

isDependNumber in src/work.mjs is the one all-digit test. The top-level shift, promote’s classifier, the shared per-entry rewriter rewriteRefEntry and validate’s numbered path all read entries through it, so a digit-led slug (10x-faster, 007-bond) is never shifted, resolved, graphed or zero-padded as a number.

138 · The session driver sees claude’s screen

A driven session’s screen is read, not inferred

When to type, what is blocking and what to record at a stop are all decided from one rendered screen per session (m138/00, m138/01). The three failures measured on 2026-09-24 were each observed closed on a real claude on two nodes (m138/02): a paste into a TUI that was not listening, a dialog idling to the 20-minute deadline, and a failure that left no screen.

Readiness holds on both of claude’s renderers

The input box is recognised on the fullscreen renderer’s alternate buffer and on the classic renderer’s normal buffer, so a drive launched after an unfinished fullscreen boot, or under /tui default, types on the box rather than timing out (ADR-002 §1 as amended at verify, m138/F-03).

An archived item’s citations resolve through the archive rule

aof work validate’s reads: check resolves a missing <work.dir>/<item>/… through <work.dir>/archive/<item>/…. An item created and archived inside one squash therefore no longer dangles on main (m138/F-16).

138/00 · The driver reads the screen

One screen model per driven session

Every interactive claude session the driver launches carries a @xterm/headless 6.0.0 model, fed from the session’s own PTY output and sized to it (80×24, scrollback: 0). src/terminal/screen.mjs is the only module that imports the package (FF-13801).

Typing waits for the input box

The directive is pasted on the first settled frame whose cursor row starts with ❯ at column 0 between two full-width ─ rules, on either buffer (ADR-002 §1 as amended at verify). If a model is live and no such frame arrives by the 60 s cap, nothing is typed, and the session stops failed / timeout with the screen recorded as screen-not-ready.

Every verdict the screen gives is acted on, by the door’s word alone

The driver acts on four verdicts from src/terminal/session-screen.mjs: type; consent (a menu walked one confirmed key at a time, then one Enter); blocked (failed / blocked_screen, with screen: { id } on the result); and wait (the heartbeat deadline suspended). The driver reads no screen itself.

Every stop but done leaves the screen

Every timeout, death, unaccepted directive and needs-input stop writes one degrade event carrying the rendered rows, the buffer and the cursor, taken before the kill. reportDegrade throttles per (code, key).

The byte gate is the fallback

With no model (the package absent, or failing to load), readiness is the pre-138 byte gate, verbatim: the floor, then bracketed paste ON with visible output since. screen-model-unavailable is logged once.

138/01 · Blocking screens are named

Six recorded screens, each with one action

src/terminal/claude-screens.mjs registers ready (type), trust (consent), mcp-approval, first-run and login (fail), and usage-limit (wait). Every entry is proved on every run against a recording committed under test/fixtures/claude-screens/, and ready claims no dialog (FF-13802).

claude 2.1.283 opens the trust dialog on ❯ No, exit. The door presses Down one confirmed step at a time until Yes, I trust this folder is highlighted, then sends one Enter. An arrow the screen does not show within 2 s gives the consent up by name.

A blocking screen is named in seconds, and the loop says which

A session on MCP approval, the first-run theme picker or the login menu stops failed / blocked_screen on the first settled frame that shows it, and is not retried. The loop’s lane narration reads settle: failed (blocked_screen: <id>), and its halt line carries screen=<id>.

138/02 · A live session proves it

The deployed driver is observed working on both nodes

On this node’s payload and the WSL node’s tree, a drive under an empty CLAUDE_CONFIG_DIR stops failed / blocked_screen with first-run within 2 s of launch. A loop over the same fixture halts run-not-retryable with screen=first-run. A real drive pastes its directive on the input box 4.7 s after launch, its session id is captured, and its cancel leaves the REPL frame in the degrade log, with no fallback code.

Every driven session declares the terminal the PTY is

Each interactive driven session launches with TERM=xterm-256color, the terminal the PTY and the screen model emulate, whatever the launching shell carries (m138/F-01).

The WSL deploy carries the lockfile, and a failed install is not stamped

scripts/deploy-wsl.sh syncs package-lock.json with package.json. A failed npm ci exits 1 and leaves .aof-wsl-deploy as it was (m138/F-02).

137 · AOF.md has a template like every other record doc

The AOF.md digest has a shipped template

src/bundle/templates/milestone/AOF.md declares the digest’s eleven frontmatter keys in order (source and importedAt marked # OMIT) and its four sections, Intent, Scope, Decisions and Lessons, and aof work update installs it at .aof/templates/work/milestone/AOF.md; no native scaffold writes an AOF.md.

Every imported digest is rendered from that template by one renderer

src/work/digest-template.mjs renders it for both the co-located write and the legacy-store path: keys in template order, a missing OMIT key dropped, sections in template order, a half that was not recovered left out, and schema/aofVersion stamped, so a fresh import never shows as stale.

Validate holds every digest to the template’s closed key and section sets

A digest record doc gets an error for a missing required key, an unknown key, an unknown section, a section out of order or a section that appears twice; the sets are parsed from the shipped template, not listed a second time.

136 · Discovery questions in the loop

A loop-driven refine settles its business rules with the operator, end to end

A refine that aof work loop drives asks each open business question as a tokened discovery question (m136/02), the loop posts it and waits in that lane while the session is recorded as waiting (m136/03), the operator’s answer through any channel 131 records is typed back and written into the map, and the example it settles reads stated or confirmed with the loop’s answer record as its anchor (m136/01) – measured live on the test-bed, where 08/00’s Q1 was asked, answered … inside max, anchored, and formulated into the contract.

Every driven ask reaches the operator on the current Claude Code

Not only discovery: any AskUserQuestion a driven session makes, at refine, build or verify, is detected and posted through the recorder hook (m136/03).

The whole-tree gate resolves 142’s moved and split modules

The rename ledger records PR #5’s 1,106 renames and 311 derived forwards, so a citation of a module 142 moved or split behind a forward resolves through history (FF-11903, FF-5204, reads:), and the unresolved-citation count holds at its ceiling of 55.

136/01 · A loop answer anchors the example

An answer given through the loop is a person’s answer to its map token

collectAnswers reads the asks entries of a story’s and its parent’s runs, in any state, beside the harness transcript: an answered entry whose question opens with exactly one map token yields a record with that token, the answer verbatim, the run’s session, answeredAt and the entry’s by, so the doctor lane and the build door accept a stated or confirmed example anchored by it.

An ask that cannot name its one token anchors none

An entry with no token at its head, a second token on a later line, or no answer yet yields no record.

136/02 · A driven refine asks through the loop

A driven refine asks each business question as its own tokened discovery question

In a session whose environment carries AOF_RUN_ID, refine.md’s discovery beat asks one question per AskUserQuestion call: its token first, the discovery marker, the rule and the example it settles, then 131’s four lines, with the options as the tool’s; a business question is never defaulted and never sent as the NEEDS_INPUT sentinel, and the answer is written into the map on resume.

A driven cascade asks one question after another

The --autonomous block asks a driven session’s open business questions one per call, each its own ask and wait; an interactive cascade still asks in batches of four.

136/03 · A pending ask is read from the hook

A driven session’s pending question is recorded before it is shown

The bundled PreToolUse hook claude-ask-pending, matched to AskUserQuestion alone, appends the call (runId, sessionId, toolUseId, input, at) to <item>/runs/.asks-pending.ndjson, prints nothing and exits 0 on every path.

The loop sees, posts and answers a question the transcript does not show

The driver settles needs-input on a record written during its drive whose call has no result in the transcript; the loop’s owner and the mesh worker read the question from it first; the re-drive types You asked:, the question, The answer: and the answer, while the run records the answer verbatim and a transcript question’s answer is still typed verbatim.

135 · Key examples in the contract

What a person agreed in discovery reaches the executable contract and stays there

With work.examples.enabled on and an applicable map, refine writes a story’s contract as one Rule: R<n> · … per map rule, with the key examples as headline Scenario: E<n> · … under it (m135/05). The parser reads the rules (m135/02), and the board shows them (m135/03). An agreed example that leaves the contract is an example-untraced error, and the build stays refused until the example is restored (m135/04). All of it ships from @aof/specification-by-example (m135/01).

135’s own contracts are the trace’s first real subjects

Stories 03, 04 and 05 are formulated in this form from their own maps, so the live work stream carries ruled contracts. 135/04’s stated E8 is traced at accept: delete its scenario and the doctor names it.

135/01 · The practice is its own package

Specification by example lives in @aof/specification-by-example

The example map grammar (map), the answer reader (answers), the examples doctor lane (doctor-lane), the continue door’s check (build-door) and the story probe (story-probe) are the package’s five exports. It depends on @aof/contracts and @aof/work alone, and core composes it into the work stream when work.examples.enabled is on.

@aof/work offers seams that never name the practice

createWorkDoctor takes a storyProbe, budgetRows and extensionGroups, and createPhaseDoorCommands takes a beforeBuild list of checks run at the continue door. No @aof/work source imports the package, lists it in its manifest or spells EXAMPLES.md (FF-13501).

135/02 · The parser reads Rule: as a group

The one Gherkin parser reports rules

parseFeature gives each scenario a rule (or none) and the feature a rules list. A rule’s tags apply to every scenario inside it and to no other, and a tag above a Rule: is listed once.

Example: is a scenario and an Examples row keeps its cells

Example: parses as a scenario, and each Examples table carries its columns and its rows’ cells. A feature with no Rule: parses to the same value under the old keys, across the whole corpus (FF-5704).

135/03 · The board groups scenarios by rule

The tasks projection carries each scenario’s rule title

aof work tasks and /api/work/tasks give each scenario the keys name, outline, lane and rule, where rule is the title of the Rule: it sits under or null, on the local and the cached path alike. Only the title crosses the wire.

The task card groups scenarios under their rule

The board’s TASKS tab (apps/ui/src/board/TasksTab.tsx) shows a task’s scenarios in file order, with the scenarios outside any rule first, as today’s list, then a heading over each rule’s indented list. A task with no rule, or a payload from an older node without the rule key, renders today’s markup.

135/04 · An agreed example cannot fall out

Every agreed example is traced to the contract by its id

The examples lane’s sixth code, example-untraced, is an error naming the story, the example, its line in EXAMPLES.md, its provenance and its rule. It fires for each confirmed or stated example that no scenario titled E<n> · … and no Examples row whose example cell is E<n> carries inside a group naming its rule. It says so when the carrier sits under a different rule. A proposed example is never required.

The continue door refuses a story missing an agreed example

aof work continue on such a story is refused with examples-question-open (409), its detail listing the same example-untraced findings the doctor reports, and nothing moves or dispatches until the example is restored.

135/05 · The contract is formulated from the map

Refine’s Contract stage formulates from an applicable map

refine.md’s story Formulation has the PO write one Rule: R<n> · … per map rule and one headline Scenario: E<n> · … per key example. QA’s outlines sit inside the rule they test, and a row restating an example carries its id in an example column. One feature per rule is the named fallback for a runner that cannot bind Rule:. With the gate off or the map not applicable, formulation reads as before.

The briefs and the guide carry the level above the matrix

The product-owner and QA briefs each carry their half of map-driven formulation. wiki/acceptance-criteria.md names the key examples as the headline above its three zoom levels, with one Rule: specimen, and every rendered copy matches a fresh render.

134 · Discovery before formulation

Discovery runs before formulation in this repository

work.examples.enabled is on here, so a story refined from now on gets an example map before any .feature, and its Contract and build stay blocked while a business question has no recorded answer; 144 is the first such story, held at its gate on one open question (m134/02, m134/04, m134/05).

A person’s answer is evidence the agent cannot write

Only an answer the harness recorded on the story’s token, stamped onto a run record at settle, can make an example confirmed or stated. The doctor refuses a label with no stamp behind it (m134/03, m134/04).

The before-number exists

At least 5 misunderstood-requirement findings and 1 amendment round over 21 stories on 124, 126, 127 and 133 is the baseline any later measurement compares against (m134/01).

134/01 · The baseline is counted

The before-number for discovery

134’s RESEARCH.md ## R7 counts misunderstood-requirement findings and amendment rounds per story on 124, 126, 127 and 133, by a stated rule with repeatable commands: at least 5 findings and 1 round across 21 stories (0.24 and 0.05 per story), read as floors.

134/02 · The map is a document

The example map’s closed grammar

A story’s EXAMPLES.md is parsed by one pure module, packages/work/src/examples/map.mjs, into rules, examples with one provenance each (proposed, confirmed, stated Q<n>), and questions with a class and a state. Any line outside the grammar is a coded malformed entry, never a silent skip.

One home for the map’s queries and its token

Open business questions, unanchored claims and the <ref> Q<n> / <ref> E<n> token are read through that module alone (FF-13402).

The work.examples.enabled gate

Discovery is off unless a project sets the boolean true. Absent, false or any other value is off, and an unknown work.examples key is examples-gate-unknown-key.

134/03 · The answer is read from the harness

One reader of a person’s answer

packages/work/src/examples/answers.mjs is the only module that reads the harness’s AskUserQuestion result (toolUseResult.answers) from a session transcript, and it keeps only answers whose question opens with a map token (FF-13401).

The answer is stamped onto the run record at settle

A settle writes each tokened answer to brief.answers as { token, question, answer, toolUseId, sessionId, at, entrypoint }; a refused or untokened question writes nothing, and collectAnswers(ref) reads every stamped record for a story.

Settle reads the real transcript store

A hand-run aof work run-complete resolves the transcript directory through claudeProjectsDir for a named workspace instead of the repository root, so it stamps spend and answers from any working directory (FF-13404).

134/04 · The readiness gate

The examples doctor lane

With work.examples.enabled on, aof work doctor reports a story’s map through five codes: example-question-open, example-provenance-unanchored and example-map-malformed (errors while the story is open, warnings once done), and example-rule-no-example and example-map-too-many-rules (warnings).

The map’s budget row

EXAMPLES.md has its own examples budget kind, 50 lines by default, governed by the doc-budget lane like PLAN.md.

The continue door

aof work continue on a story refuses with examples-question-open while any error-severity example finding stands, naming the findings, before the build starts.

Off is today

With the gate off, the doctor reports no example finding, no snapshot row carries a map, and the door refuses nothing (FF-13403).

134/05 · The discovery beat

The discovery beat at the head of a story’s Contract

With work.examples.enabled on, aof:refine drafts a story’s EXAMPLES.md from the installed template before any .feature. It strikes every question the record answers, asks each remaining business question through AskUserQuestion with its <ref> Q<n> token and its context, and stops the Contract stage on any error-severity example finding.

Briefs that propose and never label

The PO brief drafts the map and never writes confirmed or stated without a recorded answer on that token. The architect brief reviews every technical question and relabels one that is policy as business.

--autonomous asks at its one stop

A business question never takes a default in the cascade. Every open one is asked at the single end review, and a story whose question goes unanswered stays at its Contract gate.

The EXAMPLES.md template

.aof/templates/work/story/EXAMPLES.md is a legal map with no frontmatter (36 lines), carrying the Not applicable: line for a story with no rule a person owns.

133 · Architecture diagrams

An ADR’s diagram runs end to end, from config to the console

In one repository, an opt-in in work.diagrams is enough for an architect to plan, draw, export, gate and show a diagram under its ADR. Each stage is a separate surface: m133/01 (config, layout, plan), m133/02 (export), m133/03 (the doctor lane), m133/04 (the ARCHITECTURE tab, viewer and file route) and m133/05 (the prose). m133/06 proves the chain on ADR-002.

The generator is swappable at one seam

Replacing diagram-design means one new adapter file plus one config value. The architect prose, the file layout, the doctor lane and the console name no generator, and the adapter reserves a readBack direction for editing a diagram back into a superseding ADR.

133/01 · The seam is named in config

The diagram generator is a config choice

work.diagrams ({ generator, formats, style }) is read by one resolver, resolveWorkDiagrams. An absent key is off, a bad shape is one coded diagrams-* error that still resolves to off, and an unknown generator is refused by naming the registered ids.

One layout home for a diagram’s place and spelling

src/diagrams/layout.mjs alone owns the diagrams/ folder, the ADR-NNN-<slug> stem, the brief reader, the link block writer and the link parser. The parser skips fenced code and inline code spans.

One generator adapter behind one registry

generatorFor(id) resolves diagram-design, the only adapter, which carries locate, instructions, toSvg and a reserved readBack: null. The literal diagram-design appears in src/** only in that adapter (FF-13301).

aof diagram plan

aof diagram plan <ref> <ADR-NNN> --slug <slug> answers off, generator-missing, or the plan (stem, paths, brief, instructions) from config alone. It exits 0 on all three and writes nothing.

133/02 · Export writes the SVG and the PNG

aof diagram export

aof diagram export <ref> <ADR-NNN> writes the adapter’s SVG, then a PNG, beside the drawn source, and returns the link block ready to paste. A PNG miss keeps the SVG, exits non-zero and names what is missing.

A browser ladder that downloads nothing

The PNG comes from the first browser the ladder finds: the configured browser, then the cached ms-playwright headless shell or Chromium (both cache layouts), then an installed Chrome or Edge. The browser argv is formed in one function, and no Playwright, Python or npx is involved (FF-13303).

Export waits for the file

The rasterizer counts a render as done only when the process has exited AND a fresh, size-stable PNG exists. A launcher that returns before writing (Edge) is waited on, not trusted.

133/03 · The gates know about diagrams

A doctor lane for diagrams

aof work doctor runs a pure diagramsGroup lane that reports four things: a linked diagram file missing from the tree, an export owed by formats but absent, a link whose stem names a different ADR than the one it sits under, and an unlinked file in diagrams/ (an orphan). The link codes are errors while the item is open and warnings once it is done. An orphan is always a warning.

The lane is silent where there are no diagrams

An item with no diagrams/ folder and no diagram link reports nothing. This repository’s whole stream carries no diagram-* finding.

133/04 · The console shows it

Diagrams ride the artifact manifest

DIAGRAMS (diagrams/*.svg, one level) is the manifest’s last entry. aof work doc <ref> DIAGRAMS <member> answers a diagram, the board’s /api/work/doc forwards member, and a worker streams the SVG and never the source or the PNG (FF-13305).

A milestone ARCHITECTURE tab

The board’s milestone detail has an ARCHITECTURE tab, second after SPEC, that renders ARCHITECTURE.md. Each diagrams/<stem>.svg image appears inline where its ADR links it, as a loading, populated, missing or error figure. A diagram body reaches the page only as an encoded data:image/svg+xml image (FF-13304).

A full-size diagram viewer

Clicking a populated figure presents a full-size viewer over the same image, as the shell’s fullscreen occupant. It opens fit to the screen, toggles to twice the viewBox width with scrolling, and exits on Esc or the shell’s ✕ Exit fullscreen.

The pasted block’s Source · PNG links, and the viewer’s Open in new tab, open the committed .html, .png or .svg from this node’s checkout through aof diagram file at /api/diagram/file. Every answer is served under Content-Security-Policy: sandbox.

133/05 · The architect draws

One diagram step in the architect’s ADR rule and refine’s Decide stage

aof-architect and aof:refine each carry one passage, inside the ADR authoring they already describe. The architect judges whether an ADR needs a diagram and writes its ### Diagram brief, then runs aof diagram plan and follows the answer (off, missing, or the instructions), then aof diagram export and pastes the block. In solo mode the main session runs the step itself.

The prose never names the generator

No bundle document names diagram-design. Every rendered copy (.claude, .codex, .opencode) matches a fresh render, and the architect’s tools are not widened to draw.

133/06 · The live draw

This repository draws diagrams, in the console’s style

.aof/aof.config.json sets work.diagrams = { generator: "diagram-design", formats: ["svg","png"], style: ".aof/diagrams/style.md" }. The style file maps ui/src/index.css’s tokens and fonts into the plugin’s style-guide structure, so a driven session never pauses for onboarding.

ADR-002 carries its own diagram

133’s ADR-002 links diagrams/ADR-002-generator-seam.{html,svg,png}, drawn and exported through the seam it describes. The operator accepted the drawing against its brief, its style and its PNG.

132 · Run records carry the node id, not the node name

Opaque derived node ids

A derived node id is node-<installHash(salt)> whatever the machine is called; the hostname is recorded as derivedFrom and never spelled into the id, and a pinned id still wins verbatim.

Legacy ids are recognised, and moved only on purpose

The load-time self-heal leaves a hostname-stem id untouched; aof mesh identity --reidentify is the explicit move to the opaque form and refuses a pinned id.

Re-identification says what it strands

Both --reidentify and a --name that moves an existing id answer { from, to, changed, invalidated, record } from one shared scan. The scan names the old id’s node record, its enrollment credential (config and registry roster) and its control-node nomination, and neither verb repairs any of them.

The fabric joins on the declared machine name

A node record carries hostname (the machine name, kept in the aof home) beside host (the dial address), and resolvePeers joins fabric peers on it rather than on the id’s spelling. The registry sync preserves it, and both fleet faces title a node with it (macOS .local dropped), with the opaque id beside it.

Tracked run records name no machine

test/arch/mesh/acd-run-records-name-no-machine.test.mjs passes only when every tracked runs/<node>/ segment and every record’s node key is opaque or baselined. The baseline holds umamis-mac-mini → 5 alone and may only shrink.

This control node runs as node-7297

The Windows control node’s id is node-7297, and the tree’s former placeholder segment (184 files) now sits under runs/node-7297/.

131 · The human in the loop

A question no longer stops the loop

A driven session that needs a human records its question, waits in place while every other lane keeps building, and resumes the same session with the answer; only an unanswered bound parks it, and a park is announced, not a halt (m131/03, m131/07).

One question, every face, every answer path

The same question reaches the terminal, the board card and aof’s own Discord bot with its phase and cost, and is answered from aof work answer, the board’s reply box or a Discord reply from an allowlisted user, each recorded verbatim with who and when (m131/01, m131/02, m131/04, m131/05, m131/09, m131/10).

Discord as an operator console for the whole mesh

One bot on the control node posts every notification with its project, carries a worker’s ask, answers /status, /asks, /loop stop and /loop resume for allowlisted users, and serves any number of projects from one channel (m131/11, m131/12, m131/14); aof messaging sets it up and tests it from the CLI (m131/08, m131/13).

131/01 · The question is read and recorded

One reader of a session’s question

readLastAssistantTurn, askQuestionFromTurn and readAskQuestion in src/work/observe.mjs are the only transcript scan that yields the question: the last assistant turn, or a pending AskUserQuestion’s questions and options. The driver’s private scan maps over them, and its seventeen exports are unchanged.

A four-line form of the ask

NEEDS_INPUT_INSTRUCTION asks a driven session to print Decision needed:, Options:, I would pick: and What the answer changes: before the sentinel. The threshold sentences for asking are byte-identical to before.

One ask file per run in the aof home

src/loop/ask-request.mjs owns <meshRoot>/loop-asks/<runId>.json: a fifteen-key record, ASK_STATES (waiting | parked | answered), the owner’s openAsk/parkAsk/clearAsk, the reads readAsk/readAsks, and createAskPoll. No other src/ module spells loop-asks (FF-13101).

An answer sanitised once

answerAsk is the only place an answer is checked. It refuses a blank answer, one over 8,000 code points, or one holding a C0 control other than TAB/LF/CR or DEL. Otherwise it stores the answer verbatim, and the first answer wins (ask-already-answered).

asks as the run record’s seventeenth key

Every run record carries asks last, defaulting to []. Each entry holds seven keys (question, phase, askedAt, parkedAt, answer, answeredAt, by). It is written only by openRunAsk, parkRunAsk and answerRunAsk in src/run-store.mjs, and a retry starts a fresh [].

A waiting run is neither reclaimed nor charged

The stale-reclaim scan skips a run whose last ask is unanswered (FF-13103), and attemptElapsedMs subtracts the clipped, merged ask intervals.

131/02 · The notifier and its channels

A work.notify config surface that never holds the secret

work.notify is a closed schema. Channels are named by type, and urlEnv defaults to AOF_DISCORD_WEBHOOK_URL. It also takes an events filter and a link. There is no url, webhook or token key, and resolveNotifyConfig is its one resolver. An absent block is a no-op with zero network calls.

One envelope, built in one place

buildNotifyEnvelope in src/notify/notify.mjs builds the eleven-key envelope (event, ref, at, node, phase, elapsedMs, question | stop | outcome, answerPath, link) for the closed EVENTS set. stop carries the halting item’s ref.

One headline formatter every face shares

src/notify/form.mjs (zero imports, typed by form.d.mts) is the only home of formatElapsed, oneLineAsk, eventPhrase, headline, cost and accountLine. The terminal, the board and Discord all read from it (FF-13108).

A Discord webhook renderer and a channel registry

CHANNELS maps a type to its renderer and sender. renderDiscord posts plain content with allowed_mentions: { parse: [] }, at most 2,000 characters. Only the body gives way under the cap, the headline, answer command and link are kept, and code fences are closed before the suffix.

Best-effort delivery that never fails a run

notify is awaited and sends to each channel in parallel, each bounded at 5 s and never retried. It never throws. A failure degrades by one of three named codes, and no message, log or degrade carries the URL (FF-13106, FF-13107).

An accepted milestone is announced

aof work status <milestone> done fires milestone-accepted once, after the move, with the title from the resolved row. A delivery failure never fails the accept.

131/03 · The session waits and the loop keeps going

One composer at every needs-input site

Every NEEDS_INPUT goes through awaitAnswer in src/loop/ask.mjs: the wave’s lanes, the shell’s primary drive, the retry ladder and the verify branch. It reads the question, records it on the run, writes the ask file, notifies session-needs-input and narrates NN/SS – waiting on you (<phase>, <elapsed>): <question>, in that order.

A waiting run stays alive on a ref’d wait

While a run waits, its record stays running with a live heartbeat, the process is held open by a ref’d setTimeout per check, and the wait is bounded by scheduleToCloseMs counted from the ask.

The answer resumes the same session

An answer re-drives the ask’s own session through work:drive --answer, and spawnLaneDrive({ answerFile }) does the same for a lane. The answer is typed verbatim as the session’s first input. A foreign or unreadable answer is refused before any mint or spawn (FF-13104).

A waiting lane does not halt the loop

Under refine_first a waiting lane keeps its slot while the other lanes build and merge. At the bound it is parked (committed, not merged), set aside and notified session-parked-unanswered. session-needs-input is minted only by parkedHalt, and only when nothing else can run (FF-13105).

--resume re-enters a standing ask

aof work loop <scope> --resume re-drives an answered ask and re-waits a waiting one with a fresh bound and no second notice. It also clears stale ask files. Ownership is read from the last ask’s parkedAt.

The loop reports its own halt and death

loop-halted fires once after the account, but never for session-needs-input, whose ask already went out. loop-died and loop-relaunched fire on --resume from the last loop-diag event (readLastLoopDiagEvent). A halt carrying parked lanes prints each ask block.

131/04 · The answer reaches the session

One verb answers a waiting ask

aof work answer <ref> "<text>" [--as <actor>] [--json] (work:answer, in src/commands/resume.mjs) answers the latest waiting or parked ask on the item. It writes through the ask file for a local lane or primary drive. It records the answer verbatim with by: { actor, via, node } and answeredAt, and fires session-answered, whose elapsedMs is the wait. --as is at most 80 code points, with no control characters.

A mesh worker’s ask is answerable through mesh:terminal-resume

For a worker’s needs-input assignment, the verb sends answer: { text, by, askedAt } on mesh:terminal-resume through the control router. The worker types it into the resumed session, and park-resume.mjs records it. A worker-refused resume answers terminal-resume-not-started (409).

The board answers through the same verb

POST /api/work/answer runs work:answer in-process. Every board write route passes one hoisted admitWriteRequest before its body is read (FF-13109). A non-object body answers invalid-body (400) on every write route, and a non-POST to a write path answers 405.

One loopback predicate guards both write faces

isLoopbackHost in src/static-serve.mjs refuses a write whose Host is not loopback with non-loopback-host (403), on the board and on the fleet alike.

The resume sweep names a run waiting on an answer

work:resume’s sweep renders a waiting run as a NEEDS YOUR ANSWER row carrying its answer command.

131/05 · The board shows the question and takes the answer

The ask fact on a list row

With mesh: true, work:list applies applyAskOverlay outermost and sets a thirteen-key ask fact (with scope last) on a row whose item has a waiting, parked or answered ask. A local lane’s fact is read from its ask file. A mesh worker’s comes from its needs-input assignment, through awaitsAnswer in src/board-mesh-execution.mjs, the same predicate the answer verb reads. The CLI’s --json is byte-identical.

An ask card in the detail panel

ui/src/board/AskCard.tsx shows WAITING ON YOU, the phase and elapsed wait, the question as plain text (never Markdown, clamped at six lines with a toggle), and a free-text reply box. It has one Send answer button, disabled while the box is empty, and no default or “continue” button. It also shows the answered, parked and refused states, and a worker’s ask as “question unreadable”. It is keyed per ask, and DetailPanel.tsx gains only its import and mount.

One pure decision and one client

askCardState in ui/src/board/action.mjs decides everything the card shows. The header relabels to Open terminal – <node> for a worker’s ask. workApi.answer in api.ts is the card’s only fetch, and it posts { ref, text, actor } to POST /api/work/answer.

A shown card tracks its answer

While any row carries an ask, the board’s silent 5 s list poll is armed. The card’s receipt holds until the ask leaves the wire.

The ui/ freeze moved with its reason

53/FF-5307’s ui/ digest is lifted into assertUiFrozen over (path, content) pairs. It is re-pinned with 05’s measured diff, and a one-character edit to any ui/src file turns it red.

131/06 · The register

Nine resolved controls over the human-in-the-loop seams

FF-13101…FF-13109 are arch-tests in three files under test/arch/loop/: acd-loop-ask-single-home, acd-loop-ask-waits-in-place and acd-loop-ask-reaches-every-face. They are registered by import and spread in test/arch/loop/index.mjs, and the test/arch/loop budget row is 65. ARCHITECTURE.md carries no pending – 131/06 marker, and aof work doctor 131 reports no control-unresolved.

Every control has been seen red

Each control’s named mutation reds exactly that control, and each is recorded in VERIFICATION.md’s register. The non-vacuity needles of FF-13101 and FF-13107 red when misspelled.

FF-13106 reds upstream of the redaction backstop

FF-13106 carries a structural degrade-message leg, so a URL placed in a degrade message reds even though degrade()’s redaction would strip it from the fixture’s output.

131/07 · The live run

The human in the loop, measured on a running system

A real refine_first loop on this machine asked the operator build-time questions that reached T1 and the Discord bot within the minute, kept its other lanes building while they stood, took answers from a Discord reply, the CLI and the board, each resuming the session that asked, resumed a parked session with its answer, and accepted its milestone; a supervised loop was stopped and handed back from Discord with /loop stop and /loop resume, the supervisor relaunching it.

131/08 · The messaging CLI

The aof messaging command family

aof messaging init|enable|disable|status <type> are four routed commands from one module, src/commands/messaging/messaging.mjs, and aof --help lists them under Messaging:. discord is the only channel type accepted. Any other type is refused messaging-unknown-channel.

A machine-wide, owner-only webhook store

A Discord webhook URL is kept in one file, <global home>/messaging/discord.secret, written atomically. On POSIX the file is 0600 and its directory 0700. src/notify/secret.mjs is the only module that knows its path. FF-13106’s store-path leg holds that.

A URL that never passes through argv or output

init discord reads the URL only from a hidden password prompt on a TTY, or from the first line of stdin. A positional after the type is refused messaging-secret-in-argv. The URL is validated against Discord’s four webhook hosts over https. No output, JSON answer or degrade message of any verb contains it.

A per-project switch that writes only work.notify

enable discord adds work.notify.channels.discord = { type: "discord" }, and disable discord removes every channel of that type. Neither changes any key outside work.notify, and neither ever writes a url, webhook or token key. Both are idempotent, and both are refused messaging-no-project outside a project.

A status that reports presence, never value

messaging status reports three facts per type: stored on this machine, env override set, and enabled in this project. --json answers the same facts. It never reads the stored value.

A send that reads the store every time, with no restart

notify resolves a channel’s URL on every send. A non-blank env[urlEnv] wins. Otherwise it reads the store. Otherwise it degrades notify-channel-unconfigured, naming both remedies. A running daemon picks up an init without a restart.

131/09 · The bot posts

A Discord bot token as the one messaging credential

aof messaging init discord stores a bot token (hidden prompt or stdin, never argv) in 08’s owner-only discord.secret; isDiscordBotToken is the shape check, a webhook URL is refused naming the guide, and no webhook sender or URL shape remains in src/.

An offline invite URL

init discord prints https://discord.com/oauth2/authorize?client_id=<id>&scope=bot+applications.commands&permissions=2147552320, with <id> decoded from the token’s first segment and no network call.

A channel named by id in work.notify

A discord channel is { type, channelId, tokenEnv?, events?, allow? } in the closed schema (no url, webhook, token or urlEnv); enable discord --channel <id> writes it, refuses messaging-channel-id-required without the flag, and completes a channel 08 left with no id in place.

One authorised door to the Discord API

discordRequest in src/notify/discord.mjs is the only src/** code that builds Authorization: Bot … or names the API host (FF-13110); sendDiscord posts to /channels/{id}/messages after validating channelId as a snowflake, is bounded, never throws or retries, and answers the posted messageId.

Every notification posted by the bot

notify resolves the token per send (env[tokenEnv], then the store), posts through sendDiscord, and answers { delivered, failed, messages } with one { channel, channelId, messageId } per delivered channel; a 2xx whose body read outruns the bound counts as delivered with no id; a channel with no channelId degrades notify-channel-unconfigured naming the enable command.

A render that is the bot’s own message

renderDiscord answers { content, allowed_mentions: { parse: [] } } with no username, and DESIGN §3’s lines, cap and clip are unchanged.

A status that reports the bot, never its token

messaging status reports the stored token’s presence, the AOF_DISCORD_BOT_TOKEN override, and each project channel’s channelId (--json: project.channelIds, null for a channel with no id).

A bot setup guide

wiki/architecture/discord-notifications.md walks the Developer Portal, the Message Content intent, the invite, the permissions and the channel id, and every quoted CLI output in it matches the source CLI.

131/10 · Answer by replying in Discord

The bot runs on the control node

startLauncher starts src/discord/bot.mjs on the control node only, when a token resolves, and stops it with the daemon; with no token it logs discord-bot-off once and posting is unaffected; a worker never connects.

A gateway connection that resumes

src/discord/gateway.mjs is the only module that opens the gateway socket (FF-13111): it IDENTIFYs with intents 33280, heartbeats and closes a zombie, RESUMEs on every reconnect, re-identifies only on an invalid session or 4007/4009, guards the IDENTIFY budget, stops on 4004/4010–4014 with a named degrade, backs off 1 s → 60 s with jitter, and never throws into the daemon.

A machine-wide index from a posted ask to its ask

src/notify/ask-messages.mjs records { messageId, channelId, event, ref, workspaceId, projectRoot, postedAt } under <messaging store>/discord-asks/ when session-needs-input or session-parked-unanswered is delivered to a discord channel, atomically, pruning past 30 days, and a failure degrades notify-ask-index without failing the send.

An allowlisted reply answers the waiting session

src/discord/replies.mjs turns a reply to an indexed ask message from a user in that channel’s allow into invoke("work:answer", { ref, text, as: "@<username>", via: "discord" }), adds one ✅ reaction on success, replies with one coded line on a refusal, ignores bots and unindexed or foreign-channel replies silently, and never writes an ask file or run record (FF-13112).

An allowlist that defaults to nobody

work.notify.channels.<name>.allow is a unique list of user snowflakes, absent by default, so a reply is refused discord-answer-not-allowed naming the key until the operator lists someone; messaging status reports each channel’s allowlist size, never an id.

by.via: "discord" on the record

work:answer accepts via: "discord" from the in-process caller (no CLI flag), and the answer’s sanitation, first-answer-wins and session-answered are 04’s, unchanged.

An ask message that says it can be answered here

When the channel’s allow is non-empty, the needs-input action line reads Answer: reply to this message, or `aof work answer <ref> "…"`.

131/11 · Slash commands

Four guild commands registered by the bot

At every READY and hourly after it, the bot bulk-overwrites /status, /asks, /loop stop <scope> and /loop resume <scope> (each with an optional workspace) on the guild of every configured discord channelId, from one frozen table in src/discord/commands.mjs.

Every interaction deferred before its dispatch

Each interaction gets its type: 5 callback before any config read or dispatch (FF-13113) – ephemeral for /status and /asks, in-channel for /loop – and its final text is one @original PATCH capped at 2,000 characters.

Commands reach only allowlisted workspaces

An interaction counts only the served workspaces whose discord channel is the interaction’s channel and whose allow holds the user; with none it is refused discord-command-not-allowed; /loop with several and no matching workspace is refused discord-scope-ambiguous, listing them; a DM is refused.

/status and /asks read work:list

/status renders each in-progress row (ref, execution state, node, and waiting on you (<phase>, <elapsed>) for an ask) through form.mjs, and /asks lists waiting and parked asks as account lines with a jump link to the ask’s message when the index has one.

/loop stop is 130’s stop verb

/loop stop dispatches work:loop { scope, stop: true } in-process and reports the level reached (drain, then cancel) or the refusal as /loop stop <scope> was refused (<code>): <message>.

/loop resume hands the loop to the supervisor

work:loop --hand-off (handOff) writes a durable resume request, <meshRoot>/loop-resumes/<loopRunId>.json in src/loop/stop-request.mjs, which decideSupervisedDeclarations turns into a relaunch row even over an honoured stop mark, and the --resume launch clears; it refuses loop-hand-off-not-supervised, -running, -no-declaration, -not-local and -scope, is exclusive with --stop and --dry-run, and starts no process.

src/discord/** never spawns or reaches beyond two verbs

The discord family imports no node:child_process, and every invoke( in commands.mjs names work:list or work:loop (FF-13113).

131/12 · A worker’s ask reaches Discord

A worker’s park carries its question

A mesh worker’s running + needs-input park fact (assignment.reported) carries ask: { question, phase, askedAt }, read by readWorkerAsk over the worker’s own transcript through ADR-002’s one reader and clipped at 8,000 code points; every other report is byte-identical, and the journal-unavailable fallback carries no ask.

The ask kept on the control’s assignment row

global_assignments has a nullable ask column (store version 10, added by the idempotent ALTER); a park writes it, an absent ask never clears it, and the execution projection carries it only while the row awaits an answer.

A worker’s ask posted once, by the control

The settle-assignment reactor posts session-needs-input with the WORKER’s node only on the edge into needs-input (FF-13114), so a redelivered park posts nothing; with no control-side checkout it degrades worker-ask-unannounced.

The board and a reply read the worker’s question

applyAskOverlay shows a worker ask’s question, phase and askedAt from the row, and a Discord reply to the posted ask answers it through work:answer’s mesh leg (mesh:terminal-resume), which now also posts one session-answered carrying the worker’s phase.

131/13 · Test and allow from the CLI

aof messaging test <type>

The fifth aof messaging verb posts one test message, **aof – test message** · <project>, to each channel of the type in the project, through the notifier’s own pre-send checks (readyChannel, shared with deliver) and sender. It answers each message id, degrades nothing, and fails messaging-test-failed naming the fix for a 401, 403 or 404, or messaging-not-enabled with no channel of that type.

enable <type> --allow <user-id>[,…]

aof messaging enable discord --channel <id> --allow adds user ids to the channel’s allow, de-duplicated and in order. It never removes one, a repeat changes nothing, and any id that is not 17 to 20 digits is refused messaging-allow-invalid before any write. Without --allow, enable is byte-identical to 09’s.

The project on every Discord message

Line 1 of every Discord message reads **<headline>** <cost> · <project> · <node>, where the project is the config’s name, else the project folder, else absent. It is a render option, so the eleven envelope keys and the headline prefix the terminal and board share are unchanged.

131/14 · One channel, several projects

/loop finds its project by the loop the scope names

With several projects on one channel and no workspace:, /loop stop and /loop resume run in the one project whose scope carries a loop declaration (hasLoopOn, src/loop/stop.mjs); none is refused discord-loop-not-found, and a real tie is refused discord-scope-ambiguous naming only the tied projects.

A dispatch lane is never served as a project

The bot’s served list folds every member through foldDispatchWorktree, so a workspace whose presence recorded a lane’s worktree is served as its primary checkout.

130 · Stop a running loop

A running loop can be stopped from outside its own console

A live aof work loop on this machine is stopped from any other terminal, from the fleet node card, or from the desktop window, and every one of those routes writes the same one request that the loop reads. This holds on Windows, where no process outside the loop’s console can signal it. Observed end to end on the installed payload (m130/06).

One request, three faces, one verb core

The verb (m130/02), the fleet route (m130/03) and the desktop row (m130/04) each resolve a stop through stopLoop, and write the one request file m130/01 owns. No face holds a second copy of the resolution, the ladder or the vocabulary. FF-13001 and FF-13003 were each observed red when a second home was planted.

A stop never leaves the run it stopped in flight

The interrupt path that dropped 129/04’s driver observation now settles every stopped drive before the loop halts. A cancel settles cancelled with failureReason: null and a drain settles as the drive ended, so a stopped loop leaves no leaked running row to wall the next mint (m130/02, FF-13002).

A stopped loop stays stopped until the operator resumes it

The desktop’s reconcile does not relaunch a loop whose request was honoured, whether the desktop started it or attached to it. aof work loop <scope> --resume is the one door back, and it clears the request (m130/04, 130/ADR-007).

The frozen surfaces did not move for the stop

The board shows no loop, and src/board-ui.mjs and ui/src/board/ are unchanged. LoopState keeps ten keys, brief.loop nine, and LOOP_STOPS its fifteen members, with operator-interrupt the stop. src/run-store.mjs moved once, and only for 130/06’s retry carry, re-pinned under ADR-007 §5.

130/01 · The stop request has one home

A durable stop request per loop, in the aof home

A loop’s stop request is one ten-key JSON file at <meshRoot>/loop-stops/<loopRunId>.json (honouring AOF_GLOBAL_HOME, never under a checkout), written through writeText’s temp-and-rename, and read absence-tolerantly – a corrupt file reads null after a degrade report.

The two-rung ladder and its lifecycle

requestLoopStop creates a request at level 1 (drain), escalates it once to level 2 (cancel) stamping escalatedAt, is idempotent at 2 and leaves an honoured request unchanged; markStopHonoured and clearStopRequest close requested → honoured → cleared, with STOP_LEVELS and STOP_STATES the one map from level and state to word.

One interrupt source for the running loop

createStopSource composes the process’s own SIGINT/SIGTERM with the file on an unref’d poll: either raises the level 1 → 2, producer() names whichever raised it first, one AbortSignal aborts at level 2, and a third signal reaches node’s default because the source’s listeners are then removed.

src/loop/stop-request.mjs is the only speller

The literal loop-stops and the state words requested/honoured are spelled in this module and nowhere else under src/ (FF-13001, red-probed).

130/02 · The verb and the shell honour it

aof work loop <scope> --stop

--stop is a flag of work:loop that runs no foreground body: it resolves the scope’s latest declaration through src/loop/stop.mjs’s stopLoop, writes or escalates the request, and answers <scope> – stop requested (drain|cancel) for loop <loopRunId>, live|not live. <path>; --stop --resume is refused loop-stop-exclusive, a scope with no declaration loop-stop-no-declaration, and a declaration whose latest run names another node loop-stop-not-local.

One verb core for every face

stopLoop is the one resolution of a stop, imported by the command and by the fleet route alone (FF-13003); a loop found not live has its request marked honoured at once, so the next tick halts without a drive.

The running loop reads the source, not a flag

runLoopBody takes ctx.stopSource ?? createStopSource(...), halts operator-interrupt at the tick head on level 1, and composes the source’s signal onto every drive’s driver options so level 2 cancels the in-flight session through the driver’s own stop bracket (tree terminated, pty released, exit confirmed).

Every interrupted drive settles

A drive that returns after a stop is settled before the halt – cancelled with failureReason: null when the source cancelled it, as it ended otherwise – so an interrupt leaves no running row; a needs-input drive stays open for --resume by design (FF-13002).

The halt names the request, and --resume clears it

The halt line’s Details carry signal, level, request, by and cancelled; the halt marks the request honoured with the cancelled runId, and --resume clears a standing request and narrates Cleared stop request for <id> (<state>, level <n>) – resumed. exactly once.

130/03 · The fleet sees and stops it

Presence carries the node’s live loops

readActiveLoops beside readActiveRuns yields one eleven-key entry per live loopRunId (scope, level, cap, phase, cycle, ref, runId, supervised, stop); both presence producers (the heartbeat verb and the launcher tick) and the registry reshape carry it as loops, emitted LAST and only when non-empty, so a node with no loop publishes its record byte-identical to before (FF-13005).

The status body names the serving node

/api/mesh/status carries localNodeId – the control node’s id, null on an unconfigured machine (TECH_DEBT item 18 (b) discharged).

One guarded loop-stop route in assign’s shape

POST /api/mesh/loop-stop is admitted by the same same-origin + application/json guard as /api/mesh/assign (the hoisted admitWriteRequest / resolveLocalWorkspaceRow, TECH_DEBT item 44 discharged), lifts exactly { scope, workspaceId }, resolves the local workspace and answers stopLoop’s document or its coded refusal.

One line and one button on this node’s card

The fleet node card’s current-work region appends one line per live loop (loop <scope> [· stopping | · cancelling] · <phase> <ref> · cycle <n> of <cap>), drops idle when loops exist, and renders Stop then Stop now (destructive) only on the card whose node is localNodeId; a remote node’s loop line renders with no button (FF-13006).

The rung is remembered per drive

The card’s rung memory is keyed to the drive – rememberStopRung(memory, loopRunId, rung, runId) – so a cancelled-then-resumed loop shows its button again on the new drive without a reload.

The board and the Rust drift pin are untouched

fleetCurrentWorkLines and nodeCurrentWork are byte-identical, nothing under ui/src/board/ moves, and git diff -- ui/ names eight fleet files (FF-5307 re-pinned with that measurement).

130/04 · The desktop stops what it supervises

The desktop lists the loops it supervises

The desktop’s view model carries loops: [{ id, label, signal }], one per supervised declaration from the signals map and the last answered declarations tick, and the window renders them as a second .controlbar (loop <scope>, the daemons’ ramp pill, one stop control, no Start), absent when there are none.

A desktop Stop is the request first

stop_loop(id) on a declaration places the hold and spawns aof work loop <scope> --stop (the argv formed in core by stop_argv, never in the shell – FF-13007); a second press escalates to cancel, the pill reads stopping, and a failed spawn is a footer notice.

The ladder is a pure, tested decision

stop_step(presses, since_cancel_ms, grace_ms, exited) in core/supervision.rs decides Request / Cancel / Wait / Kill / Done, with STOP_GRACE_MS = 30_000 counted from the cancel; the shell applies taskkill /PID <child> /T /F then waits on a Kill, and never start_kills a declaration.

The desktop attaches to a loop it did not start

A declaration whose relaunch is refused duplicate-run and which has not been pressed attaches instead of reading dead: it shows running, spawns nothing more, and each press runs the --stop verb with no grace and no kill rung (130/ADR-007).

A stopped loop is not relaunched

The declarations producer reads each candidate’s stop request and hands decideSupervisedDeclarations a stopped set of honoured loopRunIds, so a stopped loop yields no row until --resume clears the mark; src/work/loop.mjs gains no import (FF-13004).

130/05 · The register

Seven stop controls run on every suite run

FF-13001–FF-13007 live in three files under test/arch/loop/ (acd-loop-stop-request-single-home, acd-loop-stop-settles-the-run, acd-loop-stop-reaches-every-face), registered by import and spread in that directory’s index.mjs, 20 cases green, with FF-13007’s cargo half in supervision.rs.

Every control has been seen red

Each control’s named mutation was applied in place, observed failing with the message recorded in VERIFICATION.md’s fitness register, and reverted byte-for-byte; the three with a sweep also carry a non-vacuity probe (a misspelt needle reds the sweep as reading the empty set).

The budget table moved by exactly the register

The test/arch/loop row rose 59 → 62 by exactly the three files and the src/loop exemption’s why names its new members, in this one story’s hands.

130/06 · The live stop

The stop is observed working on a real loop

On this machine’s payload, a live --supervised loop on the test-bed was stopped from the verb (drain, then cancel), from a gap between drives, from the fleet card’s two rungs, and from the desktop’s row, each read at the source: the driver’s bracket in the diag log, the run record cancelled with failureReason: null, the halt line naming the request, no relaunch across the declarations ticks, and --resume clearing the request.

An unbriefed retry carries no declaration

retryRun carries the prior run’s brief minus loop, so an operator’s aof work resume of a dead loop’s run no longer becomes the newest declaration in scope and captures the verb; every loop retry passes its own brief, so a loop’s lineage is unchanged (51cfa6a, m130/F-01).

The desktop stops a loop started on a console

The desktop attaches to a --supervised loop it did not start and stops it through the same request (130/ADR-007, 874eef6, m130/F-02) – observed on its build: press 1 wrote level 1 and press 2 level 2 through the desktop’s own --stop spawn, and the loop’s bracket cancelled the session with no fallback kill.

129 · Loop concurrency

aof work loop drives a milestone in concurrent worktree lanes, and the unit of concurrency is a worktree

Set work.loop.concurrency: "refine_first" and a loop over a milestone refines every story serially in the primary, then fans each write-disjoint wave into one dispatch worktree per member, then verifies serially – the three-phase shape of m129/01, executed by the wave tick of m129/04, over the lane verbs of m129/03 and the child drive of m129/02. Unset, the shell is byte-identical to the sequential loop that preceded it. The milestone’s own framing defect is closed by construction: no two sessions share a checkout, so a story’s grade, its run record and its progress sample are its lane’s alone.

The loop ALWAYS owns the fan-out; work.agents.mode governs only what a lane’s session spawns

The SPEC proposed that orchestrated drive a milestone-level /aof:continue and solo drive lanes; ADR-001 §5 departed and the PO ratified it (STATE.md, 2026-09-12). There is therefore ONE home for fan-out and merge-back – code, not prompt prose – and this repository, which is orchestrated, reached the SPEC’s verifiable outcome without flipping a single story session to solo. An orchestrated-milestone drive stays addable later as an additive mode value; nothing shipped forbids it.

The tree that commits the change owns the record, and the primary learns of it at the merge

A lane mints its story’s run against a ref resolved IN the lane, settles it there, grades there, commits there and merges home; the primary’s story runs/ is empty until that merge and then answers the lane’s own runId. One wave run per epoch lives on the milestone ref in the primary and carries the loop’s liveness, so the supervisor sees exactly one declaration however many lanes are open (m129/04, held by FF-12903 and FF-12907).

Merge-back is the mesh’s one verb, pointed the other way

advanceBranchToBase merges a lane home – fast-forward when it can, a real --no-ff merge otherwise, a coded refusal and a named halt on a conflict or a dirty tree, and never a rebase, a force-push or a reset (m129/03, held by FF-12904). Lanes merge serially in completion order, and the next wave is cut from the merged HEAD.

The configuration surface is self-contained under work.loop, and the operator signed it off

work.loop.concurrency, work.loop.dispatch.concurrency and work.loop.agents.<phase>.mode live in the one bounds home, each falling back to its workspace twin, the lane bound able only to NARROW the pool’s (m129/07). This surface exists because the operator held the milestone door at the 05 accept to specify it, and 07 was scoped, built and accepted against that specification.

The concurrency was driven for real on this control node, three times

m129/06 is the measurement, not a claim: two lanes open and two sessions driving at once, each lane’s record carrying the wave’s loopRunId, one baseline per wave excluding the base’s inherited reds, serial merges, every lane cleaned up, a named exit, one loop process across a 5.5-hour run. Four live-measurement gaps remain open there with discharge conditions.

Seven structural controls hold the shape, each observed red on the leg it names

FF-12901 through FF-12907 (m129/05) are the milestone’s standing guarantee: one home for the mode, no second concurrency number, no session driver or node-pty inside the loop family, the wave read from work:next and never recomputed, the lane’s records and grade taken in the lane, lanes as children of one declaration, and merge-home never discarding. Every one was made red on its declared leg and restored byte-identical; the probes are in VERIFICATION.md § Fitness functions.

129/01 · The mode and the engine decide

work.loop.concurrency resolves in the bounds home as a mode

src/loop-bounds.mjs exports LOOP_CONCURRENCY_MODES (frozen ["sequential", "refine_first"]), DEFAULT_LOOP_CONCURRENCY ("sequential"), resolveLoopConcurrency (a member verbatim, anything else the default, never a throw) and loopConcurrencyFromConfig; the key is the ninth member of BOTH LOOP_BOUND_CONFIG_RESOLVERS and LOOP_BOUND_VALUE_RESOLVERS, rangeProbe admits exactly the two modes, stepProbe refuses every step on it as outside-declared-range, and loopBoundsFromConfig is unchanged at eight keys.

A loop record citing the mode as a ceiling carries a bound with no number

configBound in src/loop-record.mjs passes every resolver answer through positiveIntegerOrNull, so ceiling: [config:work.loop.concurrency] loads without a loop-ceiling* finding and projects state: "bounded", bound: null, comparison: null.

The engine routes an in-review story to the gate on status

decideLoopPhase reads next.status verbatim; an in-review story with tasks answers { act: "gate", ref, command: "work:validate" } whatever lastPhase, cycle or the UAT count say, a story with no tasks still refines, a supplied gate still routes on its findings, and every other status decides exactly as before.

Under refine_first the engine names the refine phase from two additive inputs

concurrency and unrefined ride decideLoopPhase and decideLoop unchanged in key set; under "refine_first" a non-empty unrefined answers drive refine <unrefined[0]> at cycle 1 ahead of every head state and type, any other concurrency ignores unrefined, a malformed unrefined is empty, and a through-review { state: "done" } stays { act: "done" }.

The wave decision is pure and bound-free

decideWave({ wave, heldSet, live, setAside }) in src/work/loop.mjs answers a frozen { dispatch, hold } – the wave in its own order minus live minus set-aside, the held set minus live – reading members as refs or { ref } objects and memories as arrays or Sets, with no bound input, no configuration read, no mutation, and null for any malformed wave, held set or memory.

Three lane stops are the closed stop set’s last three members

LOOP_STOPS is fifteen members, ending lane-open-failed, lane-merge-refused, lane-merge-conflict; LOOP_REFUSALS is unchanged at six; acd-loop-probe-contract’s literal and every other LOOP_STOPS pin (FF-6306, FF-12404 leg 5, 63/03) name the same fifteen. src/work/loop.mjs still imports nothing.

129/02 · The drive is a child

aof work drive <phase> <ref> takes a lent run across the process boundary

--run <id> joins the closed input schema, CLI flag spec and argv mapper of all three phase drivers; managedRunId reads it before ctx.loopDrive.runId ("" is absent), and under it the command mints no record, settles nothing, carries the id as AOF_RUN_ID / AOF_RUN_ITEM_DIR into the session env, captures the session id on the parent’s record when one exists, and returns settlementContext ({ projectsDir, transcriptBaseline, spendBaselineAvailable }) on the result of every real drive, lent or owned. A bare drive is byte-identical; --dry-run answers { ref, phase, command } and starts nothing, lent or not.

--fix <file> is the fix transport across the process boundary

The file is read as one JSON object in fixTransport’s shape before any record is minted and before any spawn; continue alone honours it (refine and verify ignore it as they ignore ctx.loopDrive.fix); the flag wins over ctx.loopDrive.fix; every way the file fails to yield an object is the one coded refusal drive-fix-unreadable.

Under --run, the child’s stdin is the cancel channel

ctx.stdin ?? process.stdin is resumed and its end listened for inside the same try…finally as the driver call; an end after the session is live aborts a signal the driver takes, one before it is ignored; the stream is paused once the session settles. Without --run stdin is never touched.

The session driver takes a caller’s AbortSignal

driveInteractiveClaudeSession(brief, { signal }) – an abort while the session is live requests { outcome: "failed", failureReason: "cancelled" } through the driver’s own stopForOutcome bracket (stop-requested → tree terminate on win32 → pty-released → exit-confirmed); an abort after settle or behind an unconfirmed sentinel stop changes nothing; a signal already aborted at the spawn point never spawns and answers processStarted: false. The export set and direct-import set are unchanged.

runBounded aborts, additively

signal, graceMs and stdin: "pipe" are three additive options on src/work-audit/spawn.mjs’s one bounded spawn; an abort ends a piped stdin, waits the grace (DEFAULT_GRACE_MS 20 s, or a positive safe integer), then kills; SPAWN_OUTCOMES is ["exited", "deadline-expired", "not-started", "aborted"]; the first of the deadline and the abort to fire decides and exactly one kill is sent, the message naming which bound sent it; an already-aborted signal never spawns; an unknown stdin is a returned not-started. Callers passing none of the three are byte-identical, stdio stays ["ignore", "pipe", "pipe"], and no shell exists on any path.

src/loop/child-drive.mjs spawns a lane’s drive and reads exactly one document

spawnLaneDrive({ ref, phase, runId, lane, fixFile, env, deadlineMs, signal, graceMs, spawnChild }) runs process.execPath through runBounded – under a Node runtime with [<this tree's src/cli.mjs>, "work", "drive", …], under the packaged launcher (isPackaged()) with the verb words alone – cwd the lane, env the parent’s plus the caller’s, stdin: "pipe", never a shell; the whole trimmed stdout is parsed as one JSON object and the exit is classified from it (document / refused / died / timeout / aborted), with the last twenty stderr lines on every answer and whatever document parsed before a kill. It mints no failure reason, touches no run record, prints nothing and imports no session driver.

src/loop/ exists as a declared budget exemption

SOURCE_DIRECTORY_EXEMPTIONS carries src/loop with a why naming the row it owes at the ninth file or with the loop-* root-leaf move; the family holds one member.

129/03 · The lane commits and merges home

commitWorktreeChanges lives in src/mesh/worktree.mjs and is scoped by paths

The verb is defined once, beside its sibling git verbs, and re-exported from src/mesh/worker-execution.mjs (absent definition, present re-export); its runner is options.exec, else options.pushExec, else the module’s default with a five-minute budget; a non-empty paths makes the stage, the staged check and the commit all pathspec-scoped (git add -- <paths>, diff --cached -- <paths>, commit --no-verify -m … -- <paths>), with the .aof reset owed only when the scope can reach .aof and then :(exclude).aof on the commit; absent, -A, the reset and the unscoped commit exactly as before. The worker’s two call sites are unchanged lines.

resolveRefInWorktree and worktreeWorkDir live in src/work/dispatch.mjs

Both are defined in the lane’s home and resolveRefInWorktree is re-exported from worker-execution.mjs; the resolver enumerates-then-filters through findWork and answers null for any traversal-shaped ref without constructing a path. src/mesh/worktree.mjs still carries only its pre-existing loadWorkspace import from work.mjs; the assignment sink’s static closure is pinned at 73 and the driver’s reach at 24.

advanceBranchToBase answers a dirtyPolicy

"strict" (the default) is byte-identical to the mesh’s door 2 – a non-empty status --porcelain refuses with no files key. "touched-paths" refuses only when the union of every staged index entry (a rename contributing both paths) and the worktree-side/untracked paths intersected with git diff --name-only --no-renames HEAD...<commit> is non-empty, returning assignment-gate-propagation-dirty-worktree with files sorted and deduplicated, read from --untracked-files=all under core.quotePath=false; the tree is untouched on every refusal. Door 1 (already-current) is decided before any dirt check; a conflict is still aborted and refused; an unknown policy is a thrown gate-propagation-bad-option before any git verb runs. The literal set is exactly ["strict", "touched-paths"].

src/work/dispatch.mjs composes dispatchLaneBase, commitDispatchLane and mergeDispatchLaneHome

dispatchLaneBase(lanePath, { primaryRoot, exec }) answers the merge-base of the lane’s HEAD and the primary’s HEAD (the main checkout’s line only when no primaryRoot is given); commitDispatchLane(lanePath, { message, node, exec }) answers { committed, tip }; mergeDispatchLaneHome(primaryRoot, ref, { milestoneDir, message, node, exec }) refuses a detached primary before any write, commits the loop’s own writes scoped to milestoneDir under the mesh identity, then runs the one verb from the primary at the lane’s tip under touched-paths – every answer carries ref, branch, base, tip and commit, with outcomes already-current / fast-forwarded / merged / refused (lane-merge-refused, reason: "detached-head" | "branch-missing" or files) / conflict (lane-merge-conflict, aborted, the lane and its branch intact). The module spells no git merge, rebase, reset or --force of its own and imports no child_process.

A lane opened on an existing line is advanced to the primary’s HEAD first

resolveDispatchLane(projectRoot, ref, { advanceTo }) runs advanceBranchToBase(lane, advanceTo) (strict) on every door whenever a sha is given and carries the outcome as advanced; any refusal is { outcome: "refused", code: "lane-open-failed", cause: <the verb's code> }; an advanceTo that is not a hex object name is a thrown dispatch-lane-advance-not-a-sha before any door opens; without advanceTo the answer carries no advanced key.

STATE.md merges by union, and only STATE.md

.gitattributes carries exactly one merge= line, wiki/work/**/STATE.md merge=union, matched by git’s own matcher for every STATE.md under the work directory and for nothing beside it; the line names neither text nor eol.

One porcelain parser, one exec seam, one mesh identity

parsePorcelainStatus, resolveExec / defaultGitExec (which forwards env) and meshIdentityArgs are exported from src/mesh/worktree.mjs and are the only spellings; laneChanges reads through the parser.

129/04 · The wave tick

The per-story post-drive ladder is settleStoryCycle in src/loop/cycle.mjs

settleStoryCycle(phaseRun, bookkeeping, ctx, { crossToVerify }) is the one home of the grade delta, the sampler, the review gate, the four bookkeeping maps and the verify cross, parameterised by the workspace it grades in; src/commands/loop.mjs reaches work:grade and the sampler only through it, is 1,686 lines (2,311 before), and under sequential produces byte-identical LoopState (ten keys) and driven rows (six keys). The gate ladder, DOCTOR_GATE_CODES, haltDecision and the git helpers stay in the shell and reach the ladder through its options bag; narrate and report are parameters – neither family module owns a printer.

refine_first is three phases, and BUILD is the wave tick in src/loop/wave.mjs

REFINE drives every in-scope story with no tasks, in stream order, then commits the loop’s own writes scoped to the in-scope drivers’ folders under the mesh identity (skipped when nothing was driven; a git refusal is a lane-merge-refused halt with the index restored). BUILD asks work:next with throughReview: true, hands the answer to decideWave minus the lanes it holds and the units set aside, asks work:dispatch { refs } to admit the wave, and runs every admitted member as a lane; done is the boundary. VERIFY honours a fresh gate act on an in-review story – validate, doctor, the recorded grade (a successor run’s, else the attempt run’s ledger) without a re-run – and routes to verify or a continue re-drive.

A lane runs its story in its own tree

Each admitted member is opened at the primary’s HEAD (a reused lane advanced there through advanceTo; a still-fresh running record in it refuses the open by name, a stale one is reclaimed and the lane re-driven from its own tree), its item resolved in the lane, its run minted there with the loop’s declaration and brief.lane, driven by a child aof work drive with the lent id, an isolated home and a deadline of startToCloseMs + startupGraceMs, settled against the lane item, graded and gated in the lane workspace, committed on the lane branch, merged home in the primary serially through mergeDispatchLaneHome, and cleaned up through work:dispatch --cleanup. The primary’s story docs hold no record until the merge. Every step is narrated; a cleanup refusal is narrated, never a halt; a halt in one lane drains the others (each committed and merged where it merges) before the loop returns.

Every refusal of the merge home is one named halt, thrown or returned

mergeHome reads mergeDispatchLaneHome’s thrown commit-failed / gate-propagation-failed / gate-propagation-base-unresolved as the refused shape (reason = the code, merge-home-error when it has none, the message on error), so mergeHalt names lane-merge-refused / dispatch:merge-home:refused for a returned and a thrown refusal alike, and lane-merge-conflict for an aborted conflict; the lane stays committed and kept for the resume’s reconcile, and the wave run settles failed.

The grade baseline is a property of the base commit

One baseline per wave, measured once in the first lane and keyed by the base sha; readGradeBaseline({ baseCommit }) answers by sha and by ref, never across the two; every lane’s delta is its own; a baseline that cannot be taken degrades once and grades raw; a held member on a new base measures a new one; a resumed loop reads a lane-held baseline back without a rubric run; VERIFY never re-grades; sequential keeps the per-story baseline on the primary.

The wave run carries the loop’s liveness

A milestone-level run with brief.wave (members, bound, base) is minted in the primary before the first dispatch, heartbeated on the wave interval through the hook’s own queue only while a lane is open, settled done with the wave and failed on a halt, and re-minted after every merge and every mid-epoch admission so each epoch’s brief names exactly its lanes and decideSupervisedDeclarations lists exactly one row per loop; every lane run’s brief.loop deep-equals the wave run’s except cycle; the driven rows carry lane, baseCommit, merge and wave.

Admission is dispatch’s answer, re-asked after every merge

A held member is dispatched only once the colliding lane merged, from the merged HEAD; refused members are re-asked as lanes close; the bound is narrated from work:dispatch’s answer and wave.mjs holds no concurrency literal; at-capacity with own lanes in flight waits, with none in flight halts lane-open-failed / work:dispatch:at-capacity naming the foreign holders and the remedy; an empty dispatch with held members is dependency-blocked with nothing in flight and waits with a lane in flight.

Signals, the parent deadline and the resume

The first SIGINT/SIGTERM drains and halts operator-interrupt; the second ends every child’s stdin, settles each lane run cancelled and halts the same way; a child past the parent deadline is settled timeout and retried in its lane under the lineage budget; --resume reconciles every live lane under the dispatch root in order before the first ask – a stale running run reclaimed and re-driven from its tree, a committed unmerged tip merged, an ancestor tip cleaned up, dirt committed first, an unclassifiable or out-of-scope lane narrated and left – and re-mints its wave run without retrying it as the milestone’s act.

The loop is a declared supplier of the lane opener, behind the door

69/FF-6907 admits src/loop/wave.mjs by name and reason: its resolveDispatchLane( call sits inside openLane, the function it binds onto ctx.runDispatchLane for work:dispatch to call inside the admission lock and pool; a call outside that function, a binding with no work:dispatch ask, or an undeclared supplier is each a named finding, and the door count stays the door’s alone.

129/05 · The account and the register

The four controls land under test/arch/loop/, registered once, non-vacuous

acd-loop-concurrency-single-home (FF-12901), acd-loop-family-boundary (FF-12902, FF-12906), acd-lane-records-and-the-declaration (FF-12903, FF-12907) and acd-lane-grade-is-lane-scoped (FF-12905) each export archTests ({ name, run }, every name arch/129/05 FF-129…), each one import and one spread in test/arch/loop/index.mjs under a 129/05 block, nothing named in scripts/test.mjs; every sweeping leg asserts what it FOUND before what it claims (the engine’s "refine_first" branch, the one dispatch.concurrency read, the seam’s runBounded( call, the wave’s work:next asks, the lane mint’s resolveRefInWorktree( binding, the brief.loop pass-through into the lane mint, the ladder’s work:grade call, the gradeBaselines map keyed by baseCommit), and an emptied sweep is a red naming the file. The three fixture legs (FF-12903, FF-12907, FF-12905) drive a real two-member wave through runLoopBody over test/support/loop/lane-fixture.mjs with the injected spawnLaneDrive and git seams – two child calls, zero processes, the found-count guarded before the claim. Every src module is imported lazily inside run(). The test/arch/loop budget row is 59 (55 → 59, its why naming 129 and the four files).

Every declared control of 129 resolves and has been observed red

All seven FF-129xx rows of ARCHITECTURE.md’s register resolve to a file on disk, carry no pending marker, and each has been made red on exactly the leg its row names, with the message recorded – the register in VERIFICATION.md ## Fitness functions is the record; aof work doctor 129 reports no control-unresolved at either severity.

The sweeps read structure, not windows

classifySites(source, { siteRe, guardRe, declarationRe }) and topLevelArguments(body) live in test/support/source-slice.mjs – the enclosing-function rule as ONE generic (FF-12906’s wave-read leg passes a nested-aware declaration pattern, since wave.mjs declares tick / runLane two spaces in) and a depth-aware argument splitter; acd-number-null-safe’s classifyNumberSites is a five-line call onto it. Direct imports are read through importSpecifiers, comments stripped through the one scanner, no positional slice (FF-11902 / F-47-04-ARCH-2 green on the four).

FF-12904 is an extension of the never-discards control, never a twin

BRANCH_PATH_MODULES is the six (posix, repo-relative: the mesh’s three plus src/work/dispatch.mjs, src/loop/wave.mjs, src/loop/cycle.mjs); the sweeps are pure over an injected loader (forbiddenFormOffenders, armedMergeProblems, dirtyPolicyLiteralSet) so every plant is driven in-process with no checkout touched, and the default loader is readFile, so an absent member is ENOENT naming its path. The ARMED leg judges each module by its own argvs; the dirtyPolicy literal set is exactly strict, touched-paths and an empty set is NOT FOUND. The four shipped cases keep their names; four FF-12904 cases join them.

The autonomous prompt names the key beside its home

One paragraph in <config> of src/bundle/commands/autonomous.md names work.loop.concurrency, sequential, refine_first (refine → lanes → verify, in that order), .aof/aof.config.json and “flag” – no numeral, no aof work / aof: token; FF-7101 leg (a) binds it (LOOP_BOUND_VALUE_RESOLVERS["work.loop.concurrency"] is resolveLoopConcurrency by identity). The three renders (.claude, .codex, .opencode), src/bundle/manifest.json and .aof/aof.lock.json agree with the source (aof work update --dry-run → 0 updates); autonomous-shell-out-prompt’s survivors config-key set is four.

The lane suites share one fixture spelling

writeRel, mergeHeadAbsent and conflictMarkers are exported from test/support/dispatch-lane-fixture.mjs; gate-propagation-refusals-leave-branch, mesh-worker-commit-diff and work-dispatch-lanes import them and spell none of their own.

129/06 · The second live run

refine_first is switched on in this repository and drives real waves into lanes

.aof/aof.config.json carries work.loop.concurrency: "refine_first", work.loop.agents.refine.mode: "orchestrated" and work.loop.agents.continue.mode: "solo", with work.agents.mode untouched at orchestrated; three live aof work loop runs (127 on 2026-09-15 and 2026-09-15/16, 130 on 2026-09-21) drove REFINE → BUILD waves → VERIFY on this control node against real Claude sessions, and the readings of each are in VERIFICATION.md 129/06 with their run ids, lane paths, base shas and halt details.

Two lanes run at once, each with its own record, heartbeat and grade

Measured at the source on 2026-09-21 (loop-diag.130.2026-09-21T16-42-17-910Z.log, 615 lines, one loop process pid 58920): Wave 3 – dispatching 130/03, 130/04 (bound 3). opened dispatch-130-03 and dispatch-130-04 at a49725a 156 ms apart, both lanes were on disk 19:21:50 → 20:15:04Z and two sessions drove concurrently 19:29:08 → 20:03:36Z; each lane minted its own run records carrying brief.lane {worktree, branch, baseCommit} and the wave run’s brief.loop.loopRunId, each advanced its own heartbeatAt, and one baseline measured once in lane 03 at a49725a (5 inherited over 1987) served both lanes’ grades.

A lane’s grade is the lane’s, and the inherited reds are excluded by the baseline

Gate work:grade 130/04 – pass, 0 of 1987 case(s) failing (5 inherited, excluded by the baseline). at 20:14:58Z and the same for 130/03 at 21:07:04Z, each after its own fix cycles in its own worktree; no lane’s grade carried another lane’s reds, which is the whole-checkout grading defect 129 exists to end.

Lanes merge home serially through the one verb, and the next member is cut from the merged HEAD

04 merged first (own-writes 8fcd0c4, then 2f728fa = merge(8fcd0c4, 8088cd9)), then 03 over the moved primary (c107b6f, then d1e6477 = merge(c107b6f, c55b2f1)), both --no-ff through advanceBranchToBase, no conflict, write-disjoint by construction; the next member’s lane (130/05) opened at a56d7da, whose parent is 03’s merge. Every finished lane cleaned up (removed, branch … removed ×5).

The wave run is a record per epoch, and the supervisor sees one loop

Wave run 130 -0003 carried brief.wave {members: [130/03, 130/04], baseCommit 46cb366, bound 3} with a heartbeat to 20:11:49Z, settled done at 04’s close and re-minted at once as -0004 {members: [130/03], baseCommit 2f728fa} – 129/04’s epoch ruling as a record rather than a narrated line. One loop process ran the whole 5.5 hours with no supervisor relaunch.

The lane child owns its console, so a console-scoped kill never reaches the loop

runBounded takes ownConsole, and on win32 spawnLaneDrive spawns the lane child detached with its own hidden console; the 2026-09-21 run survived five lane kills whose stderr shows node-pty’s console-list agent failing AttachConsole failed against the detached child, where the same mechanism had killed the loop unbracketed twice before (deaths #4 and #5).

The driver honours the stop it requested, and a provider wait is not silence

A stop the driver itself requested settles with the requested outcome even when the liveness probe sees the dead pid first (a death nobody asked for is still agent_died), and a Usage limit reached · continuing automatically at <time> / You've hit your session limit · resets <time> line read off the output suspends the heartbeat deadline until progress resumes, with startToCloseMs still bounding the attempt and one provider-wait breadcrumb in the diag log.

The loop-diag log names the tree that ran

installLoopDiagnostics reads aof --version’s own string through an injected seam and the start line carries build=<string>, degrading to unknown when the reader throws – the payload was re-stamped mid-run on 2026-09-21 and nothing in the log could say which tree the loop had loaded.

129/07 · The loop’s settings are self-contained

The loop’s settings live under work.loop, each falling back to its workspace twin

.aof/aof.config.json → work.loop carries the mode (concurrency), the loop’s own lane bound (dispatch.concurrency) and the role mode of each driven phase (agents.refine.mode, agents.continue.mode); every one is read from the file and never passed as a flag, and a repository that sets none of the three new keys runs byte for byte as before – no flag composed, no bound passed, no prompt text changed.

The bounds home holds twelve keys, the three new ones answering null when unset

src/loop-bounds.mjs exports resolveLoopDispatchConcurrency (a positive integer verbatim, else null), resolveLoopAgentMode (solo | orchestrated verbatim, else null), LOOP_AGENT_MODES, the config-shaped loopDispatchConcurrencyFromConfig / loopAgentRefineModeFromConfig / loopAgentContinueModeFromConfig, and loopAgentModeFromConfig(workspace, phase) over LOOP_AGENT_MODE_RESOLVERS (refine, continue; any other phase answers null). Both resolver maps carry work.loop.dispatch.concurrency, work.loop.agents.refine.mode, work.loop.agents.continue.mode after work.loop.concurrency, in that order; the range probe admits exactly the members; loopBoundsFromConfig (the deadline policy) is the eight of HEAD. The home reads no non-work.loop.* key.

The lane bound narrows the pool’s, never widens it

work:dispatch takes bound (schema { type: "number" }); narrowDispatchBound(pool, requested) in src/work/dispatch.mjs answers min(requested, pool) for a positive integer and the pool’s bound for anything else, and every face of the answer (list, refs, ref, sweep, cleanup) carries the EFFECTIVE bound. work.dispatch.concurrency stays the one number for the machine, read by src/work/dispatch.mjs alone. The shell (src/commands/loop.mjs) resolves work.loop.dispatch.concurrency once through the home and hands it to the wave as bounds.laneBound; the wave passes it as bound on its --list read and its { refs } ask when it is a number, and nothing when it is null; the narration Wave N – dispatching … (bound B) and the wave run’s brief.wave.bound carry the effective bound. Under a lane bound of 1 a two-member wave runs its lanes one at a time.

The phase drive carries the phase’s role mode; --orchestrated is --solo’s twin

aof work drive refine|continue <ref> composes /aof:<phase> <ref> --solo or … --orchestrated from work.loop.agents.<phase>.mode (phaseCommand(phase, ref, mode), PHASE_MODE_FLAGS), and /aof:<phase> <ref> when the key is unset; verify never carries a flag; --dry-run shows the composed command, and the real CLI composes the same one. refine.md and continue.md parse --orchestrated as the override in the other direction (a solo work.agents.mode to orchestrated for one run), refuse the two flags together before any role runs, and name work.loop.agents.<command>.mode with its fallback to work.agents.mode; autonomous.md names the three keys and their workspace twins beside work.loop.concurrency. The nine renders, src/bundle/manifest.json and .aof/aof.lock.json agree with the source.

The standing controls admit the shape and tell the two dispatch keys apart

FF-6901’s annexation leg reds a work?.dispatch?.concurrency read in the home and admits loopConfig(workspace)?.dispatch?.concurrency; FF-12901 leg 1 pins twelve keys, leg 2 finds exactly one pool read (src/work/dispatch.mjs) and exactly one loop-key read (src/loop-bounds.mjs) and names a second reader of either, and its family sweep reads a dotted key whole; FF-7101’s key regex reads work.loop.agents.refine.mode whole. Each carries a self-check plant.

128 · work memory joins the route table

The work memory route

work:memory (src/commands/work/memory.mjs) is a registered command whose cli.route is ["work", "memory"]; deriveRouteTable() carries the key work memory, and resolveRoute(["work", "memory", …]) lands on it with the verb and its arguments as the rest.

One door

src/cli.mjs carries no subcommand === "memory" branch and no workMemoryCommandCli; workMemoryCommand is exported by nothing and imported by nothing under src/; aof --help lists aof work memory … under Work and the static Also: tail names aof session alone.

The seam’s parse, applied by its one home

The command’s cli.argv re-serialises the face’s parse into argv and hands it to parseMemoryArgv, so the six scope flags, --item as both recall filter and rebuild scope, --all, and the non-positive-or-non-numeric --limit fallback are applied by the seam and not copied; SCOPE_FLAG_SPECS is derived from the seam’s SCOPE_FLAGS.

One core path for both doors

runMemoryVerb answers help before resolving a backend, gates the verb as a coded unknown-verb refusal (status 400), then resolves and dispatches through executeMemoryVerb; the routed command’s run calls it with the face’s input, and runMemory – the in-process entry the twenty test harnesses and the retrospective-memory-ingest loop record actuate – composes over it with parseMemoryArgv’s.

The help guard at the parse

parseMemoryArgv answers help: true for --help or -h anywhere in argv, checked before any other token; through either door a help request prints the usage on stdout at exit 0 and reaches no backend; help typed as a verb is an unknown verb.

The two projections

renderMemory and memoryJson are the seam’s one human and one --json projection per verb; the command’s render/json and the collector-based defaultRender call the same two. --json recall is the records array; brief the digest without text; reindex/ingest the summary without records; status the { backend, recordCount, … } object; help { usage }.

A null render prints nothing

runCommandFace prints a rendered result only when it is not null; renderMemory answers null for an empty injection block, so recall <miss> --block on a backend that answers no record is zero bytes on stdout at exit 0.

The spine’s policies, inherited

An undeclared flag – --bogus, and --help alike – is Unknown flag "…" for work:memory. at exit 1; under --json every refusal is the face’s one envelope carrying code.

The one usage line

MEMORY_USAGE is the seam’s exported usage line; cli.spec.usage and memoryUsage()’s first line read it, and it spells --all and --block.

The four frozen lists

WORK_IDS carries work:memory; BOARD_DEFERRED carries memory with its stated reason; argsFor("memory") is status --json; PRINTERS has no work/memory.mjs row and PRINTER_CEILING is 11; src/work/memory.mjs calls console.log nowhere.

src/commands/work/

The directory exists with one member, and SOURCE_DIRECTORY_BUDGETS carries a row for it at ceiling 1 / allowance 0 whose why names the fold of the other work:* commands as a separate item; test/integration/features is a budget row (9) rather than an exemption, and test/integration/steps is at 10.

The integration scenario

test/integration/features/work-memory.feature runs through the real CLI with steps resolved by convention from the shared grammar: status renders memory: backend=none records=0 on a bare fixture, status --json answers backend: "none", and bogus fails naming the verb.

125’s README control, green

acd-readme-names-what-ships’s every command the README spells resolves row passes unedited; the five aof work memory lines the README spells resolve through deriveRouteTable.

124/02’s leg 4, flipped

acd-learning-edge-reaches-every-cut leg 4 asserts that a registered command’s route IS work memory, and still reads the memory flag surface from src/work/memory.mjs’s own branches.

The loop record

retrospective-memory-ingest.md (bundle member, installed copy and manifest entry in agreement) states that the memory surface is the registered command id work:memory and that module:src/work/memory.mjs#runMemory remains the narrowest real export for the ingest act; its four seam line citations are the shipped seam’s.

127 · Backlog and archive – the work tree holds what is live

The work tree has three roots and one rule for each

<work.dir>/ holds the live items, <work.dir>/backlog/[<group>/…]/<type>_<slug>/ holds un-numbered drivers, <work.dir>/archive/<NN_type_slug>/ holds accepted ones name verbatim – one enumerator walks all three (m127/01/Three roots, one enumerator), one verb mints a number on the way in (m127/02/One mint), one verb moves a folder on the way out (m127/03/The verb), and this repository lives that way (m127/05).

Identity is the ref, never the location

A citation by number (52, m52/ADR-003, depends: [121]) resolves wherever the folder sits; find, doc, validate, doctor, depends and memory ingest see the archive as part of the stream, and next, loop, the default list / recent and the board’s default fetch see only the live root, each with an --all / includeArchived door. No verb in this milestone changes a number an item already has.

The order of the stream is the order of work

An idea is born un-numbered and gets its number at aof work promote – at the tail, or at --at P through the one reindex engine – so the stream’s numbers are minted in the order the operator schedules; the insert-* family is that verb under its old names.

Every node sees the same shapes

A remote node’s cache-first find / list / next answer for a backlog slug or an archived ref as the owning node does (schema 9, m127/04); the board paints the backlog as rows and hides the archive behind one toggle with one mark; the fleet never offers to assign an un-numbered item.

The history the public root cannot carry is recorded

.aof/rename-ledger.tsv holds the 1,128 renames the public-root cut removed, derived once with the cited-path resolver’s own argv; every resolver edge reads it after git’s own records, so an archived document’s citation of a moved module still resolves on a fresh clone.

127/01 · One enumerator, three roots

Three roots, one enumerator

listItems walks <work.dir> (the live stream, rows byte-identical to before), then <work.dir>/backlog/** (groups at any depth; a leaf is a <type>_<slug> folder with no number, never descended, never a task), then <work.dir>/archive/ (ITEM_RE folders, name verbatim, stories walked as at the root) – each root only when it is a directory, and a project with neither root yields exactly the rows it yielded before.

The row shapes

A backlog row carries number: null, ref: <slug>, backlog: "<group path>" (forward-slashed, "" at the backlog root); an archived row carries archived: true; a live row carries neither key and the frozen seven-key listStream/findWork shape is widened only on the two new roots.

One home for the grammar

ITEM_RE, BACKLOG_ITEM_RE, BACKLOG_ROOT and ARCHIVE_ROOT are each defined once, in src/work.mjs; src/work/doctor.mjs, doctor-freshness.mjs and src/work/reindex.mjs import them and define none. Six work-root scanners are allow-listed by path and reason in FF-12701 (doctor’s orphan lane, routing’s foreign form, recovery’s foreign tree, migrate-folder’s foreign stories scan, provenance’s synchronous resolver, ratchet); observe.mjs’s three scanners and doctor-freshness.mjs’s roadmap lane now take rows from the enumerator, and migrate-folder.mjs mints through appendPosition.

One live-row predicate

isLiveStreamRow(row) ⇔ row.number != null && row.archived !== true, exported from src/work.mjs. nextWork’s driver and story walks, listStream’s default path, aof work list, aof:recent (through aof work list --json) and insert-story --under filter through it; findWork, validateWork, src/work/doctor.mjs, aof work read, depends resolution and memory ingest do not – an archived item resolves by its ref and satisfies the edge it is named in, and insert-story refuses insert-parent-archived rather than scaffolding under archive/.

--all

aof work list --all (and listStream(workDir, { all: true })) appends the archived rows after the live and backlog rows; the default listing is live rows by number, then backlog rows by group path then slug (plain string compare), and no archived row.

validate and doctor over three roots

A backlog record doc must carry no number: and is otherwise checked as at the root; a backlog row is neither a source nor a target in the depends graph; two backlog leaves sharing a slug are one backlog-slug-duplicate; the numbering lanes run over every numbered row (archived included) and no backlog row; roadmap-folder-mismatch counts an archived milestone as a folder and a backlog milestone as nothing; the orphan lane knows both root names and walks the archive; the scope grammar of doctor and validate reaches all three roots through the branches it already had.

Every .number consumer is null-safe

The ten files holding a .number parse (asserted as the exact set by FF-12702) guard each site within its enclosing function by isLiveStreamRow, a != null test or a story narrowing; appendPosition answers the highest number ever minted plus one over live AND archived rows (a number is never retired), and selectAffected shifts live rows only.

The three controls

FF-12701 (acd-work-root-one-enumerator), FF-12702 (acd-number-null-safe) and FF-12706 (acd-next-walkers-exclude-archived) are registered in test/arch/work/index.mjs, green, and each observed red under the probe its register row names.

127/02 · Promote mints the number

One mint

aof work promote <slug> [--at P] [--yes] [--json] (src/commands/promote.mjs, /aof:promote) is the only place a top-level number is minted: it resolves exactly one backlog row by slug (promote-not-found / promote-ambiguous / promote-numeric-ref otherwise), takes the next number from appendPosition over every NUMBERED row – live and archived – or opens the slot at P through the existing reindex seam (transitionStreamReindexed, space: "top-level", the count gate unchanged), renames the backlog leaf into the stream and stamps number: and the # NN · heading prefix surgically; status: and updated: are untouched.

An archived number is never re-minted

--at P refuses (promote-number-archived) when an archived driver holds a number in the shift range, and the append counts archived rows, so no promotion writes a number an archived folder already carries.

depends: is validated at promotion

A backlog item’s depends: is a planning note until promote reads it: a numeric entry must resolve live or archived (promote-depends-unresolved), an entry naming another backlog slug is refused (promote-depends-backlog) with both ways out named.

insert-* are what promote --at is

insert-milestone|chore|uat scaffold into backlog/ and promote at P through promote.mjs’s runInsertTopLevel; insert-story keeps the nested engine (a milestone’s local axis is not a stream number); src/commands/insert-shared.mjs shrank 638 → 628 lines and neither it nor any insert face computes a number.

work.intake is write-side only

initConfig writes work.intake: "backlog" only into a config it creates; an absent key reads as "stream"; the token appears in src/** only in init.mjs, init-update.mjs, promote.mjs (one refusal text) and the bundle prompts; no reader branches on it. A phase door (work:continue|refine|verify) refuses a backlog ref (phase-backlog-ref).

The prompts have one door

aof:add-milestone|chore|spike|uat|story scaffold un-numbered under <work.dir>/backlog/[group/] under either setting and compute no number – under "stream" they call aof work promote; aof:refine and aof:continue promote a backlog ref as step 0 and continue with created.ref; aof:add-story refuses a backlog parent (“promote first”) and an archived one; all eleven prompts are rendered into the three runtime trees with their manifest hashes.

127/03 · Archive is a move

The verb

aof work archive <NN> | --done [--yes] [--json] (src/commands/archive.mjs, /aof:archive) moves an accepted driver’s folder, name verbatim, from <work.dir>/ to <work.dir>/archive/; it refuses a ref that is missing, not a driver, in the backlog, already archived, not done, or whose destination exists, and never touches a number. A done milestone moves with its stories; a done story under a live milestone stays put.

--done behind one gate

--done selects every done driver at the root and refuses without --yes (archive-confirm-required, the candidate list in detail); with it, every candidate moves in number order in one run and one envelope (archived, rewritten).

The engine is the seam’s fact-writer

archiveItems and rewriteCrossingLinks live in src/work/archive.mjs (reindex.mjs’s twin: fs only, no effects, no number); the command reaches them only through transitionStreamArchived in src/effects/stream-transitions.mjs – lock → move → stream.archived appended and drained → publish – so the fleet cache follows through the existing publish-on-mutate row.

One rewrite pass over every .md under the work dir, syntactic (](target) only, fenced blocks included, frontmatter never), rewrites exactly the relative inline links that cross the archive line – a root-sibling ../NN_… gains archive/, a link out of the moved folder gains ../ – preserving each file’s EOL and BOM; every link resolves to the same path after the move as before.

Readers resolve by ref, path-readers by findWork

find, doc, validate, doctor, depends and memory ingest answer for an archived item at its new dir; the two runtime readers of a live item path outside wiki/ (acd-tune-carries-no-second-rule, acd-declared-program-single-speller) resolve through findWork, and 03’s census suite names every later reader that reads a real item folder.

Never automatic

aof:verify never archives: verify.md names aof work archive <NN> once, as the operator’s next act after the accept, and no other prompt names the verb.

127/04 · The fleet and the board see the shapes

The cache row carries the two shapes at every hop

The global work projection is schema 9: work_items gains backlog TEXT and archived INTEGER by the v8 in-place ALTER idiom, archived mapped true → 1 at the bind; the disk projection, both frame doors, the read-back and the fleet payload widen ONLY a backlog row (number: null beside backlog) and an archived row (archived: true) – a live row keeps its frozen seven keys. The row screen lives in src/work/item-row.mjs, re-exported by the store.

A remote node answers as the owning node does

cacheOnlyItem (src/work/read.mjs) rebuilds a cache-only backlog or archived row in the enumerator’s shape – number derived from backlog’s presence, never from the ref – so findWorkCacheFirst, listStreamCacheFirst (default and --all) and nextWorkCacheFirst, and aof work find|list|next on that node, answer for a backlog slug or an archived ref exactly as the owner does; a checkout that still holds the folder keeps its own location.

One list parameter

GET /api/work/list?includeArchived=1 threads work:list’s own all; the face filters nothing; the WorkItem type carries backlog and archived.

The overview shows the backlog as rows

deriveBoard partitions number === null rows out FIRST; the overview paints them as DESIGN §Surface 1’s region after the gates – a heading with the count and aof work promote <slug> in its subline, root rows first, groups as verbatim mono paths, rows as TYPE · slug · title · [stale] with no ring, chip, number, progress, dots or button; absent when the wire carries none.

One toggle reveals the archive with one mark

Show archived – one aria-pressed button in the top bar left of the legend – flips the request in place (busy + disabled in flight, reverting with the dispatch toast on error); archived cards render after the live ones with the archived pill and a bg-muted/40 surface, the ▤ N archived chip appears (even at 0) only when ON, and the mark is painted in the lane board’s switcher button, switcher rows, lane cards under all and the detail header – never on a story.

The fleet partitions the backlog out

milestoneListItems drops number: null rows before the fleet paints or offers AssignAffordance on a milestone; archived milestones fall under the fleet’s existing status filter, unmarked.

127/05 · This tree holds what is live

This repository’s intake is the backlog

.aof/aof.config.json sets work.intake: "backlog"; an aof:add-* on this tree lands un-numbered under wiki/work/backlog/, and the add → promote round trip holds on a byte-faithful shape copy of the real stream (every record doc and feature, no runs/).

The done drivers are archived

aof work archive --done --yes moved 125 done drivers (67 milestones, 19 stories, 4 spikes, 35 chores) under wiki/work/archive/, name verbatim, in one run (ed9c00c): 2,289 renames, 1,710 crossing links rewritten in 161 files, no number:, status: or updated: line changed, the four wiki/memory.md links fixed by hand, and the memory index regenerated. The root of wiki/work reads as the live items and the three roots.

The outsider’s check passes on the real stream

test/work/stream/work-this-tree-holds-what-is-live.test.mjs runs over THIS tree: every root folder is live or a sub-root, every ITEM_RE entry under archive/ is a done driver, find 52 / doc 52 / doctor 52 / validate answer for the archived milestone, next and the default listings never propose one, next 32 is ready through its archived dependencies, the board face excludes the archive by default and includes it on the parameter, no relative link resolves worse than before the move, and the 27 test files that read a real item folder read it where it lives.

126 · The declaration is the unit

A declaration survives the machine it was declared on

The four seams the SPEC measured as knowing-but-not-acting are now one chain that closes. A loop declared --supervised and killed with its runtime is listed by the predicate (m126/02), reaches the desktop supervisor through mesh status --json --declarations (m126/02), is reconciled into a running child within one 30 s tick (m126/03), and resumes against a compute budget that charges attempt time rather than calendar (m126/00). Observed end to end on 2026-09-10: a declaration minted 2026-08-23 – eighteen days before – resumed rather than refusing deadline-exhausted, and its next attempt was started by the supervisor with no operator command.

The Declaration is the supervised unit, and the supervisor knows nothing else

The noun the loop already persisted is now what a supervisor keeps alive. app/desktop holds no loop, scope, phase or run vocabulary; it receives {id, argv, cwd, label} and reconciles desired against actual. A completed loop stops appearing in the answer, so nothing relaunches it – reconciliation replaced restart-on-exit, and no completion semantics crossed the boundary (m126/03). The next thing that declares – a graph build, a soak – is supervised by the same mechanism without the supervisor learning what it is.

A supervisor-spawned loop can drive a Claude session with no console

Measured rather than assumed, on the real supervisor: an aof.exe work loop … --resume child spawned under CREATE_NO_WINDOW with piped stdio allocates a headless ConPTY (conhost.exe --headless --width 80 --height 24) and runs claude.EXE under it. Every aof.exe child of the supervisor reports MainWindowHandle = 0. This was 126/03’s one named risk and no fixture could have answered it.

An operator can see what a loop is doing while it does it

aof work loop narrates in flight through its one printer (m126/00) and aof work run-status renders the record’s own facts rather than two of its sixteen keys (m126/01). The eleven-hour silence the milestone was framed from is not reachable from this tree: the first line of a supervised run is now Driving <ref> – <phase>, cycle N of M, <level>.

The control node’s daemon environment is stated, not guessed

aof mesh desktop install --autostart registers login autostart idempotently through one injected runner, and both install and run report a three-check preflight that repairs nothing (m126/04). A Windows service was refused on the measured basis rather than deferred: session 0 has no login session, so claude there is unauthenticated and every supervised loop would start and die on auth.

One warning stopped being an eleven-hour session’s only output

m126/05.

126/00 · The loop says what it is doing and counts what it did

The attempt-series clock

scheduleToClose is measured as accumulated ATTEMPT milliseconds summed over the retryOf lineage: a settled attempt ends at updatedAt, a reclaimed one at heartbeatAt ?? updatedAt (never at the reclaim stamp), a stale running one at that same last liveness, and only a demonstrably-alive running attempt ends at now.

The millisecond decider

decideScheduleToClose takes {elapsedMs, ceilingMs} and refuses anything that is not two numbers – the {startedAt, now} form is gone rather than surviving on a branch – and both shell call sites obtain elapsedMs from the summer, so downtime between attempts is charged to nobody.

One retryOf walk, in the engine

retryLineage, attemptElapsedMs and lineageElapsedMs live in src/work/loop.mjs; src/commands/loop.mjs holds no retryOf traversal of its own, and retryLineageStartedAt points at the engine rather than duplicating beside it.

One staleness threshold per invocation

heartbeatFromConfig is resolved once per loop invocation and shared by the reclaim sweep and both deadline sites, so the sweep and the clock cannot disagree about whether a run is alive; the engine holds no staleness definition of its own and takes stalenessMs and isStale as data.

In-flight narration through the one printer

src/commands/loop.mjs announces Driving <ref> – <phase>, cycle N of C, <level> before every drivePhase call, and says Resumed, Retrying and Reclaimed once each at the sites that produce them; the four in-flight lines that already existed reach the same seam. The module still holds exactly one console.log – the injected launcher printer – and PRINTERS gains no row.

--quiet

work:loop accepts quiet in its closed input schema, its CLI spec and its argv shaper; under it zero in-flight lines are printed and the terminal account is byte-identical to the loud run, including a halt’s stop, ref, producer and resume command. The default is loud, and --json still never launches.

Nothing new on disk

The run record’s sixteen keys and the loop declaration’s eight are unchanged – every instant the clock reads was already recorded.

126/01 · run-status renders what the record holds

aof work run-status names every fact its record carries

The human render prints the run’s state, failure reason, reclaim marker and resume instant, the loop envelope’s phase, cycle-against-cap and level, the attempt, the elapsed time, the heartbeat age, the session and the node – each shown only when the record holds it, and never invented when it does not.

Elapsed and heartbeat age have one arithmetic, shared with the deadline that enforces it

The render imports attemptElapsedMs from src/work/loop.mjs rather than deriving a duration of its own, so a reclaimed run reads its last heartbeat and not its reclaim stamp: the measured 124/00 attempt renders 1,785,756 ms where a createdAt-to-updatedAt span would read 41,380,713 ms.

A human render has an injected instant, and src/spine/face.mjs is where it comes from

The one faceCtx the face constructs carries now (ISO-8601 Z) read from the wall clock in that module and no other, handed to every cli.render; src/commands/run-status.mjs contains no Date.now(), no new Date( and no readFile, and a render called without a now omits its two time figures rather than printing NaN or throwing.

A worker-mirrored run history says that it is one

A fromWorker result’s heading names the mirror and the node that reported it, so a projection-held row is not read as a live local fact; answeredFrom is deliberately not that marker, because a disk-resolved item whose runs were streamed answers cache.

The --json document is frozen across the change

All six producing return sites keep their present key sets and order – three distinct shapes, with exactly one four-key path that gains no reportedBy – the 16-key record and its 8-key brief.loop round-trip unreshaped, json: (result) => result is still identity, ref-not-found/404 is unchanged, and the render writes nothing back to the result it is handed.

53/ADR-004’s freeze is narrowed in the open: the document is frozen, the render is not

53/FF-5307 leg 2’s byte-pin of src/commands/run-status.mjs is re-pinned at a new digest with the reason written beside it rather than deleted, and src/board-ui.mjs and the ui/ tree hash are unmoved.

126/02 · The declaration predicate and its door

The supervised-declaration predicate

decideSupervisedDeclarations in src/work/loop.mjs is a pure decider that takes workspaces of run records and returns rows, spelling no failure-reason literal and no run-state literal, opening no store, touching no filesystem and reading no clock – its verdicts come from isRunning, isStale, retryReadiness and readLoopDeclaration, with deadline-exhausted recomputed from the attempt clock so a lineage whose compute budget is spent is left off the list even when the store answers ready.

isRunning on the run store

src/run-store.mjs exports isRunning(record) beside isStale – a pure predicate that tells a record in flight from a settled one, so the run-state vocabulary has one home and a caller can ask the question without spelling a state.

Supervision as the declaration’s ninth key

A loop declaration envelope is nine keys with supervised last, set by aof work loop --supervised, inherited on --resume, and projected as a sixth recoverable key defaulting to false; the usable-declaration requirement is still five keys, so every declaration already written to disk reads back as unsupervised and yields no row.

The loop’s argv in one home

src/loop-argv.mjs is a zero-import leaf holding LOOP_INPUT_KEYS, LEVEL_FLAG, RESUME_FLAG, loopInputOf and argvFor – the only module in src/** that composes a ["work","loop",…] array. src/commands/trigger.mjs imports it rather than declaring its own, and every --flag token it can emit is one work:loop declares in both cli.spec.flags and its input schema.

The declarations door on mesh status

aof mesh status --json is byte-identical to what it was; the same verb with --declarations gains exactly one key, declarations, carrying { ok, rows, skipped }. No second command and no work:* verb answers this question. resolveNodeWorkspaces’s skipped list reaches the answer verbatim, an ok: false resolver answers ok: false with empty rows and no standalone fallback, and each row carries id, label, argv, cwd, scope, level and cap – the cwd being that workspace descriptor’s own projectRoot, never process.cwd().

The producer as a closure-free module

src/mesh/declarations.mjs holds the producer and is reached only through a dynamic import inside the --declarations branch, so it sits in no static import closure and the command registry is loaded when an operator asks for declarations and at no other time.

126/03 · The supervisor reconciles a supplied set

A supervised child is an owned specification carrying its own working directory

SupervisedChild in app/desktop/crates/core/src/supervision.rs is { id, label, argv, cwd } with owned strings; the two seeded daemons keep their mesh_serve() / mesh_ui() constructors and carry cwd: None, and every declaration row carries Some(projectRoot). supervise_child calls current_dir only when the child itself carries one, so no spawn site supplies a default.

The supervised set is composed, not matched

compose_supervised_set returns the two seeded daemons followed by the supplied rows in row order. No match on is_control_node selects a supervised set; the role latch’s single server start is the one named exemption, asserted as a named exemption so a second cannot arrive silently.

The reconcile is a pure plan in the core crate

reconcile(rows, live) -> Plan { start, stop, retain } is a pure function whose #[cfg(test)] sits beside supervision.rs’s existing tests, where cargo test actually runs. A row with no controller starts; a controller with no row stops; a held controller retains; a declared, unheld, non-running child starts again – the level-triggered property. LiveController is { id, desired, held } and carries nothing about why a child stopped, so two inputs differing only in “did it exit 0” are the same value and an exit-code rule cannot be added without changing the type. The two reserved daemon ids are named in neither direction.

One poll interval supplies the set every tenth tick

poll_carries_declarations(tick) is true on every tenth tick of the existing 3 s cadence – 30 s – so the flag rides the one existing loop and no second cadence exists. The role latch still starts the server once, a declaration row never does, and the UI daemon is still started immediately.

The runtime gate is narrower than the source roster

DECLARATION_ARGV_PREFIX (["work", "loop"]) is the gate’s one home and the parse in status.rs is its only reader. A supplied row whose argv does not begin with that prefix – including a string-instead-of-array argv – is dropped, and only that row: nodes, boards, isControlNode and the surviving rows parse unchanged. A row’s scope, level and cap are not projected into the child at all, so the parse’s gate reads exactly id, argv, cwd and label.

The spawn roster is an allow-list of exactly four

test/arch/ui/acd-desktop-read-only-fleet.test.mjs now asserts that every argv-array spawn in the Rust production source is one of mesh status, mesh serve, mesh ui, work loop, with work loop admitted in writing as local process supervision. The five forbidden mutation verbs keep their deny-list in the same file.

A held declaration and a halted loop are both visible

An operator Start/Stop is a hold keyed by declaration id and survives every tick until its row disappears. duplicate-run is the fourth named clean exit, classified from the child’s emitted message rather than a code token. An exit-0 child’s last non-empty line reaches the operator through last_clean_exit, which get_view_model copies into the IPC view-model’s notice and the window footer renders; the tray menu does not carry it. A notice is cleared per child rather than on any child’s start.

Rust learns no completion semantics

No loop, scope, phase, run-status or declaration-as-a-decision vocabulary appears anywhere in app/desktop/**/*.rs (comments stripped); the token loop appears only as the second element of the admitted argv literal. A completed loop simply stops appearing in the answer, and nothing relaunches it – reconciliation replaces restart-on-exit.

126/04 · The installer fixes the daemon environment

Login autostart is a flag pair on the existing install verb

aof mesh desktop install --autostart writes one REG_SZ value named aof-mesh-desktop under HKCU\Software\Microsoft\Windows\CurrentVersion\Run whose data is the absolute path of aof-mesh-desktop.exe in the resolved install dir; --no-autostart removes it. Writing twice yields one entry and two successes, and removing an absent value succeeds. listCommands() still holds exactly mesh:desktop-install, mesh:desktop-run and mesh:desktop-stop – there is no fourth mesh:desktop-* command. --autostart together with --no-autostart is the coded refusal autostart-flags-conflict, raised on the input before any act.

Every registry path goes through one injected runner

src/commands/mesh/desktop.mjs contains no un-injectable reg, spawnSync or execFile call at all; reg is only ever an argument handed to the injected runner, the shape stop already uses for tasklist. Both invocations carry /f, so neither can prompt a child that has no console to answer. CI never touches a hive.

Off Windows the answer is a coded, non-zero refusal

admitAutostartPlatform admits exactly win32 and answers autostart-unsupported-platform naming the platform for every other value – for --no-autostart as well as --autostart, since a removal that silently succeeds where no entry could exist is the same lie in the other direction. The platform is an argument at the act, never a process.platform read inside it, so both branches run on any host.

--dry-run covers the whole verb

--dry-run is new on install and performs nothing – no placement and no registry write – while its render still names both what would be installed and what would be written. The verb’s existing artifact refusals are unchanged and still fire first, so the CLI-bijection gate can spawn this verb without performing the act it names.

A three-check preflight both verbs report and neither repairs

PREFLIGHT_CHECKS is ["claude-authenticated", "payload-build", "workspace-identity-pinned"], reported identically by install and by run, writing nothing on any path – no config, no minted identity, no registry runner. claude-authenticated parses loggedIn from claude auth status’s JSON on stdout rather than its exit code, which is 0 either way, and spawns no session. payload-build reads the launcher’s own mode and fails an embedded launcher by name. workspace-identity-pinned reads workspacePaths(projectRoot).configPath directly and the node id through readSidecar over the global identity path – no walk up, no mint, no heal – so a workspace that only resolves by path derivation reads as unpinned. A failing check refuses neither verb.

A running app is named as a running app

moveInPlace rotates a locked target to <name>.bak.<ts> before placing, the way scripts/install-local.mjs already does, and refuses desktop-app-running naming the real cause and the verb that fixes it if even that fails. The pre-existing install-dir-not-writable – a permissions message for a cause that is not permissions – is gone from that path.

126/05 · The warning has one home

One import home for the SQLite runtime

src/sqlite-runtime.mjs is the only module in src/** that spells import("node:sqlite"); src/effects/journal.mjs and src/global-work-store.mjs both reach the runtime through its importSqliteRuntime export, and neither holds a resolve body of its own.

A targeted, self-restoring warning filter

importSqliteRuntime wraps process.emitWarning for the duration of one import and restores the original in a finally, so the filter is never in force outside that call; it swallows only a warning whose type is ExperimentalWarning and whose message names SQLite, across all three emitWarning shapes (message + type string, Error whose .name is the type, and the options form), and forwards every other warning unchanged with its code and ctor intact.

The callers’ refusals are unmoved

global-work-store still throws sqlite-unavailable (501) and still honours options.sqlite === false; journal still degrades in its own words; each still performs its own DatabaseSync check. A throw from the import propagates unchanged to the caller, so the original error object and its code still reach the refusal that raises on it.

No blanket suppression in the shipped tree

src/**, bin/**, scripts/**, src/bundle/** and package.json carry no --no-warnings, no --disable-warning, no NODE_NO_WARNINGS and no NODE_OPTIONS warning flag; the deprecation warnings this repository wants to see still print.

126/06 · The preflight names the missing heartbeat hook

A fourth preflight check, and the count that supersedes three

PREFLIGHT_CHECKS is ["claude-authenticated", "payload-build", "workspace-identity-pinned", "heartbeat-hook-installed"] – the new code appended LAST, so the three that shipped with 126/04 keep their order and only the count moves. Both install and run report four lines through the one shared render.

Every workspace on this node is asked whether it can record liveness at all

heartbeat-hook-installed walks the same workspace list its sibling does and asks two questions of each: does its .claude/settings.json register the claude-run-heartbeat bundle hook, and is the file that registration names on disk. Both, because a registration pointing at a missing file fails at hook time and reads as healthy from the settings document alone. The registration is found by WALKING the document for the aofManaged marker rather than by spelling a path, so a re-nesting of the bundle’s hook tree does not silently turn the check green.

It fails closed, and names every offender rather than the first

An unreadable settings document, a node with no identity yet, a resolver that throws, and a resolver answering ok: false are each a fail naming the cause – never a pass. Every workspace lacking the hook is named; a workspace carrying it is not. A node with no workspaces registered is a pass.

Its message is readable, and skips are counted

The skip list is reported as a COUNT, never enumerated. Measured on the control node 2026-09-10 with 327 skipped workspaces: this check’s message is 1,353 characters and names seven real offenders, against workspace-identity-pinned’s 16,827 on the same data.

It reports; it repairs nothing

No aof work update is invoked, nothing is written under any project root, every read goes through an injected seam, and a failing check refuses neither verb. The remedy – aof work update – is named in the message.

125 · The loop graph gets a published face

The published site

https://umairs-workspace.github.io/agent-orchestration-framework/ serves a landing page and three pages – /loops/, /prd-acd-loop-engineering/, /prd-graph-engineering/ – and the landing page links exactly those three plus the repository.

One publishing workflow

.github/workflows/pages.yml is the second and last workflow beside release.yml; it holds two jobs, gate and deploy, and deploy declares needs: [gate].

The gate is story 79’s control, run

gate runs node scripts/test.mjs --only test/arch/loop/acd-loop-document-current.test.mjs under a fresh AOF_GLOBAL_HOME on pull_request and on push to main; the workflow holds no byte comparison and no regeneration of the loop document of its own.

The deploy runs on main alone

deploy runs for a push to main or a workflow_dispatch whose ref is main, never for a pull request; pages: write and id-token: write are granted on that job alone, the workflow’s top level and the gate job hold contents: read; concurrency group pages-deploy with cancel-in-progress: false.

The graph page is projected, never copied

scripts/site/build-site.mjs stages the docs/ shell plus wiki/work/loops.md and the two planning PRDs into dist-site/ (git-ignored, .dist-site-* staging sibling too); it takes the document’s path from loopDocumentPath, spells no basename, composes no part of the document, writes only inside --out, refuses an --out holding a foreign file by name, swaps atomically, and stages a byte-identical site on a second run. Its static import closure is node built-ins only, so the deploy job stages without npm ci.

Every published page states its provenance

Each staged page carries front matter source:, kind: (generated for the loop document, authored for the PRDs) and, for the generated page alone, regenerate: aof work loops document --write; each body is wrapped {% raw %}…{ % endraw % } and otherwise byte-identical to its source.

Mermaid renders on Pages

docs/_layouts/default.html pins mermaid@11.4.1, promotes each code.language-mermaid node kramdown emits into pre.mermaid before running Mermaid, and fetches the library only on a page that carries a fence.

The README names what ships

The /aof:autonomous row names milestone 53 as where aof work loop replaced it and still says what the command does for a run in flight; a The loop machinery section lists work loop, work loops show|graph|validate|groundedness|document, work loop-record, work tune, work trigger, work acceptor; the section links the published site.

The README cannot name a command that does not resolve

test/arch/command/acd-readme-names-what-ships.test.mjs extracts every aof … invocation from README.md’s fenced blocks and command tables (never prose), resolves each against deriveRouteTable(), keeps no route list of its own, holds a declared floor, and applies the same control to docs/index.md.

The builder’s placement is a control

test/arch/loop/acd-site-is-projected-not-copied.test.mjs holds that story 79’s reader-set row still reports exactly two src/ readers with the builder outside its walk, that the builder imports loopDocumentPath and spells no basename, and that nothing under docs/ is the document’s bytes or the builder’s output.

The workflow lints share one read boundary

test/support/workflow/workflow-lint.mjs exports readWorkflowText (CRLF normalised at the read) and stripYamlComments; release-workflow-lint and site-build both read through it.

124 · The edges aof does not draw

The declared graph is measured, and the instrument says how much of it it read

The depends: graph is checked for whether an edge is witnessed, not only whether it resolves, and the check states its own denominator every run – m124/00 delivers the lane, the one-home predicate it rests on and the class ratchet that keeps it advisory. What is new at the milestone level is the posture: an aof instrument over this stream now reports the share of its subject it could not evaluate (182 of 230 edges) in the same breath as its findings.

Two of the three return paths reach their node; the third is dropped with a trigger

Cap exhaustion hands a unit back to the plan that produced it and the range keeps running (m124/01); the outermost splitter recalls memory before it cuts (m124/02). The SCOPE line on the correction return is not built: ARCHITECTURE.md#ADR-004 records that its grounding premise was false and that the correction cycle has completed 0 times in 90 run records, and states the command whose non-zero answer reopens it.

The parallel wave and the census read one coverage rule

src/story-contract.mjs is the single home for what a story declares and what covers what; the wave (src/ready-wave.mjs) and the census lane (src/work/doctor-depends.mjs) both call it and neither carries a second rule (m124/00).

The fitness lane and the citation sweep read the tree as it is

scripts/test-rubric.mjs sweeps test/arch/** recursively (m124/00), and FF-11903’s citation sweep reads authored documents only – an item’s runs/ and observability/ subtrees are excluded, because they persist the instruments’ own output and had jammed the ratchet at a count no document repair could lower (m124/F-01). Its shrink-only ceiling stands at 47, re-measured at accept.

124/00 · The census reports its denominator

The contract set has one home

src/story-contract.mjs answers both questions about a story’s declared set – what it declares (resolveDeclaredSet, returning {present, malformed, values} plus each entry’s authored directory intent) and whether one entry covers another (contractSetCovers) – with zero project imports and no filesystem access. Directory intent is what the author wrote: a trailing / claims a subtree, and nothing stats the disk to second-guess it.

The parallel wave collides on coverage, not on equality

src/ready-wave.mjs asks the shared predicate both ways and holds no coverage rule of its own. A story declaring files: [src/commands/] and a sibling declaring a file beneath it are no longer waved together, and every pair that collided under exact-string equality still collides – the adoption is a strict tightening, asserted over a generated corpus.

A fourth advisory doctor lane names each unwitnessed depends: edge

aof work doctor runs src/work/doctor-depends.mjs as the thirteenth CHECK_GROUPS entry. For every resolved story→story edge whose dependent reads: shares nothing with the dependency’s files: it emits depends-edge-unwitnessed at warn, naming both endpoints and both sets. The word “phantom” appears in none of the lane’s codes, messages or exports.

The lane states its denominator once per run

Exactly one depends-edges-unchecked finding per run carries the two exclusion reasons separately – an endpoint that is not a story, and a story that declared no contract – and the four counts close as an identity over the edge set validateWork resolves. Over this stream at accept: 230 considered = 38 witnessed + 10 unwitnessed + 125 unchecked by type + 57 unchecked as undeclared.

An advisory lane cannot gate, as a class

Every module registered in CHECK_GROUPS that exports a frozen *_FINDING_CODES array is disjoint from CONTROL_FINDING_CODES, names no "error" severity literal and consults no acceptance horizon; src/work/doctor-controls.mjs is the one named exemption because its array is the gate’s source. A fifth advisory lane cannot re-introduce a gateable code without failing FF-12402.

The fitness lane sweeps the interior

scripts/test-rubric.mjs walks test/arch/** recursively and its zero-lane diagnostic reports modules swept, names registered and names matched. A control moved into a family folder is still in the graded lane.

124/01 · Cap exhaustion returns to the plan

One module decides cap exhaustion

src/commands/loop.mjs mints no cap-exhausted halt of its own. At its cycle-cap branch it asks decideCycleCapExhaustion in src/work/loop.mjs, handing over the facts it already holds (ref, type, parent, phase, cycle, cap, scope, and the plan’s re-entry count read off its own counter).

Cap exhaustion returns to the plan

A unit that exhausts its cycle cap is answered with the existing drive act aimed at its plan under the refine phase – the same shape decideLoopPhase already returns – with the plan ref derived from the item graph: a story’s plan is its parent milestone, a driver’s plan is itself. The range keeps running.

The escalation is bounded twice, and nothing new was added

A unit is handed back at most once per invocation and then set aside in the walk’s in-process Set; a plan is re-entered at most cap times, counted under the shell’s existing ${planRef}\0refine key. LOOP_STOPS is still twelve, LOOP_FIX_TRANSPORT_KEYS still nine, the loop declaration still eight keys; no counter or persisted key arrived.

Out of scope stays terminal, and the other eleven stops are unchanged

A derived plan ref outside the loop’s declared scope halts terminally with engine:plan-out-of-scope, naming the plan; the read-only surfaces and the other eleven members of LOOP_STOPS return exactly as before.

The engine’s own cap is ledgered, not half-wired

nextDecision still passes no cycle at any of its six call sites; wiki/work/TECH_DEBT.md item 91 records the dead guard, its three unreachable branches, the one live entry (the direct decideLoop call) and the repair’s estimate. Item 76 is compacted to budget with its stale line citations corrected.

124/02 · The learning edge reaches every cut

shatter recalls before it cuts

src/bundle/commands/shatter.md step 1 runs aof work memory recall "<objective and scope keywords>" --block once per PRD, as the product owner, before the drivers are identified – with no --item (no ref exists yet) and no --area (a milestone-level cut is cross-cutting). An empty recall is a proceed, and any near-miss that changed a framing is acknowledged under a heading the driver’s own template declares.

Every cut-making command carries a recall, by roster

FF-12405 holds a named roster of the two cut-making commands (refine.md, shatter.md) asserted in both directions, classifies every other command file with its exclusion reason, and checks each memory invocation in the bundle token by token against MEMORY_VERBS, SCOPE_FLAGS and parseMemoryArgv – never against prose, and never against the command registry, whose aof work memory door is asserted to be unrouted.

The three mirrors and both hash records agree with the source

The Claude, Codex and OpenCode renders of shatter and src/bundle/manifest.json re-render from the edited source, and .aof/aof.lock.json matches the bytes on disk for every path it names – including the four pay-debt and run-resilience paths whose recorded hashes were stale at HEAD.

123 · The review close mints no driver

The review close’s creation authority

routeFinding returns creates: null for every combination of its declared inputs, and routeFindings’ creates subset is empty by construction – a review close deposits no item in the work stream.

The destination of a checklist-shaped remedy

A checklistDischargeable finding that is not cheaperThanDriver routes to story / operator-refines / creates: null / owner: operator under every reviewed-item type the stream admits and with no reviewed-item context at all; the one exception is a reviewed chore, which still routes to amendment / reviewed-chore-definition-of-done / owner: loop.

The cost question’s bar

The only driver question 2 weighs a remedy against is a story the operator must refine, so the threshold below which a remedy is fixed at the close is the cost of a story rather than the cost of a chore.

The <finding_triage> instruction

The block states “The loop creates NO item.”, contains no aof work <verb> invocation, and is carried byte-identically by all four tracked renders – src/bundle/commands/continue.md, .claude/commands/aof/continue.md, .codex/skills/aof-continue/SKILL.md and .opencode/commands/aof/continue.md.

m71/FF-7103’s enforcement

The control asserts the no-creation bound over the decider’s whole declared input space – 9235 combinations – rather than over an enumerated set of rows, and its prose leg reports any aof work verb the block names rather than checking which one it names.

The two promotion faces

work:promote-finding and work:promote-gap are both registered and reachable by a person; work:promote-finding still refuses a finding raised while reviewing a chore, and work:promote-gap still honours --at. No routing the decider can reach leads to either.

FINDING_ROUTINGS

The export carries ["amendment", "chore", "story", "recorded", "fixed"] in its delivered order. chore is a member the decider cannot reach.

121 · The Import Specifier Extractor Has Twenty Homes And 119 00 Added The Twentieth

One import-specifier extractor under test/

test/support/module-family.mjs’s importSpecifiers is the only extractor a suite under test/arch/** reads: 41 files consume it, the DoD’s own grep (function (directImports|staticImports|importSpecifiers|imports)\() finds the home, two one-line !entry.dynamic delegates (acd-controls-never-execute, acd-command-layer-imports-downward), and the two declarers left outside test/arch/** by recorded decision – down from twenty.

FF-11901 leg 5 is a class over test/arch/**

acd-purity-is-external.test.mjs reads every suite under test/arch/** through a shape detector (a quoted-specifier capture after from/import/require, over blankStringLiterals), so a new own extractor – under any name, or inline at the call site – fails CI; the five remainders sit in a shrink-only EXTRACTOR_BASELINE, each entry carrying its file and its reason.

The home reads a backtick-quoted call specifier

A template literal with no ${…} is a literal in both call forms (importSpecifiers reports import(`./x.mjs`) and require(`./x.mjs`)); one with a substitution stays computed (computedDynamicImports), and the two partition the call.

Static-closure walkers keep a static contract

acd-trigger-holds-no-clock and acd-session-driver-mesh-blind read the home through .filter((entry) => !entry.dynamic), so FF-6301, FF-6303 (“one setTimeout, in src/fs.mjs”) and FF-5301 (“reach is exactly 71”) hold the ceilings they held before the conversion.

120 · Ff 11902 Refuses A Narrower Species Than Its Register Row Claims

FF-11902 refuses every predicate-narrowed repository walk that has no floor

acd-control-derives-its-census.test.mjs exports predicateFilteredWalks, unguardedPredicateFilters and treeWalkEqualities; at HEAD they report 60 predicate-narrowed walks, 0 unguarded, 0 silent catches, 0 retyped tree-walk equalities and 0 retyped member censuses over 1,053 controls, and the control names each carrier it finds.

A walked set is followed through set-narrowing and root-derived reads

A set bound first and narrowed later, a read handed a root-derived path (readFile(path.join(root, …)), loadLoops(…)), and a derived set asserted equal to a string-array literal are each detected as their own species; derivation is position-scoped, stops at a call, skips a test body that builds a temp directory, and does not read import(…, import.meta.url) as a root.

Every converted control asserts a floor, a declared ceiling, or named members AMONG the set

Twenty-one further controls plus the door’s own equalities carry a non-vacuity floor over the directory walked, a declared ceiling only where the number is a decision, and named members asserted among the derived set; a policy allowlist is spelled as each named member among the set with every member admitted, never as the set enumerated.

The frozen-set censuses derive from src/bundle/frozen-set.jsonc

The five sites FF-6105 cross-reads (installed set, member ids, enforcement points, deny rules) derive from frozen-set.jsonc under one declared member-count floor, FF-6105 asserts no site retypes the set, and TECH_DEBT item 81 is deleted from TECH_DEBT.md.

The detectors read every control faithfully

The walk detector is paren-aware over literal-blanked text, an expression-bodied arrow’s body is cut at its own expression, and source-slice.mjs preserves offsets across an astral character; the control asserts by name that every control is read faithfully.

119 · The tree gets an interior

Every flat layer in this repository has an interior

src/ root fell 160 → 89 modules, src/commands/ 99 → 67, and test/ and test/arch/ went 591 and 439 flat siblings to zero, across 35 and 17 subject directories. The largest directory in the test tree is now 68. Each layer’s interior is named by the families its filenames already declared, so no grouping is an invention.

One table meters every flat layer, and it cannot be blind to one

Before this milestone three ledger entries each measured their own directory – item 10 walked src/ root, item 63 walked test/arch/, and the fastest-growing flat directory in the tree was the one neither could see. A single named table now carries a per-directory ceiling for all four layers, equal to the measured count with no headroom, asserted in BOTH directions so a table naming three of four cannot pass on the fourth (m119/01).

A cited path survives a rename, across three readers

One resolver derived from git’s own rename records, with no hand-kept redirect table, serves all three: aof work doctor’s control probe, FF-11903’s sweep of src/ citations under wiki/work/**, and – added at 119/03 – suite paths cited in delivered .feature files. The third reader is what made a 1,030-suite move legal without editing 156 immutable records (m119/00, m119/03).

Nothing changed behaviour, and that is asserted rather than trusted

The assembled test registry is membership-identical across the restructure – 9,203 entries, 0 added and 0 removed. createMeshWorkerExecutionHandler’s exported surface is identical in both directions. No route, command name or registry key is derived from a filename. A command’s route is declared in the command, never in its path.

Five ledger entries are discharged; two are re-measured and kept open

Items 10, 61, 63, 78, 84 are deleted from TECH_DEBT.md, taking it 78 entries / 4,082 lines to 73 / 3,634 with the shrink-only ceiling re-stamped down. Items 81 and 83 are deliberately kept – see Gaps.

119/00 · Rule the guards that forbid the fix

Purity is a claim about EXTERNAL dependencies

A purity guard in this tree resolves its subject as a family (src/<name>/ when the directory exists, else src/<name>.mjs), extracts every static and dynamic import specifier through the one comment-stripping home, and classifies intra-family as admitted and everything else – bare specifier, node builtin, or a relative path leaving the family – as a violation naming the file and the specifier. No control under test/arch/ asserts purity by banning the import token, and the claim is held as a CLASS over test/arch/**, so a fourteenth purity guard cannot re-introduce the ban.

A control derives its facts about the tree

No control asserts an exact equality against a set derivable from the tree – a file count, a member census, a list of importers, an import allowlist – and no sweep can pass by going vacuous: every sweep asserts its own non-vacuity, so a directory move reds the control instead of emptying it. A stored literal is admitted only as a declared bound or a policy allowlist a reader cannot compute, and each admitted literal carries its reason in its own comment.

A cited path survives a rename

src/cited-path-resolve.mjs is the single home that resolves a src/**.mjs path cited in a delivered document – at HEAD, or through the repository’s own git rename records, with no hand-kept redirect table. Two readers consume it: aof work doctor’s control probe and FF-11903’s sweep over wiki/work/**. Unresolvable citations are reported by name under a shrink-only ceiling whose constant carries the command that measured it.

control-unresolved means one thing again

aof work doctor’s control-unresolved finding reports only that a register declares a control that does not exist. It no longer fires merely because somebody moved a file.

119/01 · src/ gets an interior

src/ has an interior named by the families its filenames already declared

src/mesh/ holds the 31 mesh-* modules and src/work/ the 40 work-* modules, and the face-named module the spine imports has a subject-named home (src/board-worker-stream.mjs → src/cache-read.mjs). The flat root fell from 160 modules to 89 – exactly the families’ size, with the invariant “the root falls by exactly the family’s size” holding to the unit.

Every flat layer is a row in ONE table

test/arch/testing/acd-source-directory-budget.test.mjs carries a per-directory sibling ceiling for src/ root, src/commands/, test/ and test/arch/, and the ceiling EQUALS the measured count with no headroom – so a new sibling fails CI and a story that shrinks a layer must lower its row. The claim holds in BOTH directions: every flat directory under src/ and test/ is either a row or a declared exemption, so a table naming three of four layers cannot pass on the fourth.

No path in this tree is load-bearing for behaviour

No route, command name, lane membership, bundle target or registry ordering is derived from a filename or a directory name. src/command-core.mjs and src/spine/face.mjs spell no path.basename/path.dirname/path.parse-derived identifier that reaches a route, a command id or a registry key, and COMMANDS order is a declared array order the route table reads, never a directory listing.

119/02 · src/commands/ gets an interior, and the registry stops explaining itself twice

src/commands/ has an interior

mesh/ (17), assets/ (9) and graph/ (6) hold the families their filenames already declared, and the flat command layer fell from 99 siblings to 67. The layer is now a row in the one budget table that meters flat layers, which it was previously invisible to – the fastest-growing flat directory in the tree was the one neither ledger entry could see.

The registry cites; it does not explain

No entry in src/command-core.mjs carries a rationale paragraph: each command’s registry comment is a SINGLE line whose content is a citation in m?<itemRef>/<ID> form, and the prose lives in the command module’s own header. The file fell from 582 lines to 323. The deferred-import comments that document the TDZ ring are the one exempt class and are asserted still present, because deleting them is the failure this rule must not cause.

The claim is over SHAPE, never over a count

FF-11908 asserts no comment-density number, no comment ratio and no line budget other than one per entry. A cap with no admitted decomposition is item 61’s measured failure, and this story declines the same trap over the layer item 78 names.

119/03 · The test tree gets an interior

test/ and test/arch/ have interiors

1,030 suites live in subject directories: test/ went 591 flat siblings to 0 across 35 directories, test/arch/ 439 to 0 across 17. The largest directory is now 68.

The registry spreads an INDEX instead of growing a line per suite

scripts/test.mjs fell from 5,193 lines to 333 and names directories rather than suites; 52 index.mjs files own their own membership. The assembled registry is membership-identical across the restructure – 9,203 entries at the base commit and at the tip, 0 added and 0 removed. registrationDecision is untouched and still the single decider; only the text handed to it is wider.

“Registered” is TRANSITIVE, through one home

test/support/registration-surface.mjs is the single home the three readers of “is this suite registered?” consume – the runner’s text plus every index’s – so a suite imported by its directory’s index and spread by the runner reads as registered rather than as an orphan.

A cited SUITE path resolves at HEAD or through a recorded rename

ADR-004 is amended to a THIRD reader: test/support/cited-suite-path.mjs resolves suite paths cited in delivered .feature files, which is what admits a move that would otherwise strand 489 citations across 156 immutable records. FF-11903 asserts that reader.

119/04 · The mesh god-node is split

The mesh worker’s two proven seams are their own modules

src/mesh/worker-launch.mjs (launch composition) and src/mesh/worker-repo-admission.mjs (repo admission) hold the two of item 83’s four seams that the module’s own prior extraction proved tractable. src/mesh/worker-execution.mjs fell from 2,462 lines to 1,957.

The split SUBTRACTS

Each extracted symbol is absent from the parent as a DEFINITION and present only as a re-export, and the extracted set is DERIVED from the children rather than retyped. No extracted module imports its parent back – not directly, not through a third module – and none re-exports the parent’s names. The parent performs neither extracted concern any more: what it still calls, it imports inward.

The exported surface did not move

createMeshWorkerExecutionHandler’s exported surface is identical across the split, asserted as a set equality in both directions against the frozen names, so the 56 dependents are untouched by construction rather than by inspection.

The two security controls the split would have disarmed still bite

acd-worker-clone-target-scoped and acd-worker-clone-no-credential-persisted assert negatives over the parent’s source text, so moving the clone into a sibling would have made both pass over a subject that no longer contains what they forbid – permanently, and invisibly, since their own self-checks run over planted strings. Both moved with the seam, and the credential control’s positive leg is now pinned to the clone’s own argv.

SINK_CEILING fell

The sink ceiling is lowered to the post-split measured count, carries the command that produced it, and stays shrink-only with no headroom – never softened, never deleted, which is item 83’s explicit “what not to do”.

118 · Finding triage weighs what a driver costs

The triage router has a fifth routing, appended last

FINDING_ROUTINGS reads ["amendment", "chore", "story", "recorded", "fixed"] – the four prior members unmoved in their prior order and with their prior meanings, fixed fifth and final, and no sixth member.

A remedy cheaper than the driver that would carry it is fixed at the close

A finding carrying cheaperThanDriver: true routes to fixed with vehicle fixed-at-close, creates: null and owner: loop; the close’s creates subset holds no entry for it, so it mints no item and grants no review round.

The cost question decides ahead of the checklist question and behind the two that already decided

routeFinding asks the cost question after the locked-contract question and after the Nit skip, and before the checklist question: a finding that is both cheap and checklist-dischargeable is fixed, a finding that is cheap and requires a delivered .feature or ADR change is amendment, and a Nit that is cheap is recorded and can still never be promoted.

The router takes the reviewed item’s type as an additive second argument

routeFinding(finding, { reviewedType }) and routeFindings(findings, context) accept the type of the item under review as the pass’s context rather than as a field on the finding; with no context supplied the decider returns exactly what it returned before this story, so no existing caller changes and no existing row moves.

Reviewing a chore, a checklist-dischargeable finding folds instead of promoting

With reviewedType equal to chore, that finding routes to amendment with vehicle reviewed-chore-definition-of-done, creating nothing; with any other type – story, milestone, spike, uat, or none – it still routes to chore/top-level-chore and still creates a top-level chore. The bound is read from LOOP_CREATED_ITEM_TYPE, so the one type the loop may create is the one type it may not be promoted from.

The verb refuses the same case, before it can write anything

aof work promote-finding <chore-ref> … exits 1 with promote-finding-reviewing-a-chore (400), naming the reviewed item as a chore and naming both destinations open to the remedy. The refusal sits after ref resolution and before the idempotence scan, so a finding already promoted once cannot cross the bound by having crossed it; nothing is created, nothing is renumbered and no CHORE.md is written or amended.

The operator’s own promotion face is unbound

work:promote-gap is byte-unchanged: it still declares at in its input schema and still advertises [--at <P>], so a person who wants the chore anyway types the gap face.

The shipped rule says five questions, and every rendered copy says the same

The <finding_triage> region of src/bundle/commands/continue.md states five ordered questions, puts the cost question second, weighs it against a driver’s ceremony rather than a line count or a duration, and states the depth bound with both of its destinations. The three tracked renders – .claude/commands/aof/continue.md, .codex/skills/aof-continue/SKILL.md, .opencode/commands/aof/continue.md – carry that region identically to the source, and every src/bundle/manifest.json entry hashes to its re-rendered member. No occurrence of “four ordered questions” survives in the tree.

FF-7103 asserts the two new bounds, with a probe for each

test/arch/acd-promotion-creates-one-type.test.mjs gains three legs: leg (b) reports a block that drops the cost question, asks it after the chore question, or prices it in lines; leg (b) reports a block that drops the depth bound or either of its destinations; leg (c) reports the verb’s type check removed, and reports it moved to after the idempotence scan. Its routing set is derived from the exported FINDING_ROUTINGS rather than hand-listed, so a routing added to the export is asserted against the block with no edit to the control.

117 · The Marker S Ref Is A Placeholder So The Copied Line Still Does Not Run

The regeneration line an operator copies out of EXECUTION.md is invokable verbatim

renderExecutionDocument spells the aof-generated marker as regenerateCommand(ref), so the marker of a record rendered for item 117 reads aof work loop-record 117 --write – the copied line runs as copied, with nothing left for the operator to substitute.

All three faces of the spelling name the same runnable command

The marker (src/loop-record-render.mjs:237), the sign-off prose (signoffProse(ref), src/commands/loop-record.mjs:93) and the --json face’s regenerate field (src/commands/loop-record.mjs:342) each interpolate the item’s own ref through the single regenerateCommand(ref) home; ref reaches the sign-off block from item.ref at the call site, and SIGNOFF_PROSE is a per-ref function rather than a module-load frozen constant.

A call carrying no ref degrades to the placeholder rather than to a malformed line

REGENERATE_REF_PLACEHOLDER (<ref>) survives as regenerateCommand’s fallback for an absent or blank ref, so the well-formedness chore 100 delivered holds for every caller while the ref-carrying callers get the runnable form.

The marker’s bytes are pinned over the interpolated form

78/01’s marker byte assertion is re-pinned as “the generated marker spells the regeneration command with the ref the verb requires” (test/loop-record-render.test.mjs), so a marker that regresses to a literal <ref> for a ref-carrying render turns that suite red.

116 · The command layer is imported upward by loop-record-render

The shared glyph table lives below the command layer

KIND_SHAPES lives in src/loop-graph-shapes.mjs; src/commands/loops-graph.mjs imports and re-exports it, and src/loop-record-render.mjs imports it downward. No src-root module imports src/commands/*, and the commands/loop-record.mjs → loop-record-render.mjs → commands/loops-graph.mjs cycle is closed.

Two contradictory controls are jointly satisfiable

78/FF-7802 (the glyph table is imported, never restated) and m42 wave (d) (acd-command-layer-imports-downward) are both green at once, which they had not been since the renderer landed.

115 · The Contract And Register Condensers Still Emit A Husk At A Tight Budget And Boundedfill Starves A Skeleton To Hold Room A First Entry Cannot Use

No bounded reduction is offered as one while naming none of its own entries

condensedResult (src/phase-brief.mjs:430) returns null for any reduction with total > 0 and kept === 0, so the never-a-husk rule sits in the one place a reduction becomes a section and binds every bounded condenser – including a sixth one – rather than the single packer path chore 95 bound.

The contract index and the fitness register carry an OPENED first entry instead of a husk

condenseTaskContracts and condenseFitnessRegister share the architecture slice’s retry through boundedFillOrOpen (src/phase-brief.mjs:533): where no scenario or row fits the room whole, the first is carried opened under TASKS_OPENED_FORM / FITNESS_OPENED_FORM, and the stated count says 1 of N in the form that was actually emitted.

A contract’s opening names a scenario rather than its tags

What condenseTaskContracts opens is headlineOf(scenarios[0]) – the last line of the unit – so the room is spent on the line that names the scenario, not on the @executable tag lines above it.

The fill’s reserve is held only for a first entry that could claim it

boundedFill (src/phase-brief.mjs:466) holds optional[0].length + join back only while that is <= room, and holds nothing otherwise, so a skeleton is no longer starved of room nothing can spend – the defect that lost three of milestone 72’s four ADR headings, now fixed once in the helper under all three condensers instead of worked around in one of them.

Unshippability is priced against every declared form, including the best one

assemble (src/phase-brief.mjs:1141) takes the minimum of full, floorLength and bestLength, so a section whose condenser now DECLINES a tiny room still reaches the brief in the larger form it does answer at, rather than being called unshippable and dropped entirely.

The invariant is swept, not sampled

test/brief-carries-the-contract.test.mjs runs green (exit 0, 0 failures) with the never-a-husk rule asserted at every budget for every bounded condenser, alongside a lane pinning the reserve’s could-claim-it condition.

114 · Doctor’s lane module roster omits work-doctor-loop-record.mjs

FF-5905’s roster names every doctor lane the spine imports

DOCTOR_LANE_MODULES names seven modules, matching the seven ./work-doctor-*.mjs imports in src/work-doctor.mjs, so acd-controls-never-execute reads green and the audit/doctor boundary it guards is measured rather than assumed.

113 · An Explicitly Named Config Path That Does Not Exist Is Silent

A config the operator NAMED and that is not there is a fault; one merely not DISCOVERED is not

configFaultFrom (src/work.mjs:216) takes the explicit discriminator loadWorkspace already holds and returns a missing-config fault for an absent config that arrived through --config, null for one it failed to discover by walking up from the cwd, so an unconfigured project stays a legitimate silent state while a mistyped path does not.

ENOTDIR is absence, not unreadability

A named path whose parent is a file reaches the same missing-config fault as one whose parent is empty, so the operator is told the file is not there rather than that it could not be read.

aof work doctor carries a third code through the finding chore 94 added

config-missing (src/commands/doctor.mjs:228) is an error finding appended at the workspace-level edge, so no scope filters it away, and its message differs from the two present-but-unusable codes in the one way it must: with no file to fix and no parse position to report, it names the path that was passed and offers dropping the flag to let the config be discovered.

The load door still degrades

loadWorkspace returns {} for a named-but-missing config and never throws, so every daemon and face that loads through it still resolves its defaults; the fault is reported alongside the load, never in place of it.

The two halves of the discrimination cannot drift apart

test/config-fault-visible.test.mjs pins that a falsy --config reads as DISCOVERY to the fault predicate and the path-resolution predicate alike – 14 lanes green (6 from chore 94, 8 added here) – so a discovered path can never be reported as a named one that is missing.

112 · The worktree prepare installs the ui workspace every dispatch

The prepare step’s install breadth is a measured ruling rather than an unexamined default

scripts/prepare-worktree.mjs runs an unnarrowed npm ci, and its header carries the three-arm measurement that says why: cache-warm medians of 15.0s (shipped), 3.4s (--workspaces=false --include-workspace-root) and 15.0s (--omit=dev), all taken in the same detached worktree with node_modules cleared between alternating runs.

The ui workspace is priced, and the reason its 77% is not banked is recorded beside the price

The workspace is 11.5s of the 15.0s install, and both narrowings that would drop it red the suite every lane runs: --workspaces=false reds test/terminal-harness-drives-a-grid.test.mjs with Could not resolve "clsx" (ui/src/lib/utils.ts:1) because the root suite esbuild-bundles real ui/src components through test/support/react-app-harness.mjs, and --omit=dev saves nothing measurable while removing the root’s own esbuild and ajv.

A lane that touches ui/ has a stated answer, and it is “nothing special”

The workspace the lane needs is already installed by the shipped step, so no lane carries a conditional install, and the header states that narrowing would have inverted the cost – every lane paying a red suite so the ui lanes could skip an install.

The ~6 minutes that raised the finding is attributed to a cold cache, not to per-dispatch cost

A cache-warm install in this tree is 15 seconds, so the figure the finding was raised on is a once-per-machine cold-cache cost; the prepare step carries no six-minute per-dispatch charge.

The junction inside a prepared worktree is on the record, with its remover census

npm ci creates node_modules/@aof/ui as a Windows junction with an absolute intra-tree target (<worktree>/ui), and the header records the probe: node fs.rm({recursive}), rm -rf, PowerShell Remove-Item -Recurse -Force and cmd rmdir /s /q all unlink the reparse point and leave an out-of-tree canary intact, while git worktree remove --force (git 2.47.0.windows.1) traverses it and empties the canary – so the remover 72/ADR-007 §3 blesses is the one remover that follows a junction, and §3 stays safe here because the target is intra-tree rather than because junctions are harmless.

72/ADR-007 §2 reads as a claim about aof’s own code, at the declaration a reader meets first

The header states that “aof creates no symlink or junction whose path lies inside a worktree, ever” is true as written and is censused over src/ by FF-7207, and is NOT the claim that a prepared worktree contains no junction – one does, on every dispatch, put there by this script’s own npm ci. ADR-007 is unedited; the precise reading lives beside the declaration.

111 · FF-5308’s necessity leg is red, and it is the good news it says it is

LOOP_SCOPE_FORMS is a ratified freeze rather than an unruled default

src/work-loop.mjs exports exactly the two frozen forms driver (/^\d+$/) and range (/^\d+-\d+$/), and 53_milestone_loop-artifact/ARCHITECTURE.md ADR-017 carries the argument that holds after TECH_DEBT item 49’s payment, so the pair rests on stated grounds rather than on the fall-through defect ADR-003 argued from.

The scope vocabulary is recorded as the admission grammar of four unattended launch surfaces

ADR-017 names commands/loop.mjs:814, commands/trigger.mjs:310, work-trigger/declaration.mjs:211 and mesh-assignment-directive.mjs:128 – the last dispatching to a remote worker – so widening the vocabulary is a change to four doors plus the engine’s invocation, declaration and resume doors, never to one regex.

No surface in the stream still points at a widening that will not happen

FF-5308’s register row and its State now cell, and the item-49 codebase-health note, are amended in place in 53/ARCHITECTURE.md to the file’s own dated convention; wiki/work/TECH_DEBT.md carries no copy, so those two plus ADR-017 are the whole set and a reader of any of them finds the ruling instead of the stale follow-on.

FF-5308’s register row describes the leg that actually ships

The row’s necessity-leg text states the inverted claim delivered at milestone 96’s gate – a story-shaped scope resolves to its own item and reaches no earlier milestone’s competitor – so the register and test/arch/acd-loop-scope-guard.test.mjs (6/6 green) now make the same claim, and a red on FF-5308 is a regression rather than the designed good news.

110 · The Promotion Family Has Two Listitems Reads The Cache Read Surface Gate Classifies Neither Way

Every promotion-family disk read carries an ADR-005 category

The acd-cache-read-surface-boundary gate names all three reads the promotion family makes: appendPosition and findPromotedChore are pinned (c) STRUCTURAL against src/work-promote/promotion.mjs, and runPromoteFindingToChore’s ref-resolution scan is an (a) CONTROL_SIDE entry – no promotion read is invisible to the gate in either direction.

The reviewed ref resolves cache-first

runPromoteFindingToChore reads the stream through listItemsCacheFirst(ctx.workspace) rather than listItems(workDir), so a finding raised while reviewing a worker-authored item resolves on control instead of being refused as promote-finding-unknown-ref, and the depth bound is reached for a ref this node’s disk has never held.

A CONTROL_SIDE entry fails on relocation instead of passing green

Every entry in the (a) list is { file, subject }, and the ARMED leg reports a straggler when the module is gone or no longer declares that subject – so a module the migrated read has LEFT, or one deleted outright, no longer satisfies the absence for free. Proven non-vacuous by a planted runPromoteFindingToChoreMOVED subject, which trips the leg.

promote-finding-to-chore.mjs imports nothing from work.mjs

src/commands/promote-finding-to-chore.mjs holds no import of src/work.mjs, one fewer importer of the 37-module god-node named by m41/ADR-001.

109 · Tech Debt Item 49 Still Records The Nextwork Fail Open As Open Though Story 86 Closed It

TECH_DEBT item 49 prices only the half that is still owed

The entry reads open – half paid by story 86 at severity low, and its “What’s wrong” states that nextWork’s inRange refuses an unparseable story-grained shape (invalid-scope, 400) and scopes NN/SS to that one story – so an operator scheduling from the ledger prices the surviving duplication, not the fail-open that no longer exists.

Item 49’s cited line numbers resolve at their current homes

The three citations point at the code they name – src/work.mjs:1237-1273 (inRange), src/work.mjs:1037-1044 (validateWork’s inScope copy) and src/work-doctor.mjs:757-759 (the leaf-backed inScope) – and the entry carries story 86’s FF-5301 reach-ceiling reason for why folding both callers onto src/work-ref-scope.mjs was not taken, so the remaining half names the condition that unblocks it.

108 · The Rendered Bundle Is Stale For Verify And Assimilate Code So This Repo Runs The Pre Story Prompts

This repo’s own installed bundle is re-renderable on a Windows checkout

.claude/**, .codex/** and .opencode/** are pinned text eol=lf, so on a core.autocrlf=true checkout all 144 installed members classify keep and none classify drift-warning – aof work update re-renders without --force, where before it could render none of them.

The installed bundle is current with its src/bundle/ source

Every rendered member across the three runtime trees is byte-identical to the source it renders from, including aof-architect.md – the member that was actually stale, not verify.md or assimilate-code.md.

The install-tree EOL pin is guarded, and the guard can fail

acd-bundle-install-eol-pinned resolves every render target across all three runtimes through git check-attr and asserts eol=lf, with a non-vacuity control outside the trees reporting unspecified; it is registered in the scripts/test.mjs array, so it runs in the suite.

107 · Acd Cache Read Surface Boundary Is Red On A Clean Tree Its Pinned Reader Promote Gap To Chore Mjs No Longer Declares Defaultat

The cache-read-surface boundary is green on a clean tree

acd-cache-read-surface-boundary passes 5/5 with no standing red, so a failure in that suite is now a signal rather than known noise a reviewer reads past.

The promotion engine’s disk reads are pinned at their real home

Both of src/work-promote/promotion.mjs’s structural reads – appendPosition and findPromotedChore – are pinned STRUCTURAL subjects on listItems, and promote-gap-to-chore.mjs imports no disk reader of its own, so both promotion faces are guarded through the one engine.

106 · Ff 5905 S Doctor Lane Module List Is Red On A Clean Tree And 66 S Fold The Family Ratchet Has Been Passed Twice Unfolded

66’s fold-the-family ratchet is answered, and the answer is a refusal with a price

wiki/work/TECH_DEBT.md item 10 records the ratchet as SUPERSEDED with the measurement behind it – the fold buys 8 of 158 root modules (5.1%) and strands 214 src/work-doctor-*.mjs citations across 87 wiki/ documents that no gate in this tree resolves – so an eighth doctor lane arrives at a decided gate instead of an unanswered one.

The doctor family’s growth is governed by FF-5905’s named roster, not by its placement

The three rules that replace the ratchet are recorded in the same item 10 amendment – placement is decided at birth, this family’s growth is governed by the named roster in test/arch/acd-controls-never-execute.test.mjs, and the root count stays item 10’s own src/mesh/ + src/work/ partition – so the control that actually caught the seventh lane is the one the tree relies on.

Every in-source citation of the ratchet resolves to the refusal

src/work-doctor-controls.mjs, src/work-doctor.mjs (the controlsLane entry in CHECK_GROUPS) and src/commands/audit.mjs each state the ratchet as superseded and name where the refusal lives, and each records that 66’s “4 → 5” was mismeasured at birth – 54/04’s rubric lane already made the roster six – so no reader of the doctor family meets a live gate that was never untripped.

105 · Outcome Md Is Not In The Streamed Requestable Artifact Set So The Mesh Cache Can Never Answer For It

OUTCOME.md is a streamed and requestable artifact

WORK_ITEM_ARTIFACTS carries { name: "OUTCOME", file: "OUTCOME.md" } between VERIFICATION and RETROSPECTIVE (src/work-artifacts.mjs:41), so a worker streams a built worktree’s OUTCOME.md alongside its other records and work:doc OUTCOME resolves a document that exists on disk instead of refusing it as a coded invalid-doc.

The derived view widened without an edit

WORK_ITEM_DOC_FILES names nine documents from that one definition rather than a second literal list, and all seven cases of acd-work-artifact-set-single-home – including ADR-013/C9’s clause that the derived view EQUALS the manifest’s file-kind entries in manifest order – pass untouched.

The drain suite is sized off the manifest, not off a copy of it

test/artifact-sync-drain.test.mjs derives its seed and both of its counts from the fixture’s RECORD_DOCS, which derives from the manifest, so a suite that was self-consistently green while asserting over a smaller set than the code streams now reds when the two diverge.

104 · The Coherence Lane Is The Third Reader That Was Missed

One exported home for “which numbers a depends: edge may name”

isDependTarget is exported from src/work.mjs and re-exported through the doctor spine at src/work-doctor.mjs, so every reader of the naming question reaches the same predicate instead of holding a module-local copy.

Doctor’s coherence lane resolves parentless stories

src/work-doctor-coherence.mjs builds driverStatusByNumber and gates depends-blocked-in-progress (:238) on isDependTarget, so a driver whose depends: names a done top-level story reports no unmet dependency, and an in-progress parentless story is itself judged for working ahead.

numbering-gap counts a top-level story’s number as filled

src/work-doctor-freshness.mjs resolves the number sequence over isDependTarget: on the real stream the check reports 42 alone – the genuinely item-less 42_structural-overhaul/ – where it previously reported fifteen existing story folders as absences.

duplicate-driver-number sees a milestone/story collision

src/work-doctor.mjs resolves over isDependTarget, so a milestone and a top-level story sharing a number are reported as the ambiguous findWork/nextWork resolution the check’s own message names.

isDriver still answers the phase question alone

The predicate at src/work.mjs and its spine mirror are byte-unchanged, and nextWork’s driver walk still filters on it – readiness and scheduling are unaffected by the widening.

103 · Doctor Reports Health Over A Stream It Cannot See

Depth-independent workspace resolution

findProjectConfig walks UP from the directory it is handed, so work.dir resolves against the directory aof.config.json was found in rather than process.cwd() – every reader of a workspace path answers identically from the repo root, from src/, and from a work-item folder.

Bounded walk

The upward walk stops at aof’s own config home (both the configured AOF_GLOBAL_HOME and the conventional ~/.aof) and at an ancestor .aof state dir, so a temp-dir fixture never adopts the operator’s real home and a mesh worktree resolves itself rather than the origin it was materialised inside.

An empty scan is a refusal, not a pass

aof work doctor over zero items emits an error: empty-stream finding naming the directory it scanned and exits non-zero; an empty result set and a clean result set no longer render identically.

The cwd-independence regression is pinned

test/doctor-cwd-independence.test.mjs drives the real CLI with a real cwd across five cases, and asserts the finding set is non-empty before asserting equality across directories.

102 · The declaration names its loop

The loop declaration envelope carries a registry id

buildLoopDeclaration returns eight keys – loopRunId, scope, level, cap, phase, cycle, startedAt, id – with the original seven unmoved in their original order and id appended last, and it refuses to build an envelope at all when id is absent, empty or not a string.

The refusal vocabulary has a sixth member

LOOP_REFUSALS reads loop-scope-unsupported, loop-level-locked, loop-level-gate, loop-level-unknown, loop-bound-unresolved, loop-id-missing – the five prior codes in their prior order, the new one sixth and last, and the earlier guards still decide first.

The loop shell declares the one loop it is

src/commands/loop.mjs exports SHELL_LOOP_ID, the constant loop:autonomous-cascade, and hands it to every declaration it mints – the same id across every phase, cycle and resume of an engagement, and never derived from the invocation.

The shell reads no registry at run time

A tree with no .aof/loops/ directory runs the loop shell unchanged; an id the registry does not declare is reported by 78’s projection as ran-undeclared and is never refused mid-run.

The shell’s id is pinned to the shipped registry by a check, not by a second literal

test/arch/acd-shell-loop-id-is-declared.test.mjs fails when the record declaring SHELL_LOOP_ID in src/bundle/loops/ is re-pointed, naming both the id the shell mints and the ids the registry declares; it takes the shipped records through test/support/registry-fixture.mjs, the one helper that copies them.

A run minted before this change still resumes

The resume reader recovers exactly five keys from a stored declaration, so a seven-key run record written before the eighth key existed resumes unchanged.

78’s projection joins for real, end to end

test/loop-declaration-join.test.mjs drives the real four in series – producer → the run store’s mint verb → readRuns → projectExecution – with no brief.loop literal hand-written anywhere: a carrying run reports ratio 1 and takes its loop off declared-never-ran, an undeclared id reports ran-undeclared, an empty-brief run is counted rather than dropped, and the store’s frozen sixteen keys are unmoved.

101 · Committed Generated Markdown Under Wiki Work Has No Eol Pin

The committed loop document is byte-stable across platforms

wiki/work/loops.md is pinned text eol=lf in .gitattributes:86, so it checks out LF on every platform and acd-loop-document-current compares the committed bytes against the composer’s LF output rather than against a core.autocrlf=true checkout’s CRLF.

The eol pin carries its own guard

test/arch/acd-loop-document-eol-pinned.test.mjs asserts through git check-attr that wiki/work/loops.md answers eol=lf, so deleting the .gitattributes rule now reds on every platform instead of only on a Windows checkout.

The pin is path-scoped, and the scoping is asserted

The rule covers the one generated projection, not a wiki/work/**/*.md glob: the hand-authored record docs under the work directory still answer unspecified, including wiki/work/26_.../SPEC.md, which is acd-runs-eol-pinned’s non-vacuity control – and that scoping is itself an assertion, so a widening of the rule reds rather than silently making that proof vacuous.

100 · The Generated Marker Names A Command With No Ref

Invokable regeneration marker on every generated EXECUTION.md

REGENERATE_COMMAND in src/loop-record-render.mjs is aof work loop-record <ref> --write, so the marker line stamped into every rendered EXECUTION.md names the route the verb actually accepts, and 78/01’s byte assertions pin that exact spelling.

99 · aof’s own exemption ledger reddens a governed project’s audit – TECH_DEBT 72’s species, one lane over

Exemption ledger scoped to the project it describes

runCensus’s baseline defaults to null and resolves to UNREGISTERED_BASELINE only when ledgerApplies(repoRoot) holds, so aof work audit over any project that is not aof reports no audit-baseline-stale finding for aof’s own suites.

Project identity, not install directory, decides the ledger

ledgerApplies matches the subject against LEDGER_PROJECT by directory identity with the install or by the subject’s own manifest name, so a payload install at ~/.aof/bin – which ships src/ and no test/ – still applies the ledger when it audits aof’s own repository.

isToolkitRoot as the toolkit’s own root predicate

src/work-audit/toolkit.mjs exports isToolkitRoot(subjectRoot, toolkit), a pure predicate deciding by path.relative, so drive-letter and separator comparison is Node’s single answer rather than each caller’s.

A named baseline is honoured as given

A caller that passes baseline to runCensus receives exactly that list regardless of the subject root, so the injection seam the audit lane’s tests drive is unchanged.

98 · The Reference Corpus, Re-Fetched And Confirmed At Source

Harness reference corpus confirmed at source

All six rows of src/harness-reference.mjs carry checked: "2026-09-04", the date each row’s value was reached at its cited source, and wiki/reference/harness-baselines.md was regenerated from that same run.

Every corpus row cites a source that states its number outright

langchain-agent-executor-max-iterations and openai-agents-runner-max-turns now cite the upstream declarations (langchain_classic/agents/agent.py, agents/run_config.py) that carry 15 and 10 in the response body, replacing two client-rendered doc pages whose bodies never contained the value.

97 · validate refuses a story’s honest forward reference

A reads: entry may name a sibling story’s declared write

aof work validate accepts a story reads: path that does not exist yet when some story under the same milestone claims it in its own files:, and still reports story reads path "<p>" does not exist for a path no story claims.

The forward-reference exemption is claim-bounded, not existence-bounded

The claim set is the milestone’s own story files: declarations, matched case-sensitively on the resolved project path, so a wrong-case or cross-milestone read borrows no claim and stays a finding.

The two stories that had to under-declare now declare in full

62/04 carries all five src/work-tune/*.mjs modules in its reads: and 77/05 carries src/work-audit/declared-bounds.mjs and src/work-audit/toolkit.mjs unedited, and the work stream validates PASS.

96 · The declaration earns its keep

A milestone cannot be accepted on story-scoped greens

aof work status <NN> done refuses a milestone with no green whole-tree row beside its records, in the command layer, with --gate-override "<reason>" the only way past and the reason written into REGRESSION.md as its own row. The refusal is scoped to type: milestone; a story’s done is untouched. Stated whole by 96/04.

The gate’s verdict is a document, and its reds are history

Every gate run appends to the accepting item’s own REGRESSION.md – never under runs/ or observability/ – carrying the commit, the instant, the scope and what failed. A rerun appends, so the newest row is what the door reads and the earlier rows stay readable. 96’s own record carries five reds before its green.

The read/write declaration is proposed from the graph, not recalled

src/story-contract-derive.mjs proposes reads:/files: from the codebase graph’s imports and call sites plus the contract’s own citations, every entry carrying a reason from a closed exported set. It reaches the graph only through normalizeGraph/computeImpact, holds no write path to any STORY.md, and answers an absent or unreadable graph with a citation-only proposal that says so.

A story’s test run is selected from its declared write set

aof test --scope impacted --story <ref> derives the changed set from files: through the existing selectSuites, adding a second changed-set source beside the git one rather than a second selector. A declared path the graph has not seen WIDENS the run under an existing reason and is never dropped.

The phase commands mint a run record that observe can join on

aof work run-start writes a record under the item’s own runs/ carrying itemRef and the session id when one resolves, and null when it does not – ambiguity is absence, never a guess. The transcript→item join stays the sessionId join and gains no second path.

The declaration is checked against what a bundle change actually lands

A story declaring a src/bundle/ member must also declare src/bundle/manifest.json and every git-tracked render of that member. 96’s five stories were short by 23 such entries and now declare them.

95 · The brief compiler sacrifices sections with budget unspent on a long story record, and condenses a long ARCHITECTURE to a husk

A sacrifice in a compiled phase brief is a true claim

A section named SACRIFICED in a phase brief is one that could not be carried in the room the finished brief has left: after the bottom-up shedding, the room it freed is offered back highest-priority first, the survivors are re-planned so the bounded condensers hold the shares the departure changed, and any slack a granular fill strands is spent before the claim stands. No brief in this stream now gives a section up with more than a tenth of its ceiling unspent.

The retained set of a brief is no longer a prefix of the declared priority order

A brief may carry tasks, fitness and dependencies while objective, which outranks all three, is the section given up – the outcome on every one of milestone 72’s story refine briefs. The priority order still decides who pays when two sections compete for the same room; it no longer decides membership on its own.

Every story that declares a dependency edge receives it

All 58 real stories in this stream declaring a non-empty depends: carry a DEPENDENCIES section in their refine brief. 72/02 and 72/04 were the last two without one.

The architecture slice never arrives as a husk

When no decision passage fits the room whole, the slice carries the first one OPENED – its heading and as many leading lines as the room holds, ellipsed – under a second declared form that says the passage was opened rather than listed, so the count beside it stays honest. Every declared ADR’s heading survives with it. No condensed section anywhere in this stream is now counted as carried while naming none of its own entries.

94 · A Malformed Aof Config Reads As An Empty One So Every Optional Declared Step Silently Does Not Run

A present-but-unreadable config is distinguishable from an absent one

loadWorkspace answers configFault – { path, code, message } when .aof/aof.config.json is present and does not parse or cannot be read, and null both when it parses and when there is no config at all – while still degrading to { config: {} } so no door ever dies on a torn config.

A torn config is a reported health fault, not silence

aof work doctor emits an error finding (config-unparseable / config-unreadable) naming the file, the parse error and what is consequently not running; it is appended as a workspace fact, so no scope filters it away.

readJson carries one vocabulary for a parse failure

A parse failure from readJson carries code: "malformed-json" – the word config-inspect.mjs and the board face already use – while the read leg keeps node’s own ENOENT.

93 · Ff 5809 Leaves Acd Prompt Bounds Name Their Home Unclassified In The Registry Reach Lane

FF-5809’s registry-reach taxonomy classifies every suite that reaches the shipped loops directory

test/arch/acd-prompt-bounds-name-their-home.test.mjs (FF-7101) sits in lane 3 (READS_WITHOUT_COPYING) of test/arch/acd-registry-fixture-closed.test.mjs with its reason recorded beside it – it reads shipped src/bundle/** record text in place and copies no subset into a temp registry – so the classification lane is green and no file reaching src/bundle/loops/ is unclassified.

The red four milestone gates carried as inherited is closed at its source

FF-5809 is green on its own control run rather than named as an inherited red with a home, so gates 63, 71, 72 and 77’s carried finding has a verified discharge instead of a standing exemption.

92 · Ff 6109 S Harness Switch Lane Reds On The Shipped Declared Document

FF-6109’s harness-switch lane, evaluated over the shipped harness document

The lane derives its named/unnamed partition from src/bundle/commands/continue.md’s own text rather than from a key list held in the control, so the fail-closed ground lifts for a knob the day the prompt names it with nothing in src/work-acceptor/admissibility.mjs edited – and the lane is green at HEAD over the document as shipped.

91 · The Cache Read Surface Pin Points At A Defaultat That Promote Gap To Chore Mjs No Longer Declares

A resolving subject anchor on ADR-016/G2’s structural-read pin

test/arch/acd-cache-read-surface-boundary.test.mjs’s STRUCTURAL entry pins the promotion family’s append-position disk read at src/work-promote/promotion.mjs’s appendPosition (via listItems) rather than at a defaultAt no module under src/ declares any more, so the assertPinned lane is green on a subject that exists and answers for both faces of the promotion engine at once.

90 · This Repo Cannot Declare Work Worktree Prepare Without A Prepare Script No Story Owns

Declared worktree preparation for this repo

.aof/aof.config.json declares work.worktree.prepare as node scripts/prepare-worktree.mjs (deadline 900000 ms), so every dispatch worktree materialised here is installed by the declared step rather than by hand, and the declaration compiles under ADR-001’s no-shell rule because its program is a real executable rather than a package-manager .cmd shim.

89 · FF-7106 is declared in milestone 71’s fitness register but owned by no story

A story that lands a bundle member is held to its whole write set, in CI

test/arch/acd-declared-writes-include-generated-siblings.test.mjs is on disk and registered in scripts/test.mjs. Over every STORY.md under wiki/work whose item is open, a files: entry that is a bundle member obliges src/bundle/manifest.json and every git-tracked rendered output of that member; each omission is reported naming the story and the missing sibling. Milestone 71’s register declares six controls and all six now resolve – aof work doctor 71 reports no control-unresolved at either severity.

The member→render mapping is asked of the render engine, not pattern-matched

Each member’s outputs are obtained by rendering that member alone, so no runtime’s id-mapping convention is encoded here. This is load-bearing rather than stylistic: the codex render of src/bundle/commands/continue.md presents as skill:aof-continue, not command:continue, so a control keyed on an output’s own resource id loses .codex/skills/aof-continue/SKILL.md – one of the three tracked files commit 231ee134 moved as one – and would pass the story this control exists to catch. The union of per-member renders equals the whole-bundle render exactly (143 = 143), and that equality is asserted, so an unattributable member fails here rather than hiding.

Membership, the runtime set and the tracked set are all derived

Membership comes from loadBundle()’s descriptor, never a src/bundle/ prefix: frozen-set.jsonc is a member that renders to .aof/frozen-set.jsonc, while manifest.json and bundle.json sit under the same prefix and are machinery that render to nothing. The runtimes handed to the render are the union of what the members themselves declare, so a fourth runtime is covered with no edit to the file (renderBundleOutputs defaults to ["claude"] alone, so passing the derived set is load-bearing). The demanded renders are intersected with git ls-files, so a render the repository does not keep is never asked for.

The control is non-vacuous by construction, and green over the stream today

The decision is a pure function over story records; the live leg feeds it the real tree and four further legs feed it synthetic ones. Measured at landing: 276 STORY.md walked, 84 member source paths, 143 tracked renders, 9 open stories of which 0 declare a bundle member – so the live leg has no subject today and the plants are what keep the control falsifiable. The scan’s own reach is asserted separately from the existence of a subject.

The horizon is read through the one predicate, and the read itself is pinned

isOpen is imported from src/acceptance-horizon.mjs rather than re-derived from the literal "done" (FF-6602). Every STORY.md here is checked out CRLF, and a status read that keeps the \r hands isOpen a word it does not recognise, which fails OPEN and puts every accepted story back in scope – measured, that is 33 violations across four done stories (59/04, 61/01, 83, 84). The contract “a done record reads done in both checkouts” is asserted directly, so a rewrite to the obvious line.slice("status: ".length) shape fails here.

88 · Three arch tests are red from 71/00 and 71/01, both already done

The four controls 71/00 and 71/01 moved out from under are green at their new subjects

acd-cache-read-surface-boundary, acd-registry-fixture-closed (FF-5809), acd-progress-ledger-consumed (FF-6109) and the 61/03 task 02 outline row in acceptor-admissibility.test.mjs pass with no gate deleted, relaxed or carved out – the cache-read pin resolves at promotion.mjs’s appendPosition() where 71/01’s engine extraction took it, and every arch test file is classified into one of FF-5809’s four lanes.

Two of the four now derive their subject from the tree instead of pinning a snapshot of it

FF-6109’s harness leg reads its refused/considered partition off the harness document’s own text, and the 61/03 task 02 row asserts the harness ground applies iff the harness of record does not name the key – so the next config key commands/continue.md names moves sides with nothing edited, rather than reddening a control that was only ever recording how many knobs the prompt happened to name.

87 · The test-isolation guard stops shipping

The shipped frozen set carries five members, and none of them is repo-specific policy

The declaration the framework ships names locked-contract, litmus, tag-vocabulary, gate-order and anchors; no member protects a global store from unisolated suite invocations, and no member names the tool-call hook enforcement point.

The bundle installs no test-isolation guard

Neither src/bundle/bundle.json nor the generated src/bundle/manifest.json targets a guard in a consumer’s hook directory, src/bundle/hooks/ holds no such body, and the asset census is re-measured to the smaller tree as an exact 79-file set-equality in both directions.

A consumer’s own build commands are the consumer’s own business

A fresh aof work init plants no framework-authored entry that judges a command before it runs; an already-installed consumer has that entry retracted by the next aof work update, with the operator’s own entries at the same event surviving in their own positions and no other compiled member’s output moved.

55/FF-5505 is enforced against the declaration rather than against a census of what shipped

The control asserts that every rule produced at every enforcement point carries its declaring member’s id, that a point no member names compiles to nothing and is reported as unnamed rather than as satisfied, and that a declared member reaching no enforcement point fails – so it binds the next hook member anyone declares without being re-armed, and refuses the vacuously-true repair over an empty set.

The compiler’s hook path is exercised by a member built for the purpose

The marker, the surgical splice into a co-authored settings file, the retraction when the member leaves and the coded tamper are all driven through a synthetic hook-shaped member in the test, so the coverage sits with the compiler that owns the behaviour rather than with whatever the bundle happens to ship.

This repository runs the compiled predicate as its own hand-owned hook

.claude/hooks/aof/guard-test-isolation.mjs here is the segment- and token-aware predicate – it strips here-documents, splits into segments, tokenises each, and clears on an isolation prefix in any of its spellings – invoked from a PreToolUse entry carrying no aofManaged marker, so aof neither adopts, edits, retracts nor drift-reports it. Its cases are re-homed onto test/repo-test-isolation-guard.test.mjs and registered in the repo suite.

55’s pre-55-hook gap is discharged, by the route actually taken

55’s ledger entry reads discharged (story 87, 2026-08-27) and records that its written discharge condition – an update replacing the file with the compiled member – was withdrawn with the member, and that the file was replaced by hand instead.

86 · The span vocabulary, completed

A story-grained scope that cannot be parsed is refused

aof work next throws invalid-scope (400) for any scope matching ^\d+/ that is not a story ref or a span – 44/01-03x, 44/, 44/01-, 44/01-02-03 and the en-dashed 44/01–02 each name the admitted forms in the refusal instead of silently walking the whole stream.

find and next answer for the same vocabulary

The two surfaces agree about every story-grained ref: what findWork resolves, nextWork scopes to, and what findWork answers [] for, nextWork refuses – there is no shape one admits and the other widens.

A bare story ref is a scope

NN/SS is the one-story span NN/SS-SS: aof work next 44/01 answers with story 44/01 alone, offers neither another milestone’s stories nor milestone 44 itself for acceptance, and parseStorySpan admits the bare form through the same rule as the span rather than through a second narrowing beside it.

A free-text scope still falls through

A slug scope is unaffected by the refusal, which reaches exactly ^\d+/ – and the driver form 44, the driver range 44-46, the span 44/01-03 and the descending span 44/03-01 (which parses and admits nothing) all answer exactly as they did before.

The next command face resolves a span to the one driver it names

A finished span answers done rather than naming an unrelated held driver; a span whose own driver is held still reports that driver and names its holder; and a driver range keeps its documented over-reporting unchanged.

parseStorySpan is src/work.mjs’s exported, single scope parser

The span shape is defined once and read by three surfaces – findWork, inRange/inSpan, and skippedEntries in src/commands/next.mjs – rather than re-derived in the command face.

The shipped continue.md span branch is pinned by a test

test/work-story-span-scope.test.mjs asserts that the shipped prompt dispatches on a story span, scopes the walk to the span rather than to the bare milestone, and states each of the branch’s three obligations; the pin is non-vacuous – the same assertions applied to a copy with the branch cut out throw.

85 · Records follow the story

Both accepting doors instruct both story records

aof:verify and aof:assimilate-code each instruct the session to instantiate OUTCOME.md from .aof/templates/work/shared/OUTCOME.md and to author a RETROSPECTIVE.md in the story’s own folder, nested or standalone, over the same delivering-type partition.

The one-writer rule is role-derived rather than filename-pinned

test/verify-outcome-per-type.test.mjs admits the commands that ACCEPT an item and refuses every agent prompt, and asserts its own non-vacuity by requiring that the accepting set is exactly verify + assimilate-code and that both match the rule the others must not.

The spike and uat exclusions travel with the authoring permission

Each door names spike and uat as types that carry no OUTCOME.md and states the reason for each, so neither omission reads as an oversight from either door.

A delivered story missing its records is reported by name

aof work doctor emits story-record-missing (warn) for any story at status: done whose own folder carries no OUTCOME.md or no RETROSPECTIVE.md, naming which record is absent; it judges a story on its own folder, so no record above it in a milestone satisfies it, and it raises nothing against a non-story item or against a story at any pre-done status.

The new check cannot reach a gate

story-record-missing is disjoint from CONTROL_FINDING_CODES and is emitted only at warn, so no severity change to it can fail aof work validate or the loop’s doctor rung. Measured on its first real run over this stream: 278 findings, none of them an error.

The check is a pure function of the snapshot

It lives in lifecycleCompletenessGroup (src/work-doctor-coherence.mjs) and answers over a literal snapshot with no filesystem and no clock, yielding byte-identical findings across runs and across processes. It reads the disk that is the subject of its operation rather than the mesh cache, because OUTCOME.md is not in the streamed artifact set and the cache answers for it 0 times out of 277.

The record-completeness backlog is measured and visible

The stream reports 278 done stories carrying no OUTCOME.md and/or no RETROSPECTIVE.md, up from an unreported condition; 81, 84 and 85 are the stories that satisfy the rule.

84 · A story span is a ref

The story-span ref form

NN/MM-PP is an admitted work-item ref: aof work find 44/01-03 resolves it to the story rows 44/01, 44/02 and 44/03, ordered by story number rather than by directory listing order, and a span that names no existing story answers with the empty set at exit 1 on the human face and [] at exit 0 on --json.

A span-scoped walk

aof work next 44/01-03 narrows the walk to driver 44 and to the stories 01..03 within it, and answers with the same readySet/wave/heldSet shape every other scope form answers with. The milestone’s own dependency answer is unchanged by the narrowing: an in-span story that depends on an out-of-span sibling is reported blocked naming that sibling as a full ref.

A span never offers its milestone

No driver-grained offer reaches a span-scoped answer. Every in-span story being done answers { state: "done" } rather than offering the milestone for acceptance; a uat session, spike or chore addressed by a span offers nothing rather than itself; and an un-broken-down milestone addressed by a span offers nothing rather than “needs break-down”. A driver-level depends block still reports blocked at the milestone.

A continue lane for a named subset

aof:continue dispatches on a span as well as on an item type. Its span branch reuses the milestone lane and states three differences from it: the walk is scoped to the span and never widened to the bare milestone mid-walk, an out-of-span dependency is reported and stops the walk rather than being built, and the milestone’s own status: is neither moved nor accepted.

83 · The agent layer’s four bounds

A reporting bar on every reviewer lens

Each of the five reviewer agents (aof-architect, aof-qa, aof-designer, aof-security, aof-compliance) ships a reporting bar in its own prompt – a >80% confidence floor, a four-gate evidence checklist, Blocker/Important/Nit severities, an explicit do-not-flag list, and the statement that a clean review is a valid review.

A declared context contract on the story record

A story record carries reads: and files: frontmatter lists, the authoring prompts populate them, and aof work validate reports a malformed declaration, a missing read path, a drifting anchor, a backslash-spelled path, a section anchor in files:, an entry escaping the project root, and a both-present-and-empty pair as an unauthored scaffold. An absent declaration stays valid; an unwritten files: entry stays valid.

A build wave partitioned by declared writes, in code

aof work next --json returns wave and heldSet beside readySet, computed by partitionReadySetByDeclaredFiles as a greedy stable walk with case-folded collision keys. An unknown or unrefined write set overlaps everything – it runs alone if first and is held otherwise – and the orchestrating prompt is told to obey the result rather than perform the set arithmetic itself.

A review loop bounded by the runtime, not by prose

MAX_REVIEW_ROUNDS = 3 lives in src/loop-bounds.mjs, resolveReviewRounds clamps work.loop.reviewRounds to it, src/commands/loop.mjs owns the persisted counters, and src/work-loop.mjs decides admission, deduplicates structured Blockers across resume, stops on a non-decreasing Blocker count, and refuses a fourth round. A bounded stop is reported as its own outcome and never as an accept.

81 · The loop’s bounds survive a grader that takes real time

A grade that runs without stopping the loop

aof work grade <ref> --run spawns the declared rubric through spawnRubricAsync (src/commands/grade.mjs), an asynchronous child that resolves the same { status, stdout, stderr, error, signal } shape spawnSync answered, so the event loop keeps turning for the whole time a runner works: SIGINT reaches onSigint and becomes an operator-interrupt halt naming the item being graded, queued timers fire, and every other run’s heartbeat consumption keeps draining. The spawn stamps no heartbeat of its own.

A grade deadline derived from the window that supervises it

gradeDeadlineFromConfig (src/loop-bounds.mjs) returns min(startToCloseMs, heartbeatMs), and src/commands/grade.mjs resolves its deadline only through that function – it hard-codes no timeout, reads neither work.loop.startToCloseMs nor work.loop.heartbeatMs behind their resolvers, and declares no work.loop.* key of its own, so LOOP_BOUND_CONFIG_KEYS, LOOP_BOUND_VALUE_RESOLVERS and the tuner’s declared ranges are unchanged. On this repository the derived deadline is 900,000 ms, down from the 1,800,000 ms startToClose the grade used to resolve.

A runner that outlasts its deadline is graded, not reaped

A child that exceeds the derived deadline is force-killed and reported as ETIMEDOUT, which reaches compileGrade as the existing runner-timeout code and an indeterminate verdict; the loop halts on grade-indeterminate naming work:grade:runner-timeout as its producer and carrying the deadline that was exceeded, and no run is left running to be reclaimed as stranded. A child that out-talks the capture ceiling is a distinct outcome – ENOBUFS, classified ahead of the shared SIGKILL – so an overflow is never reported as a deadline.

A capture ceiling counted in bytes

spawnRubricAsync keeps both streams as buffers and decodes the concatenation once at the end, so the ceiling is the bytes its name and maxBuffer claim rather than UTF-16 code units, and a multi-byte character split across a chunk boundary is settled rather than left to where the kernel cut it.

One bound on every grade payload that is written

boundGradeFailures (src/work-grade.mjs) is the single function that bounds a grade payload. It lives in the pure leaf, imports nothing from src/, takes its ceilings and its measure from the caller, refuses to return an over-ceiling payload, never returns an empty one, and appends a truncation entry – a key, not prose – naming how many failures were dropped and whether a message was cut. All four write surfaces call it: the re-driven run’s brief.grade, the cap-exhausted report line, the fix transport’s ## REVIEW FINDINGS block, and the operator’s rendered verdict, whose former failures.slice(0, 20) is replaced by the call rather than left beside it. The entry ceiling is GRADE_FAILURE_MAX_ENTRIES = 20 (the operator render’s own literal, promoted) and the character ceiling is 70’s PHASE_BRIEF_MAX_CHARS; each surface measures the payload as it will be written, including the gate key a statement entry gains after the bound returns.

The GradeRecord itself is unbounded, and still carries the whole truth

compileGrade returns the runner’s failures verbatim and whole, aof work grade <ref> --run --json carries every one of them, the record’s key set is exactly the one it carried before, no tenth GRADE_CODES entry is coined, and 54/ADR-005 §2’s ratchet is untouched.

A re-drive the resume path reconstructed declares that it carries no grade

drivenRow (src/commands/loop.mjs) emits graded: false and gradeAbsence: "reconstructed-on-resume" for a drive the resume path rebuilt – a key a consumer branches on rather than prose it parses – and only where a rubric is declared, so a repository with no work.rubric gains no byte. No grade is fabricated, the pre-interruption grade is not carried forward onto the new tree (it stays readable where it was recorded), and a resume launches no child process. LoopState keeps its ten top-level keys in their order and actShape()’s whitelist is untouched.

The fix transport carries the transport’s own keys, and the grade rides beside it

pendingFixes is 70/04’s declared shape verbatim, and ctx.loopDrive.fix is that shape – no GradeRecord is among its keys and the driver’s registered input schema is unchanged. The grade travels in a separate pendingGrades map keyed by ref, read at exactly one place: the seam that writes brief.grade on the run being started, through transitionRunStart’s edge.brief. src/effects/run-transitions.mjs and src/run-store.mjs are passed through and unedited. All four sites that prepare a pending fix – the gate re-drive, the progress reset, the progress continue and the resume reconstruction – now prepare it the same way, so every graded entry names work:grade as its producing gate on every branch and a validate finding is told from a graded case by a key.

80 · Every item that delivers says what it delivered

The OUTCOME template has one home, and it is filed under no type

The template ships once at src/bundle/templates/shared/OUTCOME.md and renders to .aof/templates/work/shared/OUTCOME.md; aof work update carries a delete action for the milestone-filed copy, so an existing install ends with one copy of the grammar rather than two. This supersedes m39’s delivered statement that the template ships at src/bundle/templates/milestone/OUTCOME.md and .aof/templates/work/milestone/OUTCOME.md – neither path exists.

aof:verify authors an outcome for a milestone, a story and a chore

Each of the three carries an OUTCOME.md in its own folder authored at Accept, whether a story sits under a milestone or is parentless; a spike and a uat carry none, and both exclusions are stated in the verify prompt rather than left as omissions.

A chore states what its ticking made true, without becoming a story

A chore’s deliverable stays its ticked ## Definition of Done, and its outcome adds ## Delivered alone – the accept path for a chore reaches the shared template from its own dispatch branch, not through the numbered process steps a chore never enters.

A milestone’s outcome is authored, never a concatenation of its stories’

The verify prompt directs a milestone’s outcome to state what is true at the milestone level and to cite a story’s whole capability as m<NN/SS>/<id>; the aggregation happens in the index, where buildRecords unions every item’s records into one recall surface.

The memory index reads an OUTCOME.md from any item that carries one

The scan is the whole item set rather than type === "milestone" && parent == null, so a parentless story’s, a nested story’s and a chore’s capability and gap records enter the same recall surface a milestone’s do.

A delivery record cites its item by ref

A record carries its item’s full ref in item ("39/02" for a nested story), so a recall renders the citation as m39/02 – the ref the citation grammar already expects – rather than m02, which is a different, real milestone.

An --item or --only scope matches the item and its subtree

A milestone-scoped recall or rebuild carries its own stories’ delivery records, from one subtree rule in the zero-import leaf src/work-ref-scope.mjs shared by work doctor’s inScope and both memory scopes; an unresolved scope returns an empty block at exit 0.

An OUTCOME.md is unbudgeted, whatever type carries it

doc-over-budget covers SPEC.md, ARCHITECTURE.md, STORY.md and *.feature and gains no kind here, so a second Accept-time artifact anchors no finding at itself.

79 · The committed loop graph

The committed loop graph

wiki/work/loops.md is a tracked markdown document at the work-directory root holding the loop registry’s Mermaid graph in a fenced block, a ## Health summary (declared records, declared edges, error/warning totals and the per-check split), a leading `` marker naming its own regenerating command, and no frontmatter at all.

work:loop-document – the registry’s one writer

A registered command whose bare face composes the document and emits it touching no disk, whose --write is the only door to the filesystem, and whose input contract accepts no caller-supplied output path – the single home is derived from work.dir and cannot be redirected.

Byte-identical regeneration

Running aof work loops document --write against an unchanged registry rewrites the same bytes and reports changed: false, in-process and across separate processes; the composer reaches no clock, no filesystem and no environment, so the same registry composes identically from any working directory.

The drift gate

test/arch/acd-loop-document-current.test.mjs reds the test suite when a loop record is edited without regenerating the document, naming the committed document and the command that regenerates it; it reads and renders only, repairs nothing, reports an absent document rather than passing it, and gates the suite alone – no item transition, acceptor door or doctor severity reads it.

The frozen lists admit the writer, and the board does not

work:loop-document is a member of the frozen WORK_IDS census and of the BOARD_DEFERRED carve-out with its deferral reason recorded beside chore 64’s four read verbs; no /api/work route is served for it and ui/ carries no token naming the command, its modules or its id.

The registry family is untouched

The writer’s modules (src/loop-document.mjs, src/commands/loop-document.mjs) match neither of 52/FF-5201’s discovery patterns, that gate’s expected module list and read-only sweep are unchanged, and src/commands/loops-graph.mjs is byte-unmodified – the frozen renderer is imported, never restated.

78 · The loop execution record

aof can now show the loops that RAN, per work item, as a committed document

Before this milestone aof could render its loop registry – the same nine framework-wide nodes in every repository, to stdout only – and could not answer “what ran for item 54”. It can now: one registered command produces a per-item execution record, and --write commits it beside the work. This is the additive face 52 pre-authorised (52/ARCHITECTURE.md:869-870) and it changed no byte of 52’s frozen renderer.

Execution is legible against DECLARATION, not merely reported

The record states observed facts against what the registry declared: cycles against the declared ceiling, phases entered, attempts, terminal outcome and stop reason. SPEC.md opened on the complaint that “a ceiling: uncapped loop and a capped one must not produce identical records; today they do.” They no longer do – the four ceiling states are distinguishable in the model (m78/00/) and, because that is the property that regresses silently, in the rendered BYTES (FF-7806).

Absence is delivered as a finding rather than as an empty page

The record names three classes of gap – a loop that ran and is not declared, a declared loop that never ran, an authority that cannot be resolved – under their own headings, and emits no heading and no “None” placeholder for a class with no gaps. Measured on this repository at the accept gate: zero coverage, zero engagements, and all seven registry loops reported under declared-never-ran. The milestone’s central claim is therefore demonstrated in the one condition that matters – the condition where there is nothing to show.

A machine-readable place for a human to say the execution was acceptable

The arc 52–63 had no machine-readable sign-off of any kind; @uat tags and prose in ## Accept decision were the whole vocabulary. There is now a frozen block (m78/03/) with a shape a check can read, and a signature in it survives regeneration verbatim (m78/02/) – which is what makes the document safe to regenerate and therefore worth committing.

The whole thing is a face, with the writer as its only door to disk

Every execution fact reaching a consumer is computed from the run records; nothing recovers one from the document. The record can be tampered with and the command’s answer does not move by one field. Regeneration is byte-identical on unchanged inputs across processes and working directories, so a diff in a pull request means an input changed. Cited whole at m78/02/ and m78/00/.

The milestone stayed outside the registry’s read-only law without weakening it

A milestone whose deliverable is a WRITER landed inside a domain whose defining law (52/FF-5201) is that it does not write. The modules take the execution family’s name, FF-5201’s discovery patterns match none of them, its expected six-module list is unchanged, and its read-only sweep is re-measured independently rather than re-run. The law is intact and the writer is provably outside it.

Ten declared controls, all landed, all red-probed

Every entry in this milestone’s ARCHITECTURE.md fitness register resolves to a file in the runnable test tree, is green, and carries a red probe recorded in VERIFICATION.md – 24 probes in total, each applied to the working tree, run in a fresh process, reverted, and the restore confirmed byte-identical by sha256. One probe did not go red and is recorded as a finding (F-78-H) rather than replaced with one that did.

78/00 · The execution projection

The execution projection

src/loop-record.mjs turns a loaded loop registry model and one work item’s run records into the item’s execution facts – engagements, cycles against declared ceilings, phases, attempts, outcomes, join coverage and gaps – as a pure function over injected values.

Engagement identity by loopRunId

Two engagements of the same loop are two rows: runs are grouped by brief.loop.loopRunId, so separate engagements of one loop are never fused into one.

Four distinguishable ceiling states

bounded, none, unknown and uncapped are separate states carried in the model, and two engagements identical in every observed fact but differing in ceiling are not equal models – “terminates by construction”, “nobody has said” and “deliberately unbounded” no longer collapse to one word.

Join coverage as a stated measurement

Every model carries how many run records were found, how many carried a loop declaration, and the ratio. A zero-coverage model over 14 undeclared runs is a different object from a model over no runs at all, so an empty answer is distinguishable from a broken projection.

Three separately named gap classes

ran-undeclared, declared-never-ran and authority-unresolved are reported under their own names in a frozen order, no gap appearing in more than one class, and gaps within a class ordered by their subject’s code-unit sort.

A structurally enforced purity boundary

FF-7801 (test/arch/acd-loop-record-projection-pure.test.mjs) holds src/loop-record.mjs and every module it directly imports free of node:fs, a clock and a child process, and pins the direct-import set to ["src/loop-bounds.mjs"] so a new import re-opens the question.

78/01 · The item-scoped renderer

The item-scoped execution graph

renderExecutionGraph (src/loop-record-render.mjs) draws a Mermaid flowchart LR of the loops one item ENGAGED plus the authorities those loops cite – a 17-record registry whose item engaged 2 draws 2 loops and none of the other 15.

A two-pass scope rule that cannot readmit the framework-wide picture

An authority is drawn because a scoped loop cited it; a registry-declared edge is drawn only when both of its endpoints are already in the finished scope, so an edge to a loop the item never engaged adds no node.

The execution document

renderExecutionDocument returns Markdown wrapping a fenced mermaid block – frontmatter first and the generated-file marker after it (the F-73-G order), then the coverage line, the per-engagement facts, the graph, and one heading per non-empty gap class.

Coverage stated on the page, always

The document carries how many runs were found, how many carried a loop declaration, and the percentage, whether coverage is zero or complete; a zero-coverage document says “No loop ran for this item.” in words rather than rendering blank.

Absence rendered as absence, not as a placeholder

A gap class with no members renders no heading, and an empty scope still renders flowchart LR – an empty answer is distinguishable from a renderer that failed to run, and no “None” placeholder is emitted.

Four ceiling states legible in the rendered bytes

A capped engagement and a none / unknown / uncapped one render pairwise-different bytes, each non-numeric state carrying its own remedy in words, and a numeric bound renders the observed cycles against it – with , over the bound when exceeded and no claim that the bound was respected.

In flight is not failure

An engagement with no terminal outcome renders as in flight; a settled one renders its outcome and, when one is carried, its stop reason.

One glyph table across all three faces

FF-7802 (test/arch/acd-loop-record-renderer-additive.test.mjs) holds the new renderer importing KIND_SHAPES from src/commands/loops-graph.mjs, spelling no declared kind’s glyph itself, and declaring no second table – and holds src/commands/loops-graph.mjs byte-unmodified, asked of git against HEAD rather than of a pinned digest a maintainer would re-stamp.

A rendering with nothing in it that changes by itself

The document carries no generation timestamp, no absolute filesystem path and no host or node name, so two renderings of one model in one process are byte-identical.

78/02 · The record command

aof work loop-record <ref> – the per-item execution record as a command

work:loop-record (src/commands/loop-record.mjs) resolves a work item, projects its execution model from the run records and the loop registry, and emits the composed document. It is registered once in the shared command core, resolves as aof work loop-record <ref> with no ladder, requires a ref, and accepts exactly two inputs – ref and write.

A read face that touches nothing

Run without --write, the command composes and returns the document’s bytes and writes no file. The loop registry is left byte-identical, an item with no run records is still answerable, and a zero- coverage answer exits successfully rather than as an error.

--write as the only door to disk

The record lands at EXECUTION.md inside the item’s own folder, on a path derived from the item that no caller can redirect – there is no output-path input to supply. Exactly one file is created or modified per run, through one shared atomic writeText, and nothing under .aof/ or at the work directory’s root is touched.

Byte-identical regeneration

Running the command twice on unchanged inputs produces the same bytes, in one process, across two fresh processes, and from a different working directory. The read face composes the same bytes the writer writes, so a regeneration can be reviewed before it is taken, and a non-empty diff in a pull request means an input moved rather than that somebody ran the command.

A human signature that survives regeneration verbatim

The writer is a read-modify-write: it parses the existing document’s sign-off table, carries every signed row forward as its own line – the operator’s spacing, date format and wording untouched – and re-derives every other line. A signature survives even when the facts it signed have since changed. A row counts as signed only when signer, date and verdict are all filled; an untouched – placeholder never does. A document the parser cannot read is refused rather than regenerated, so a malformed record loses no signature.

The record is a face, never a second truth

EXECUTION.md’s basename is named in exactly two modules – this writer and 78/03’s checker. The document is read exactly once, and the only thing parsed out of it is the sign-off block. Every consumer of an execution fact computes it through 78/00’s projection from the run records; the renderer is imported in one place and only ever written to. A record whose fact lines have been rewritten to lies does not move the command’s answer by one field, and a regeneration re-derives every tampered line away.

A board-deferred command, by recorded decision

work:loop-record is a member of the frozen WORK_IDS census and of the BOARD_DEFERRED carve-out, no /api/work/loop-record route is served, and ui/ gains no reference to it. It ships without an ACD bundle command wrapper, and the CLI↔bundle parity control demands none – that control is scoped to the work:insert-* family and names neither this command nor story 79’s writer.

The registry family is provably untouched

The module takes the EXECUTION family’s name rather than loops-record.mjs, so it matches neither of 52/FF-5201’s discovery patterns; FF-5201’s expected six-module list is unchanged, and its read-only sweep over that family is re-measured independently rather than re-run.

78/03 · The signature and the doctor lane

A sign-off block in a shape a check can read

Every EXECUTION.md ends with a frozen block: the h2 ## Sign-off, the header row | loop | signer | date | verdict |, and the divider |---|---|---|---|. One row per engagement, each naming its own loop, and the block is emitted even for a document with no engagements. A human signature in this record is now findable by a program rather than only by a reader.

The loop id stands alone in the first cell

Following m66/ADR-001’s positional rule, a sign-off row’s first cell carries the loop id and nothing else. A first cell carrying prose declares nothing, and both the writer and the checker refuse such a document rather than reading a signature out of it. The block is deliberately NOT a REGISTER_BLOCKS member and this milestone added no new member to m66’s closed ID_FORMS set – the five it pinned are still five.

Signed means complete

A row is signed only when signer, date and verdict are all filled. A partially filled row is unsigned, an empty row is unsigned, and the – placeholder every freshly written record is full of is unsigned. A rejected verdict is a signature, not an absence.

Three independent copies of the frozen shape, held byte-equal

The literals live in the writer (src/commands/loop-record.mjs), in the checker (src/work-doctor-loop-record.mjs), and a third time in FF-7809’s own source. The checker’s direct imports are exactly ["node:path"], so the instrument cannot inherit its subject’s opinion of the shape it polices, and it drags neither the registry loader nor the filesystem into a work-doctor* module’s import closure. The freeze is round-tripped as well as compared: the block the writer emits is parsed back by the checker’s own parser.

A doctor lane that reports the record and gates nothing

loopRecordLane (src/work-doctor-loop-record.mjs) is registered once in CHECK_GROUPS, appended after every lane that existed before it, and emits four codes – loop-record-unsigned, loop-record-part-signed, loop-record-stale, loop-record-malformed. It reads the record when one is present and never demands one: an item with no record produces no finding.

Every finding is a warning, structurally

The lane’s severity is one module constant and it names no error anywhere; it does not consult the acceptance horizon, so a record on a done item reports the same warning it reported while the item was open. Its four codes are a different frozen array from CONTROL_FINDING_CODES, and DOCTOR_GATE_CODES is derived from that array – so no code of this lane can reach the gate ladder even if one were hardened. No status, validate or acceptor door names the record, its signature or its codes; the doctor engine reads the record at its one impure edge and renders no verdict about it. An item carrying an unsigned record moves to done and work:validate stays green.

Staleness answered from the runs, not from the document

The lane’s staleness check compares the record’s engagements against the engagement list projected from the item’s run records – it does not parse the document’s own fact lines. An orphaned signed row is not staleness, a record behind its runs is reported with what is missing named, and with no engagement list available the lane makes no staleness claim at all.

77 · Harness audit – doctor for the machine, not the record

aof work audit audits the machine, not only the record

aof work audit carries seven lanes and 33 finding codes, four lanes and seven codes of them added here, and its subject is now the harness that produces the records rather than the records themselves – the sibling aof work doctor never grew, and 59/FF-5905 still refuses a shared code between the two.

The rules travel

Every rule this milestone adds runs against an audited project’s installed layer rather than against this checkout, and the command’s own child programs resolve against the toolkit root while its subject resolves against the audited root – so aof work audit --strict runs in a governed project that is not an aof checkout at all (m77/04).

The audit is clean over its own repository, and every error leg measures zero

aof work audit --strict exits zero here: 337 findings, all warn, with every error leg of the seven codes added measuring zero on arrival. The 337 are a standing measurement of this repository – 262 duplicated prompt-document pairs, 9 capability gaps, 6 unwired seams, 5 undeclared bounds and 1 bound off the reference – not a backlog the command failed to clear.

The command’s report states what it could not see

Every lane declares a read floor and every text-basis sweep returns a limit naming its own blindness, both enforced from the registry rather than per lane, so a lane that looked at nothing cannot report the same thing as a lane that found nothing (m77/00, m77/02, m77/05).

Three bespoke local gates became travelling rules

The capability gap, the duplicated instruction and the hook twin were each gated in this repository alone before this milestone; each now has a form that runs wherever the bundle is installed, and the repo-local gates they came from (72/FF-7206 in particular) are unweakened and still in service.

A versioned reference corpus ships in the payload

src/harness-reference.mjs carries six bounds other systems declare, each with a source URL and a checked date, and the audit joins a project’s declared bounds against it offline – the corpus travels with src/, and the program that rewrites it is reachable from no CLI door (m77/03).

The audit’s code space is derived, so a fourth lane cannot arrive outside the disjointness check

AUDITABLE_CODES is computed from REPORT_LANES rather than enumerated, and the control that asserts disjointness from doctor’s codes reads that derived set – a lane added tomorrow is inside the check on arrival (m77/05).

Two ledgered debts are closed

TECH_DEBT items 70 and 72 are discharged: every program the audit family spawns is under src/, named in one enumeration, and resolved against the toolkit root.

72 · The inner loop

The inner loop’s tools are declared by the project and launched by aof, and aof imports nothing of the project’s

One boundary holds across all five stories: the test runner and the worktree prepare step are work.test and work.worktree.prepare declarations compiled by one module (m72/00), every child process this milestone starts comes from runBounded with a declared deadline, and no test module enters the aof process on any path (m72/02, FF-7204). The framework works in any repository that declares its toolchain and refuses, by coded message, in one that does not.

Selective testing is a command, not a per-agent chore

aof test --scope impacted|file|all replaces the throwaway array-importing script: the selection (m72/01) reads the code graph and widens on every unknown, the face (m72/02) prints failures only from both streams, and this repository’s own runner accepts --only through its one execution loop. The result is a report that no transition door consumes.

A hook costs a hook’s worth of boot

aof session ping, fired on every prompt, loads a 25-module closure instead of the 277-module command registry, and fires once per event rather than twice (m72/03).

A worktree arrives prepared, and nothing is ever linked into one

Dependencies reach a worktree by the declared install at every materialisation door, a failed install removes the tree it half-filled before it throws, and FF-7207 ratchets the TECH_DEBT-36 hazard shut over all three worktree roots (m72/04).

Two of the six SPEC levers are declined, not missing

The pre-apply edit gate and the blocking write-thrash guard were declined on evidence (ARCHITECTURE.md#ADR-006, TECH_DEBT item 87), and the SPEC’s PreToolUse output-rewriting hook was replaced by the command’s own output contract (ADR-003). No hook of any kind ships from this milestone.

72/00 · The declared toolchain

The test runner aof launches is the project’s declaration, and there is no default

src/work-toolchain.mjs is the only module in src/ that reads work.test.command, work.test.args, work.test.selectArgs, work.test.roots, work.test.deadlineMs or work.worktree.prepare, and no program name a declaration may supply – npm, vitest, pytest, yarn, pnpm – is spelled anywhere in src/ in an executable position. An absent declaration is test-runner-undeclared naming the key, and no program is substituted for the one nobody declared.

The three ways a declaration fails are three different answers

test-runner-undeclared (nothing declared), test-runner-declaration-invalid (a present declaration with a bad field, the message naming the field) and test-runner-unresolvable (a well-formed command that resolves to no file) are distinct codes. Every field fault within one declaration shares the one code and is separated by the field its message names.

The program is resolved to a real file before anything is launched

A bare name is resolved against PATH with PATHEXT honoured case-insensitively, an absolute path is carried unaltered, and a relative one is resolved under the project root – each checked to be an existing file, not merely an existing path. The resolution sits in FRONT of runBounded’s argument-vector door, so that door refuses exactly what it refused before and a command that resolves nowhere is a coded refusal rather than a spawn that fails later and further away.

A shim is undeclarable, and the refusal says what to declare instead

A resolved .cmd, .bat or .ps1 is refused by the resolver with a remedy naming an interpreter plus its script, rather than left to the seam to report as a failure to start.

Every child process this milestone starts comes from the one bounded seam

src/work-toolchain.mjs imports node:child_process nowhere, names no exec/execFile/execSync/spawnSync/fork, passes no shell: option, and hands runBounded an argument vector plus the deadline the declaration states rather than the seam’s own 60,000 ms default. The injected launch parameter is named launch, after no process API.

Selection reaches the runner through one expansion rule

{file} expands in place, once per selected file; no other token is a placeholder – {root}, {files} and {suite} are carried through as literals. Zero selected files yields an empty selection, so a flag runner never emits a flag standing without its operand.

A run that produced no verdict is never reported as a passing one

exited maps to the run’s own verdict and its observed exit code; deadline-expired and not-started are each reported as themselves, with the bound applied or what was attempted, and each exits non-zero. The three are distinguishable from each other and from a run that exited non-zero.

The prepare declaration is compiled by the same module and differs only in absence

work.worktree.prepare absent is a silent no-op – no refusal and no warning – while a present-but-malformed one is worktree-prepare-declaration-invalid raised where the declaration is compiled, not at the launch, and told apart from absence. A prepare command resolving nowhere is worktree-prepare-unresolvable.

This repository declares its own test runner

.aof/aof.config.json carries work.test as node scripts/test.mjs, selecting with ["--only", "{file}"], rooted at test, bounded at 900,000 ms, reporting tap – an interpreter plus a script, never a shim. aof is now an installed project against its own boundary.

72/01 · The selection

A changed file reaches the suite files that can break it, through the graph the repo already builds

selectSuites (src/work-test-select.mjs) answers a changed set with the suite files coupled to it, taken from graphify-out/graph.json through the shipped normalizeGraph and computeImpact. It reads the artifact and never builds one: no graphify invocation, no second artifact read, no second parse and no child process exists anywhere in the module or in src/graph-impact.mjs.

An unknown widens the selection to the whole suite and says which unknown did it

A changed file the graph cannot account for produces scope: "all", the whole suite selected, and an entry in widened[] naming that file and one of exactly four reasons – no-graph, not-in-graph, no-registered-dependent, graph-unreadable. The vocabulary is frozen at four in the module itself, and the rule is checkable from both sides: wideningRuleProblems refuses a widened result that selected a proper subset and refuses a non-widened result carrying a changed file that never resolved.

Presence outranks the union, so a test file created this turn widens rather than selecting itself

A changed file absent from the graph widens even when its own path satisfies the suite predicate. The union of the changed file with its dependents is taken first and the path predicate last, so a changed file that IS a registered suite is in its own selection – but only after presence has been established.

No option suppresses a widening

selectSuites accepts projectRoot, changed, allSuites and roots and nothing else. No key that suppresses, narrows or overrides a widening is accepted, no --no-widen / --strict-scope / --narrow / --assume-fresh / --skip-widening is spelled anywhere in the story’s modules, and an unknown suppressing key passed anyway leaves the widening standing and named.

Every result carries the artifact’s own instant, or exactly null

builtAt is graphArtifactBuiltAt’s reading of the artifact’s file mtime on every path where the artifact was read – including the two widening paths that read one. On no-graph there is no file, and on graph-unreadable the file’s perfectly good mtime is discarded; both report exactly null, with the reason carried in widened[]. No module in this family reads a clock.

Selection is pure across calls

The same changed set against two different artifacts answers twice, differently, in one process, in either order. Nothing is cached – not the normalised graph, not the impact walk, not the answer.

A selection is a report and decides no transition

Every result carries gate: false unless the whole suite was selected AND nothing widened. Nothing in the tree consumes it, so 09/ADR-004 is not crossed by this story.

A selected suite the runner does not assemble is reported, in the census’s own words

registrationReport imports registrationDecision from src/work-audit/census.mjs and carries its verdict verbatim – the same code and the same message, the census’s audit-suite-unregistered / audit-suite-imported-never-spread / audit-runtime-membership-unavailable vocabulary and its baseline’s carried reasons. No second derivation exists in this story: no pattern over a suite import line, no spread-row matcher, no second unregistered baseline, no second read of the assembled array.

The changed set is read from git through the one bounded seam, and three ways of having nothing to select are three answers

src/work-test-changed.mjs runs status --porcelain, diff --name-only and rev-parse --verify through runBounded with a 60,000 ms bound, keeping untracked files and both ends of a rename. The default base is the working tree; no default branch is inferred, and no branch name – main, master, origin/HEAD, @{upstream}, symbolic-ref, merge-base – is spelled anywhere in it. since-rev-unresolvable, changed-set-empty and changed-set-unreadable are three distinct refusals, none of them a fifth widening reason.

computeImpact has a home below the command layer, and its face is byte-equivalent

src/graph-impact.mjs holds the pure coupling core; src/commands/graph-impact.mjs re-exports it, so both of its previous importers are unchanged and graph:impact behaves exactly as it did. matchFile, declared in the command and never used, did not come with it. Both frozen graph-reader allowlists now name src/work-test-select.mjs – and not src/graph-impact.mjs, which takes an already-normalised graph and names no reader symbol.

72/02 · The test command’s face

aof test is a registered top-level command, the one face this milestone adds to src/commands/

src/commands/test.mjs is registered in src/command-core.mjs under the id test with the one-word route ["test"], outside the work: namespace; it composes 72/00’s declaration and bounded launch with 72/01’s selection and changed-set reader, and holds nothing of its own beyond one frozen result object and two faces over it.

The scope forms are three, and a fourth is a refusal

--scope impacted, file and all are the only forms. An absent, differently-cased or unknown scope is test-scope-unrecognised; file with no file named is test-file-scope-names-no-file. Both are results rendered on either face with nothing selected, no runner launched and exit 1 – never a fallback to one of the three.

The report is failures-only by default and is derived from both streams and the exit code

Each failing case is printed with its assertion text, then one summary line – selected/total suites · scope · graph built <builtAt> · <widenings> · may / may not stand as a verdict. Stdout and stderr are concatenated before the one shipped TAP reader (normaliseReport) parses them, so a not ok this repository’s runner writes to stderr is read. A green-reading report beside a non-zero exit is reported as a contradiction and exits 1; a report no normaliser can read is reported as producing no verdict and exits 1.

One result object, two faces

--json returns the frozen result itself and the human face renders from the same object. --verbose adds the passing rows and removes no line the quiet run printed.

Every result says whether it may stand as a verdict, and only an unwidened whole run may

gate is true only when the scope ran as all and nothing widened; every impacted, file and widened result carries gate: false. No status, accept, merge, loop or audit door invokes the command – censused by invocation shape over src/commands/item-status.mjs, src/work-doctor.mjs, src/work-loop.mjs, src/work-audit/** and src/bundle/** – and the registered command declares no cli.launch.

No test module enters the aof process

None of the nine modules the command reaches resolves a specifier under a declared test root when imported alone in a fresh process, and the family holds no dynamic import of a test path.

This repository’s runner runs named suites additively, through its one execution loop

scripts/test.mjs --only <file>… imports each named suite file, takes every runner-shaped array it exports, and runs them through the same runSuite the full path calls – parameterised in place as runSuite(tests, { lanes = true }), the integration, cargo and shell lanes skipped by one early return on a selection. A file that is absent, does not evaluate or exports nothing runnable is reported not ok by path and the run exits 1; --only with no file refuses naming the option; the assembled array is not read, reordered, restructured or appended to; and importing the runner runs nothing, whatever the importer’s argv holds.

REG-MUT-11’s residue is re-derived and no leg is weakened

test/arch/acd-loop-suite-registration.test.mjs differs from HEAD by exactly one line, :269 (RUNNER_RESIDUE); the four region pins, residueFloor, the integration-lane count, both registration patterns and the resolve-on-disk leg are byte-unchanged.

72/03 · The cold boot

A session verb boots without the command registry

src/cli.mjs’s static import closure holds neither src/command-core.mjs nor src/spine/face.mjs – 25 modules, down from 277 – and src/commands/mesh-session.mjs’s closure holds neither and no dynamic import of them. The registry and the generic face are reached by await import() only on the dispatch arms below the session arm and inside helpText(), which is async and awaited at both of its call sites.

The session arm is the first statement of run()

command === "session" dispatches before helpText(), before the route table is resolved and before any registry binding is used; src/work.mjs stays a static import.

aof session start, ping and end behave as they did

Each verb records, refreshes or removes its own session leaf exactly as before, and the refusals it made before – invalid-input, unknown-subcommand, session-arg-missing-workspace, session-arg-missing-repo, session-cwd-not-workspace – it still makes, with no record written.

This repository’s settings carry one session hook per event, and the survivor is the managed one

.claude/settings.json holds one SessionStart, one UserPromptSubmit and one SessionEnd entry, each carrying its aofManaged marker; the three hand-authored copies are gone (six aof session … commands at HEAD, three now), so the presence ping shells once per prompt. The unmanaged Bash|PowerShell guard entry is present character for character.

No unmanaged settings entry may duplicate a managed one, and the merge is untouched

FF-7206 pairs entries by event AND matcher, compares resolved invocations (command plus args, ${CLAUDE_PROJECT_DIR} unexpanded, either spelling), admits the operator’s own guard with a managed neighbour beside it, and imports nothing from src/claude-settings.mjs. isAofEntry and spliceSettings are unchanged.

FF-7205 asserts structure and never a duration

The control walks both closures through the shipped importClosure, proves the walk transitive and non-empty against a planted fixture and two-sided floors, and censuses its own source for clock readings, elapsed-time subtractions and duration bounds.

The delivered drift lane pins a literal

test/bundle-claude-session-hooks.test.mjs’s reference for the hand-wired session command is the literal "aof session start", so a bundle changed to another spelling still reds; the delivered feature 49/07/00 is untouched, the supersession of its premise recorded in ARCHITECTURE.md#ADR-005 §5.

72/04 · The prepared worktree

A worktree is prepared through the project’s declared program, at the one choke point every door funnels through

src/mesh-worktree.mjs runs the compiled work.worktree.prepare step inside the tree each materialisation door returns – addWorktree, reuseWorktreeOnBranch (the dominant continuing-item path, now routed through the private runWorktreeAdd with its two thrown codes and messages byte-unchanged), addSessionWorktree and addDispatchWorktree – through runBounded, with the declaration’s own deadline armed.

An absent declaration is a silent no-op; a malformed one is a compile-time refusal

No work.worktree.prepare means no step, no error and no warning. A present declaration naming no command, with a non-list args, a non-positive deadlineMs or a command resolving nowhere is worktree-prepare-declaration-invalid or worktree-prepare-unresolvable, raised by src/work-toolchain.mjs before any door runs and told apart from absence.

A failed prepare is loud, coded, and leaves no half-installed tree behind

A non-zero exit, a deadline expiry and a failure to start are worktree-prepare-failed, worktree-prepare-deadline-expired and worktree-prepare-not-started; on any of them the module git worktree remove --forces the tree it just created and throws, with the prepare’s stdout and stderr riding the thrown message. A teardown that itself fails is reported on the module’s degrade channel, never swallowed.

The prepare outcome is observed, not returned

Every door still returns a bare path string; the outcome surfaces through the module’s existing options observer idiom, and the compiled declaration can be injected as options.prepare.

FF-7207 censuses every src/ module for symlink, symlinkSync, link, linkSync, mklink and New-Item -ItemType Junction whose path or target reaches a worktree, and for rm, rmSync and rimraf with recursion over a path derived from the worktree seam. src/mesh-worktree.mjs reaches no filesystem delete at all, and removal is git worktree remove, forced or not.

A worktree is recognised by derivation, over all three roots

isInsideMeshWorktree(projectRoot, path) composes the three shipped isUnder…Root predicates with resolve(path) !== resolve(root), so a keyed child under the assignment, session or dispatch root is inside, and the bare root, a lookalike sibling and a foreign project’s tree are not. The three shipped predicates are byte-unchanged.

71 · Loop discipline – one review round, and findings become work items

Every loop in the ACD prompt layer now terminates, and says so where an agent reads it

Before this milestone the build step had a written terminator and the review step had none – no round cap, no re-review bound, no exit criterion. Both halves now terminate in the shipped assets and in their fifteen rendered runtime copies: build stops on green-or-no-progress (m71/00), review runs one round by default with a named-Blocker second and a hard third (m71/02), the free gate ladder runs before any reviewer is spawned and again after every fix round, and a red ladder consumes no round. The prompt layer and the runtime speak the same bounds in the same words.

A bound stated anywhere in the bundle names its home, and equals it – enforced across the whole bundle

This is the milestone-level property no single story holds: FF-7101 reads every asset under src/bundle/, resolves every work.loop.* key it names through LOOP_BOUND_VALUE_RESOLVERS, and binds every stated value to that bound’s own answer – to the exported clamp where a clamp identifier stands in the same sentence, and to the config key otherwise. A numeral that names no key and no clamp is out of reach by construction, and the control asserts that it is. Combined with 69’s acd-loop-cap-single-home over src/**, there is now no home for a loop bound in this repository that some control does not read.

The loop’s creation authority is bounded, and the bound is structural rather than numeric

An uncapped round loop became a capped one with a queue behind it. The loop creates exactly one item type (chore), in exactly one place (top level, invisible to the walk that created it), appending after the highest number and shifting nothing, idempotent per finding – see m71/01 for the verb, the router and the engine. It is in live use, and that is the milestone-level fact: three chores in this stream now carry a **Promoted from review finding:** back-reference – 88 and 90 from build/review closes, and 89 from this milestone’s own gate. Before this milestone, grep -rl "Promoted from review finding" wiki/work/*/CHORE.md answered nothing.

The milestone’s own gate was refused by a rule this milestone wrote, and the queue is what cleared it

71’s first gate pass failed its own accept rule: ARCHITECTURE.md declared FF-7106 and the four-story partition gave it no owner, so a declared control did not resolve. The declaration was kept rather than dropped – chore 89’s Definition of Done cited the register as its specification – and the chore was executed to done, landing the control, whereupon the second pass cleared. The loop closed on itself: a finding the cap stopped chasing became a named work item, which was worked, which cleared the gate that raised it.

The design lane can no longer burn spawns on a surface it cannot render

m71/03 states the precondition and the skip; the milestone-level fact is that this repository is itself the unconfigurable case – .aof/aof.config.json carries no work.ui key – and the lane now costs one recorded INCONCLUSIVE instead of three breakpoints × N surfaces of failed invocations plus two agent spawns. The cached-Chromium path that lived in seven records and no prompt is now the only render the bundle names, and 07’s npx playwright clause is superseded in 71’s own contract with 07’s delivered .feature files untouched.

Six declared controls, six resolving, seventeen planted defects

The register is fully discharged: FF-7101, FF-7102, FF-7103, FF-7104, FF-7105 and FF-7106 each resolve to a registered file and each carries a red probe in VERIFICATION.md recording what was changed to make it fail and the message observed – seventeen plants in all, every one run in a fresh process and reverted with sha256 proven equal before and after. aof work doctor 71 reports no control-unresolved at either severity.

71/00 · The build’s terminator, and the free gate that runs before any reviewer is spawned

The build phase has a spoken failure-to-progress terminator

aof:continue’s build step states two terminators and which one it stopped at: success is all three legs together (every task’s @executable scenarios green, typecheck and lint clean, fitness functions passing), and a build whose failing-scenario count stops falling for 2 consecutive rounds – work.loop.buildNoProgressRounds – stops, hands back naming the failing scenarios and the bound, and prints no accept hand-off.

The free deterministic gate runs before any review lane is spawned

aof:continue walks aof work validate <ref> then aof work doctor <ref>, both scoped to the driven item’s own ref, as step 4 – between Build and Review. The first red rung short-circuits, a rung that exits non-zero is a red rung, a red gate is reported as its own outcome rather than as a review verdict, and the ladder re-runs after every fix round without consuming a review round.

The prompt’s ladder is derived from the shell’s, not copied from it

test/arch/acd-prompt-gate-ladder-parity.test.mjs reads the gate ids out of invokeGateLadder (src/commands/loop.mjs) and asserts set equality in both directions plus rung order, so a rung added or removed in the shell fails CI until the prompt agrees.

A loop bound stated in a bundled asset names its home and equals it

test/arch/acd-prompt-bounds-name-their-home.test.mjs resolves every work.loop.* key any asset under src/bundle/ names through LOOP_BOUND_VALUE_RESOLVERS, compares every stated value against that bound’s own declared answer – binding to the exported clamp when one stands in the same sentence, and to the config key otherwise – and requires the three command bound facts (work.loop.reviewRounds, MAX_REVIEW_ROUNDS, work.loop.buildNoProgressRounds) each to name their home. A numeral in prose that names no key and no clamp is out of reach by construction.

71/01 · A finding the cap stops chasing becomes a named work item, or a named question for a human

The review close routes every surviving finding by one ordered rule, as code

routeFinding() and routeFindings() in src/work-loop.mjs are a pure decider over ADR-003’s four ordered questions with a closed answer set – amendment, chore, story, recorded – where a Blocker (chased or outstanding) and a claim that did not reproduce never reach the questions, a Nit is recorded and never promoted, two lenses’ reports of one defect (title and file:line normalized) are routed once, creates is only ever "chore" or null, the story routing is owned by the operator, and no finding id and no finding tag is ever produced.

aof work promote-finding schedules a finding as a top-level chore

work:promote-finding is registered in the command core and reachable as aof work promote-finding with a reviewed ref, a finding title, --remedy, and optional --location/--round: it creates a top-level chore born not-started whose ## Definition of Done is the finding’s remedy plus the no-regression line, whose ## Notes names the reviewed ref, the review round, the finding’s title and file:line and a visible **Promotion key:**, which appends after the highest existing number with shifted: 0, which is idempotent on (reviewed ref + title normalized for case, whitespace and backticks) – including after the chore has been closed – which refuses a missing/unresolvable ref, a blank or unsluggable title and a blank remedy with coded reasons, and which refuses any input outside its declared set (type, at, parent, under, …) rather than ignoring it. It has no /aof: bundle wrapper and is deferred from the board, by decision (ADR-004).

One promotion engine under src/work-promote/, two faces on it

chore-seed.mjs holds the DoD seed and both back-reference shapes; promotion.mjs holds the slug derivation, the append-position resolver, the idempotence key and scan, and the seed writer. src/commands/promote-gap-to-chore.mjs delegates to them with its refusals, codes, rendered lines and --at flag unchanged (39/03’s suite is the control, 9/9 green), and the family imports nothing from ../commands/.

A promotion appends after the highest number, not after the count

appendPosition() answers max(highest top-level number + 1, count). Over this repository’s own stream (87 top-level items, highest number 87) the count-derived default 39/03 shipped would have inserted at 87 and renumbered one item; the engine answers 88 and shifts nothing. Both faces take this default when no position is named.

aof:continue states the triage rule at the review close

The <finding_triage> region of src/bundle/commands/continue.md – and its three rendered runtime copies under .claude/, .codex/ and .opencode/ – instructs the four ordered questions, first answer wins; names aof work promote-finding as the close’s only creating verb; states that the loop creates exactly one type in exactly one place; forbids allocating a finding id; and requires a report of each finding’s routing, each chore created by ref, and each finding handed to the operator.

Two controls hold the creation bound

FF-7103 (test/arch/acd-promotion-creates-one-type.test.mjs) reads the insert engine’s admissible types out of its module-private DOCS_BY_TYPE by source-parsing, refuses any insert type on the promotion path other than PROMOTED_TYPE, refuses runInsertStory, requires the prompt’s review close to name only promote-finding as a creator and all four routings, and binds the finding face to no at/type/parent/under input, an unknown-input refusal, and a position resolved through appendPosition. FF-7104 (test/arch/acd-one-promotion-engine.test.mjs) requires one definition site per mechanic, both faces reaching the family by import, no module anywhere in src/ matching the promotion signature outside the family, and no ../commands/ import inside it. Both are registered in scripts/test.mjs.

71/02 · One review pass – lanes spawned together, mode read off the wave, and re-work confined to the delta

The review lanes are spawned together, staggered, and bounded

The <review_lanes> region of src/bundle/commands/continue.md instructs the review step to spawn its lanes together and wait for all of them – the same terms the build fan-out already used – names the concurrent set as aof-architect + aof-qa + the automated craft pass always, with aof-designer joining as a fourth only when the story has UI, forbids any lane reading another lane’s verdict or findings before all have returned, forbids a returning lane cancelling one still running, staggers the spawns by a handful of seconds to warm the shared prompt prefix, and caps concurrency at the bound aof work dispatch --list --json reports with the remainder spawned as earlier lanes return.

Execution mode is derived from the wave, as code

decideExecutionMode() in src/work-loop.mjs resolves (configured mode × --solo × wave size) to exactly one of EXECUTION_MODES with a reason from the closed EXECUTION_MODE_REASONS set, and returns null for a wave it cannot count rather than guessing. A wave of one is inline whatever the static config says; --solo and a configured solo both win over the wave in both directions; an empty wave is inline and carries emptyWave: true, so a caller can tell “nothing ready yet” from “milestone finished”. dispatches and spawns are both false for every inline answer – inline is defined by what it does not do: no worktree, no agent, no dispatch record.

The derivation is taken before anything is dispatched, and widens nothing

The <execution_mode> region of continue.md places the derivation before the first dispatch, states that it reads the wave the ready set already computed and never recomputes it, that a solo setting is never widened to orchestrated by a large wave, and that the derivation changes how the lanes run and never how many lenses exist – no review lens is dropped by running inline.

Round two re-reviews the delta, not the story

The <delta_review> region confines a granted second round to the fix it was granted for: only the lens or lenses that raised a surviving Blocker are re-spawned, a lens that reported clean in round one keeps that verdict as the answer of record, and each re-spawned lens is handed exactly three things – the fix diff, the Blockers that lens itself raised, and the contract clauses those Blockers cite – and not another lens’s Blockers, not another lens’s round-one verdict, not the round-one findings below Blocker, and not the story’s whole reads: set. The design lane re-renders only the surfaces a surviving design-gap Blocker named. One deduplicated Blocker re-spawns exactly one lens, chosen by claim class (production-defect → architect, locked-contract-violation → QA, design gap → designer, ambiguous → the lens whose report survived reproduction). The delta is named by the reproduce-and-deduplicate step, never guessed at.

The gate ladder re-runs after every fix round, and a red ladder consumes no round

The <review_rounds> region states that step 4’s gate ladder runs again before every re-review and that a red ladder after a fix round does not advance the round counter – the counter advances on review rounds, not on gate walks.

An amendment ratifies in the beat that raised it

The <amendment_ratification> region of src/bundle/commands/refine.md closes the Decide stage before the contract fan-out begins, states that each contract has exactly one authoring beat, and routes every delta by WHEN it was raised across five cases – during the architecture pass, while a contract is being authored, while the fan-out is in flight, after the contracts are authored, and after the item is delivered (a superseding ADR in the accepting item’s own contract; the delivered .feature is never edited, annotated or tagged). The one delta that still earns its round is the one that would ship a wrong criterion – already a locked-contract-violation and therefore already a Blocker inside the existing round bound. A re-authoring wave is stated to be no legal response to a delta: no stage spawns an authoring agent whose only work is re-applying a decision.

The four rendered runtime copies carry all of it

.claude/commands/aof/{continue,refine,code-review}.md, .codex/skills/aof-{continue,refine,code-review}/SKILL.md and .opencode/commands/aof/{continue,refine,code-review}.md are byte-identical to the live bundle render, so the prompt an agent actually reads carries <review_lanes>, <execution_mode>, <delta_review> and <amendment_ratification>.

71/03 · The render lane is gated on renderability – and this milestone supersedes 07’s npx playwright clause

The design lane resolves renderability before it spawns anything

src/bundle/commands/continue.md and src/bundle/commands/verify.md both state a renderability precondition ahead of the render, evaluated before any render is attempted and before anything is spawned. It resolves two halves in a declared order: a base URL (--url when given, else work.ui.baseUrl) and a renderer (work.ui.renderer when declared, else the highest-revision Chromium found by globbing the platform’s ms-playwright cache). Resolvable means exists and is executable, not that the key is set – a declared path that is not on disk is the precondition’s own finding, named with the path that failed, rather than a render-time crash.

A failed precondition is a recorded skip, per surface

When either half is unresolved, or the surface declares no Route, the lane attempts no render at any breakpoint, records the reason naming the missing key / missing binary / missing Route, returns INCONCLUSIVE, spawns no designer session and no QA session, and continues. The precondition is evaluated per surface, so a surface that resolves is still rendered and judged when a sibling surface does not. A missing baseline is a different recorded reason from a missing precondition half.

The render drives an already-cached Chromium, and carries its breakpoint’s width

The invocation both commands name is <renderer> --headless=new --disable-gpu --hide-scrollbars --window-size=<W>,<H> --screenshot="<absolute forward-slash path>" "<baseUrl><Route>", one invocation per breakpoint at that breakpoint’s own width, each writing its own screenshot to a path made absolute and forward-slashed on every platform. The 390 / 768 / 1280 default survives the mechanism swap and stays DESIGN-overridable per milestone. A render that exits non-zero, exits zero but writes no file at the named path, writes a zero-byte file, or does not return within the step’s own wait is INCONCLUSIVE with that failure recorded as the reason – an “exited zero, wrote nothing” render is never judged as a screenshot.

work.ui.renderer is a declared config key

schemas/aof.schema.json declares work.ui.renderer as an optional string inside the closed (additionalProperties: false) work.ui block: absent ≡ discover the highest revision in the platform’s ms-playwright cache, declared ≡ use this path.

07’s npx playwright render clause is superseded, in 71’s own contract

No file under src/bundle/ names npx playwright. The three milestone-07 arch tests that pinned it – acd-conformance-verdict-contract, acd-design-conformance-bundled and acd-design-role-split – are amended in place: no surviving assertion requires the superseded render, while the legs 07 cared about are asserted still enforced (Playwright absent from package.json dependencies/devDependencies; the designer never told to run the browser; the designer body free of the render command; the render → hand-off marker carrying its hand-off half). No file under wiki/work/07_* is written by any story of this milestone – 07’s delivered .feature files are immutable and are neither edited, annotated nor tagged.

FF-7102 holds all three claims

test/arch/acd-render-lane-is-gated.test.mjs walks every file under src/bundle/ rather than a listed set, asserts the render invocation itself (not the file) carries --screenshot= and --window-size=, asserts the precondition precedes the render by order rather than by presence, asserts QA’s own Playwright harness and toHaveScreenshot regression survive so the control cannot be satisfied by deleting design conformance, and asserts the three superseded 07 controls were amended rather than gutted. It is registered in scripts/test.mjs.

The six rendered runtime copies carry all of it

.claude/commands/aof/{continue,verify}.md, .codex/skills/aof-{continue,verify}/SKILL.md and .opencode/commands/aof/{continue,verify}.md are byte-identical to the live bundle render.

70 · Warm start

A phase spawn is handed its context by value – end to end, for the first time

The chain each story delivered in isolation now exists as one path: a compiled brief (m70/00) that carries the phase’s actual contract and architecture slice (m70/05, m70/03), launched behind a deliberately shareable prefix (m70/01), crossing the PTY as one atomic input (m70/06), and landing in a run record whose spend is read back as a per-phase cache ratio (m70/02). Before this milestone the spawn brief was {itemRef, worktreeCwd, task, command}, every run record read "brief": {}, and the prompt was /aof:continue <ref> – an instruction to go and rediscover the tree.

The loop’s economics are observable rather than inferred

aof work observe <ref> reports, per declared phase, the four token buckets, a priced cost, a cacheRead ÷ cacheCreate ratio and a met/missed verdict against a configured target – and reports unmeasured distinguishably from a warm phase that divided by nothing. Phase is read from brief.loop.phase, never minted, so a run the loop shell did not mint reports as having no declared phase instead of being silently pooled.

The milestone’s own thesis is falsifiable, and has been tested once

This is the state no single story delivers: the SPEC’s baseline figures have a measured successor taken through the production door (m70/06), so “warm start worked” is a claim that can be checked rather than asserted. The first check does not confirm it – see Gaps – but the instrument exists, its target is derived from a measurement, and the next check costs a run rather than a milestone.

Four structural rules bind that did not exist before

ARCHITECTURE.md stays one artifact whose ADRs are addressable in place (m70/03, ADR-006); the artifact budget refuses an accept rather than warning about it (m70/03, ADR-007); the reviewer is structurally barred from resuming a build session (m70/04, ADR-008); and aof can never replace the system prompt, which is the condition the cache flag’s admissibility rests on (m70/01, ADR-004).

The budget that refuses an accept was proved by refusing this milestone’s own

ADR-007 is not a warning that was believed – it bound at this gate. Recording F-21’s ADR-004 amendment pushed ARCHITECTURE.md to 740 lines against its own 700-line budget, and the → done transition could not proceed until the document was compacted back to exactly 700 by giving up prose that had stopped paying its way.

Ten declared controls, each with a red probe that was observed failing

Every control in the register was planted against the live bytes, watched to fail with the message quoted, and restored byte-identically. Seven of the ten extend a guard already in service, where the red probe is the only evidence the extension is armed at all.

70/00 · The phase brief

A pure phase-brief compiler

src/phase-brief.mjs turns supplied document text into one rendered brief: it imports nothing, reads no filesystem and no wall-clock, and returns a byte-identical result for identical inputs.

A caller-side reader that keeps the compiler pure

src/phase-brief-read.mjs is where the item’s documents are read; compileBriefForItem is the one function both spawn seams call, so there is one reader and one compiler rather than two implementations of the same assembly.

A declared, priority-ordered section set, selected per phase

BRIEF_SECTION_PRIORITY is item, story, objective, tasks, fitness, dependencies in that order, and refine, continue and verify each resolve to their own subset of it rather than to every section that exists.

A size ceiling enforced inside the writer

compilePhaseBrief cannot return an over-ceiling brief: PHASE_BRIEF_CEILING_CHARS (PHASE_BRIEF_CEILING_TOKENS 2000 × PHASE_BRIEF_CHARS_PER_TOKEN 4 = 8,000 characters) is the only ceiling literal in src/**, and no caller applies a bound of its own.

Loud, non-empty truncation

An over-ceiling brief drops from the bottom of the priority list, retains the item section, and carries a notice naming which sections were dropped or shortened; the compiler never returns an empty brief and never silently ships the overflow.

Honest degradation, and refusal of a subject-less brief

An absent or whitespace-only section is omitted rather than faked, an empty task-contract set is stated as empty, a read fault contributes an absent section rather than failing a spawn, and a brief with no item ref is refused by the compiler rather than returned.

The brief on the bag that already existed, at both spawn seams

src/commands/drive.mjs and src/mesh-worker-execution.mjs both set an additive context key beside the four existing brief keys (itemRef, worktreeCwd, task, command), which keep their meaning; the driver’s signature stays driveInteractiveClaudeSession(brief, options) and it reads brief.context. No rival context/payload/digest object exists.

The context arrives as input, not as an instruction to go and read

composePhaseBriefInput appends the rendered brief (and its truncation notice, when present) to the command the driver types into the session, so the phase receives the content by value.

Absence is benign at both seams

A caller that supplies no compiled context produces a byte-identical launch to the pre-story behaviour, so the two seams were landable independently and every existing run record stays readable.

Three declared controls in service

FF-7001 (one brief bag), FF-7002 (the compiler is a pure leaf and the driver’s export set stays the frozen seventeen) and FF-7003 (the bound is in the writer) are armed, registered in the suite, and each was observed failing against a planted violation.

70/01 · A launch whose prefix is shareable

The stable-prefix flag on every production launch

resolveInteractiveDriverLaunch builds its argv with --exclude-dynamic-system-prompt-sections beside --append-system-prompt, so cwd, environment info, memory paths and git status ride the first user message instead of the cached system prompt – for both spawn callers, unconditionally.

The append form is the only system-prompt form in src/**

No --system-prompt replacement argv is constructed anywhere in the source tree, which is the condition under which the stable-prefix flag is not silently inert.

A session model and effort resolved per phase and passed explicitly

src/session-model.mjs resolves --model and --effort from work.agents.session ({ models: { <phase>: <model> }, effort: { <phase>: <level> } }) and src/commands/drive.mjs hands them to the seam, so a phase-scoped launch’s cache key is a configured decision rather than whatever the session defaulted to.

Two model surfaces with no join between them

The render-time role map (work.agents.models, milestone 30) and the session path (work.agents.session) are resolved by different modules and neither reads the other’s path; SESSION_MODEL_CONFIG_PATH is the single literal naming the session path in src/**.

Absence is silence at the launch

No work.agents.session, no routing entry for the phase, an unknown phase name, an empty model string, or a work.agents.session that is not an object all resolve to {} – the launch passes neither flag and is byte-identical to the pre-story spawn.

The one-hour prompt-cache window held by the spawn rather than by the billing mode

The launch environment carries ENABLE_PROMPT_CACHING_1H=1, set after the IDE-attachment scrub, so the scrub cannot delete it and the window is the same on a subscription as on usage credits.

Per-phase routing bound to the phase-scoped caller

The local drive command carries --model/--effort; the mesh worker dispatch – assignment-scoped and carrying no phase – carries neither, while still receiving the stable-prefix flag and the one-hour window.

Three declared controls in service

FF-7004 (aof never replaces the system prompt, and the append form and the flag travel together), FF-7005 (one launch seam – no module outside it assembles a claude argv) and FF-7006 (the two model surfaces resolve from distinct paths) are armed, registered in the suite, and each was observed failing against a planted violation.

70/02 · Cache economics per phase

The cache ratio, reported per phase from the run record

aof work observe reports cacheRead ÷ cacheCreate for every phase of an item’s runs, derived from the spend.tokens buckets milestone 68 writes and never re-counted from a transcript.

Phase read from the loop’s own declaration, with no rival column

Runs are grouped by brief.loop.phase and by nothing else; a run the loop did not mint reports under an explicit “no declared phase” row carrying its own figures, rather than being folded into a phase or dropped.

An unmeasured run is unmeasured, never a zero

A run carrying no spend envelope is counted in unmeasuredSpend, excluded from the ratio, and reported as unmeasured – it is never a 0.000 that would read as a cache failure.

Three non-arithmetic cases each carry their own reported state

cacheState is measured, unbounded or unmeasured: cache reads with no creations is a warm phase reported ∞ (warm, unbounded) rather than a division by zero; creations with no reads is a measured 0.000 distinguishable from an unmeasured phase; measured runs that populate neither cache bucket are unmeasured, because there is no cache signal to divide.

A stated target turns the ratio into a per-phase verdict

work.observability.cacheRatioTarget yields met or missed per measured phase – at the target is met – and the target it was judged against is stated in the report beside the verdicts.

An absent or unhonourable target is a first-class silent case

No target, a non-numeric target and a negative target all resolve to cacheTarget: null: no verdict is stated, none is invented, and the ratio is reported unchanged in every case.

An unmeasured phase is never judged missed

A phase with no cache signal carries no verdict under a configured target – cacheVerdict is null and the row reads –, so a missing measurement cannot be mistaken for a failed one.

The verdict changes nothing about any run

applyCacheTarget returns a new rollup and mutates neither its input nor any run record; no run is failed, retried, capped or killed on a verdict. The milestone records and does not enforce (ADR-008).

The reporting leaf still writes no record and adds no key

src/work-observe.mjs reads what milestone 68 made true; src/run-store.mjs and its seventeen src/ dependents are untouched by this story, and the run record’s shape is unchanged.

70/03 · A story reads its slice

adrs: is a story frontmatter key, optional and additive

A STORY.md may carry adrs: [ADR-006, ADR-007]. Absence is the pre-existing behaviour exactly – every one of this stream’s 201 declaration-free stories validates untouched – and the key is de-duplicated in declaration order rather than sorted.

A declaration that does not resolve is a validate finding, by ref and by file

aof work validate reports ADR declaration "ADR-999" does not resolve in 70/ARCHITECTURE.md for an id the story’s own milestone does not carry, and ADR declaration adrs must be an inline list for any present-but-non-list shape. A story with no parent milestone is told it has no architecture to resolve against rather than borrowing an identically-numbered ADR from another item.

One ADR is addressable inside the one architecture document

extractAdrBlocks(text, ids) returns the requested ## ADR-NNN blocks from a single ARCHITECTURE.md text, in document order, byte-preserved, ending each block at the next h2 – so a following register or partition section never leaks into the last ADR. It matches exact ids only: ADR-00 never resolves to ADR-001, and every requested miss is reported.

Fenced and commented headings are examples, not document structure

The one structural-h2 scan behind both ADR addressing and the register fallback ignores headings inside code fences and HTML comments, so a documentation example of a ## ADR-NNN heading or of a ## Fitness functions register is neither an address nor an early block boundary.

A story’s brief carries the slice it declared; a declaration-free story carries the register

compileBriefForItem puts the declared ADR blocks in the brief’s architecture section, names any unresolved id inside it, and falls back to the milestone’s ## Fitness functions register – never the whole architecture document – when a story declares nothing. A milestone’s own brief still carries the full architecture record.

ARCHITECTURE.md remains one artifact, and a control says so

WORK_ITEM_ARTIFACTS enumerates exactly one architecture entry, REGISTER_BLOCKS’s declaring file for the fitness register is still ARCHITECTURE.md, and the extractor names no sibling per-ADR file or directory. FF-7008 fails if any of those three change.

The artifact budget binds at accept, on the item being accepted

aof work status <ref> done runs the same budgetGroup, over the same measurement and the same resolved budgets as the advisory sweep, with the accepting ref injected, and refuses the transition with artifact-budget-exceeded (409) naming artifact, measured lines and budget. No other status edge applies the gate.

Nothing already accepted is re-litigated, and the sweep is unchanged

An open item’s over-budget artifact is still a warn, the stream-wide sweep still reports every overage as a warning, and an artifact exactly at its budget is healthy. The 15 milestones already past 700 lines stay green.

The budget numbers keep one home

budgetsFromConfig is the single resolution path for both the warning and the refusal, so the two gates cannot be given different numbers.

The frozen work core is left as it was found

The declaration’s resolution lives in src/commands/validate.mjs, not in src/work.mjs; the core validator’s bytes at HEAD are the bytes it had at 70/00’s merge.

70/04 · The fix loop resumes the build

A fix resumes the build session that wrote the code

When the loop’s review gate produces a fix, the drive command resolves the session id recorded on that particular build run and launches with --resume <id>. The two halves that existed separately – the driver’s resumeSessionId option and the run record’s persisted sessionId – are joined.

A resumed fix is handed the findings and the diff, not the tree

The compiled brief is attached only when no resume target resolved. A warm fix receives the review findings and the change under review; the session it re-enters already holds the tree.

The change under review is the build’s delta, not the working tree

The diff handed to a fix is taken between the build’s recorded baseline and the tree it produced, so staged and unstaged work that predated the build is excluded while the build’s own delta is retained.

An unresolvable target degrades cold rather than refusing to run

A pruned transcript, an absent session id, a session id that is not a bare basename, and a run recorded on another node all resolve to no target; the fix then runs as a cold spawn with a compiled brief. A fix that cannot be warmed still runs.

The cold fallback is a visible retry record, never a hidden second process

A positively unavailable target produces its own retry record rather than a second process inside one attempt, and a resumed process that fails for a generic reason is not re-dressed as a cold one.

A resume whose spend baseline cannot be established says so

When the resumed session’s transcript cannot be snapshotted, the run reports resume-spend-baseline-unavailable and the spend settle is skipped by name, rather than being stamped from an unreliable baseline.

The resume target is derived from the phase, never from a caller

A caller’s resumeSessionId is stripped from the options before they reach the launch, and a fix payload is admitted only on the continue phase. A refine or verify launch supplied with both a raw resume id and a rich fix payload resumes neither.

Node placement is decided by the loop, not by the executor

admitResumeBuildRun returns the build run only when it was recorded on the node now running; the drive command receives either an admitted local run or null and makes no placement decision of its own.

A review is never resumed, and a control enforces it

FF-7007 drives the real production commands rather than the resolver helper, so a helper that returns null cannot stay green while a caller still forwards a build session into a review launch.

Interruption preserves the pending fix

A fix interrupted after its review keeps its identity and its findings through the existing run lineage, and the review-round cap survives repeated interruption and resume.

Bookkeeping is unchanged by warmth

A resumed fix mints its run, attributes its session, settles its spend, and carries its attempt and retry lineage exactly as a cold one does.

70/05 · A brief that carries what the phase must satisfy

Every brief section declares a reduction, and the declaration is exported and frozen

src/phase-brief.mjs exports BRIEF_SECTION_CONDENSERS, BRIEF_NON_CONDENSABLE_SECTIONS and BRIEF_BOUNDED_CONDENSERS, all Object.freezed. The union of the first two is exactly BRIEF_SECTION_PRIORITY and their intersection is empty, so a section cannot exist without a declared reduction – an eighth one added without either declaration fails on its first commit.

A section too large for the budget is condensed, never dropped for size alone

Every section is offered its condenser before it is considered for sacrifice. Across all 219 story folders in this repo’s wiki/work/, compiled in all three phases through the production reader, the only disposition the compiler emits is condensed – 0 sacrificed and 0 unshippable in 657 compilations.

A condensed section states its own count, not merely its size

A bounded condenser reports kept / total / omitted, and the notice renders it in words – “All 27 scenarios listed, none omitted”, “2 of 3 decisions listed, 1 omitted”. A condensed section that named none of its own entries is therefore visible as such rather than indistinguishable from a full one; no such section exists in this stream today.

Sacrifice runs strictly bottom-up, so one miss no longer cascades

A section that does not fit no longer evicts the sections beneath it. A section whose condensed form exceeds the whole ceiling is reported as unshippable and costs nothing below it.

The truncation notice distinguishes three dispositions and points at the source

CONDENSED (shortened to fit – still carried) is distinguishable from dropped and from unshippable, and each condensed section names the form that survived and the file the complete text is read in.

The compiler is handed addressed extracts, never documents

src/phase-brief-read.mjs binds each section to its own named …Section const, produced by an imported addressing helper, before the compilePhaseBrief call. A milestone’s specification reaches the brief as its ## Objective block and a story record as its ## User story block – never as the whole file, and never carrying frontmatter or scaffold comments.

The ceiling is unchanged and still has one home

PHASE_BRIEF_MAX_CHARS is 8,000, enforced inside compilePhaseBrief, and no second ceiling literal exists outside the compiler. No brief in the stream exceeds it in any phase.

The contract reaches every story in this repository that has one

211 of the 211 story folders carrying a tasks/ directory receive their acceptance criteria in every phase, and 194 of 219 receive their milestone’s architecture or fitness register. Mean brief size is 7,440 / 7,234 / 6,753 chars of 8,000 at refine / continue / verify.

A story’s declared ADR slice reaches its brief on real data

A STORY.md carrying adrs: receives those ADRs’ headings and decision passages, counted, in place of the milestone register. 70/05 is the only such story of the 219 and its brief carries all three of its declared ADRs’ headings.

Two record shapes the reader previously answered nothing for now resolve

All 34 stories declaring a non-empty depends: receive a dependencies section at refine, and the 3 records writing ## User Story rather than ## User story receive a story section.

The brief is pinned against this repository’s own work stream

test/brief-pinned-to-the-stream.test.mjs compiles briefs for real items under wiki/work/ and asserts invariants that hold for any real item rather than byte counts for particular ones. Its declared-slice assertion fails if no story in the stream declares any ADRs, so the path cannot pass by emptiness.

Two structural controls guard the declarations and the call site

FF-7009 fails if the frozen declarations stop covering BRIEF_SECTION_PRIORITY exactly; FF-7010 fails if any section value at the compilePhaseBrief call site is an identifier bound directly from a disk read. Both were observed failing at this gate against the live bytes.

70/06 · The saving is a number, not a claim

The stream carries a measured phase, where every run record said unmeasured

Run 20260824T101918473Z-0005 is settled done with brief.loop.phase: "continue" and a spend envelope carrying all four token buckets, the model, the effort key and a priced cost source ($4.0940, price-table-2026-08-v1). aof work observe reports a cache ratio of 12.921 for that phase instead of unmeasured.

The milestone’s before-and-after is a committed artifact, not a claim in a commit message

observability/snapshots/2026-08-24T10-28-17-153Z/report.md holds every figure this milestone is judged by, each re-read rather than quoted: a 143-row baseline across six pre-70 milestones (2026-08-08 → 2026-08-14), the after side taken by the same per-agent instrument, the delta for each headline figure, four figures reported not taken with a reason each, the sample size behind every number, and the confounders that bound them.

A delta that went the wrong way is recorded with its measured direction

The per-agent cache read÷create ratio is stated as 23.936 → 10.383, a regression, with the structural reason it is probably a lifetime artefact and the explicit statement that the comparison cannot settle whether prefix sharing improved. The comparison is not restricted to the figures that improved.

work.observability.cacheRatioTarget is a number derived from a measurement

Set to 13 – ceil(12.921), the smallest integer strictly above the worst measured phase – and recorded with the run count, phase count and date it was taken over. The report states it, and states missed for the measured phase.

The cache-ratio verdict actuates nothing

cacheRatioTarget and cacheTargetStatus are read in exactly two modules, both reporting surfaces (src/commands/observe.mjs, src/work-observe.mjs). No loop, drive, run-store or transition module reads either: a missed target fails, retries, caps and kills no run.

A directive crosses the PTY as ONE atomic input at any length

The body is written as a bracketed paste and the Enter follows as its own write, after a settle derived from the caller’s readiness delay. A 43-line directive that previously arrived as eight separate user turns now arrives as one; the live transcript records it as a single 1,249-character message with no ESC byte in the content.

A directive body cannot close its own paste

An end-of-paste sequence occurring inside assembled brief content is stripped before the write, with a coded directive-paste-marker-stripped degrade, so content can never be reinterpreted as protocol.

A settled run is never sent a late Enter

The queued submit reads the settle flag at fire time as well as being cleared at cleanup, so a PTY that exits on the body write cannot receive a keystroke afterwards.

A worker session no longer presents as a child of the operator’s Claude Code session

The launch env is scrubbed of CLAUDECODE, every CLAUDE_CODE_* key, CLAUDE_PID, CLAUDE_EFFORT and CLAUDE_AGENT_SDK_VERSION. With those inherited, a real turn ran to completion and no transcript was written at all, leaving the session id, the spend envelope and the phase ratio permanently absent; scrubbed, the transcript appears ~2s after the directive write. Everything aof sets for itself is set after the scrub, so the widened scrub cannot eat the 1-hour cache window.

The folder-trust pre-write lands under the key claude actually reads

Trust keys are normalised to the separator spelling claude uses. ~/.claude.json held 661 backslash-spelled project keys carrying exactly one field each – aof’s own write, never read – beside claude’s own forward-slash keys for the same directories.

The shared PTY double separates the wire from the input, and consumers no longer re-derive it

createScriptedPty exposes pty.writes/rawChunk as the bytes written and chunk as the input a paste-aware terminal would present. Suites that previously carried their own paste-strip now read the view they mean, and the local re-derivations were deleted rather than duplicated. A transport change invalidates doubles in one place instead of in every suite that models a spawn.

69 · Loop bounds

The runtime enforces bounds at all, and their values are declared rather than assumed

Before this milestone there was no token limit, no turn limit, no wall-clock limit and no cost ceiling anywhere in aof, and the interactive claude path had no timeout of any kind. There are now seven declared bounds resolving from work.loop.* through one home, four of them deadlines the runtime enforces against a process it holds – see [[m69/00]] for the declaration and its refusals, [[m69/02]] for the enforcement.

Liveness and progress are two signals with two different consequences

This is the milestone’s central claim and no single story makes it. A run that has gone silent is killed and retried against the liveness deadline ([[m69/01]] supplies the signal, [[m69/02]] the consequence). A run that is alive and getting nowhere is reset with a summary and, on repeated resets, escalated with its work preserved ([[m69/03]] supplies the ledger, [[m69/06]] the caller that consults it). The two are deliberately not merged: the eight-day zombie and the two 11h07m burns are different failures, and a heartbeat alone catches only the first. One signal carries one terminal behaviour throughout – a block the pending-question detector misses costs an attempt rather than parking, because the runtime cannot read human intent out of silence.

Concurrency is a property of the system on both surfaces, and waiting no longer costs capacity

A slot is acquired before work is accepted – the local slot is the git lane, counted before one is materialised; the mesh tick consults its counted set before it sends ([[m69/04]]) – and a run blocked on a human ends its process, releases that slot and resumes the same conversation when the answer arrives ([[m69/05]]). The two compose into the milestone-level fact: the largest recorded lost-time category (107h28m in m47, 58h05m in m48, 46h38m in m50) no longer holds a slot while it waits.

The bound four other milestones were waiting on has a value

Milestone 54 was not-started in its own words because “it enforces a bound; it does not invent its value”, and 53, 62 and 65 deferred to the same arc. Review is N = 1 by default with a named blocker admitting a second round; build is bounded on failure-to-progress rather than an iteration count; maxAttempts stays 3, paired with a total-duration ceiling. Every framework loop record’s ceiling: now resolves, and a ceiling whose pointer resolves while nothing reads the bound is a named finding rather than a declaration ([[m69/00]], [[m69/06]]).

The run record gained nothing, and that is enforced rather than intended

src/run-store.mjs has seventeen src/ dependents. This milestone adds no key, no state and no transition to it: its key set and LEGAL_TRANSITIONS set are byte-frozen by SHA-256 under FF-6908, and there is no lease table, claim file or per-slot persisted object anywhere. A slot is a count over rows and lanes that already exist.

Enforcement is out-of-process, and no bound is spoken to the model

No --max-turns, --max-budget-usd, -p, --print or --output-format argv is constructed for the interactive driver anywhere in src/. The deadline is armed against a held process handle and reaches term.kill(). Telling the agent about its budget is milestone 71’s subject and is refused here by ADR-004.

Eleven declared controls, each with a red probe that was observed failing

The register in VERIFICATION.md carries all eleven with the plant and the message observed, not a placeholder. Four of them extend a guard already in service, where the probe is the only evidence the extension is armed at all.

69/00 · The declared cap

Every loop deadline and cap resolves from one home

src/loop-bounds.mjs resolves all seven declared bounds from work.loop.* – heartbeat, schedule-to-start, start-to-close, schedule-to-close, review rounds, build no-progress rounds and progress max resets – each with a documented default, and a malformed declared value falls back to that default rather than crashing the caller.

The two bounds that already had homes still have them

work.dispatch.concurrency keeps its single pre-existing reader and work.autonomous.maxAttempts keeps its closed reader set; neither was moved into the new leaf, and the existing cap single-home guard was extended to cover the leaf rather than joined by a sibling.

A second review round is refused unless a blocker is named

decideReviewRound admits round one and refuses round two without an explicit blocker claim, reporting which blocker class would admit it; an exhausted cap halts rather than looping.

The declared review cap binds the production re-review path

decideReviewGate (src/work-loop.mjs) consumes the gate’s real { path, problem } finding shape and is called from src/commands/loop.mjs, so the cap is enforced where re-review actually happens; the review-round count is counted apart from the engine cycle, and a blocker is an explicit claim rather than an inference from finding prose.

No framework loop record declares an uncapped ceiling

Every ceiling: under src/bundle/loops/*.md is none or a pointer list, and a config: pointer naming a key no resolver resolves is reported as a finding rather than accepted as a declaration.

69/01 · Heartbeat by consumption

A driven run stamps its own liveness from the tool results it produces

A bundled PostToolUse hook (src/bundle/hooks/run-heartbeat-enqueue.mjs + claude-run-heartbeat.json) enqueues a liveness record for the run named in its environment, and src/run-heartbeat-consumption.mjs drains that queue into run-store.heartbeat() – which had zero callers in src/ before this story. heartbeatAt now moves while a run works, rather than staying null from mint to terminal.

The hook derives nothing and cannot break the session it rides in

The hook body imports nothing from src/, opens no store, derives no workspace identity from cwd, and exits 0 on every path – the clause set already proven for the artifact-sync hook, now enforced over every bundled hook body as a class.

The run’s identity is handed in, never derived

The run id reaches the hook through the spawn environment (OTEL_RESOURCE_ATTRIBUTES), which is per-process and untracked, so the absolute-path-in-a-tracked-file failure that a git worktree inherits cannot recur through this route.

The periodic scan reaps on the absence of liveness

The reclaim tick consumes the queue and reclaims a run whose liveness has gone stale against the one declared threshold, through the reclaim edge that already existed rather than a third copy of it.

One staleness constant answers for every consumer

The reclaim threshold, the loop shell’s default and the session driver’s undeclared-completion idle window all resolve through DEFAULT_HEARTBEAT_MS in src/loop-bounds.mjs; src/ holds exactly one 15-minute literal, and declaring work.loop.heartbeatMs moves all three together.

The dual-staleness gate has a real second signal

dualStalenessDecision reads the linked run’s heartbeatAt before falling back to updatedAt, so the AND of node presence and run liveness runs on two signals for the first time; a fresh node with a silent run stays hands-off, and an absent presence record is unknown liveness rather than staleness.

69/02 · The four deadlines

An attempt has a deadline aof enforces against a process it holds

driveInteractiveClaudeSession – the only function in this repo holding a live process handle – arms the per-attempt (start-to-close) and liveness deadlines against that handle and reaches term.kill() on expiry, resolving a retryable timeout. All three production callers supply the policy from the workspace: src/commands/drive.mjs:212 and src/mesh-worker-execution.mjs:1689,2230, each through loopBoundsFromConfig(ws).

The total-across-attempts ceiling gives up rather than retrying

Schedule-to-close escalates instead of spawning another attempt, and preserves the worktree for triage rather than cleaning it up.

A startup grace suspends the liveness deadline and nothing else

A clone plus a dependency install produces no tool-result events at all; the grace covers that window without pausing the wall clock, so a five-minute allowance does not become an unbounded one.

No bound is spoken to the model

No --max-turns, --max-budget-usd, -p, --print or --output-format argv is constructed for the interactive claude driver anywhere in src/; enforcement is entirely out-of-process.

The exit vocabulary 68 fixed is reachable for the first time

timeout was already classified retryable and already a member of 68’s exit vocabulary; this story is what makes it – and its siblings – a state a run can actually reach.

69/03 · Progress, not liveness

An attempt’s progress is sampled from deterministic signals only

src/loop-progress.mjs samples files touched, lines changed, commits made and the failing-scenario count from git status --porcelain inside the lane’s own tree – the reader the lane sweep already used – and nothing else. No model, prompt or agent surface is imported, so the sample is a measurement rather than a judgement.

The ledger is append-only and invisible to the run reader

Samples land in a runs/[<node>/]<runId>.progress.ndjson sibling through appendFile; nothing opens an existing sample file for truncation. readRuns skips every entry that is not *.json in both the flat and the node-partitioned branch, so the ledger is invisible to the god-node’s reader by construction, and src/run-store.mjs – seventeen src/ dependents – gains no key.

Consecutive no-progress samples reset the attempt, and repeated resets escalate

At maxStalls the policy resets the attempt and carries a summary forward; at maxResets it escalates. Both bounds are declared in work.loop.* and resolved through [[69/00]]’s leaf.

The build loop’s ceiling is a failure-to-progress rule, not an iteration count

decideBuildProgress stops the build when consecutive measured rounds show no reduction in the failing-scenario count – the derivative of the number the build loop already drives to zero – rather than after an arbitrary N.

69/04 · Slots before work

The bounded worker pool has a production caller

dispatchReadySet – the only bounded worker pool in this repo, and one with no production caller before this story – is reached from src/commands/dispatch.mjs:104, so at most bound lanes are materialised at once and the remainder dispatch as lanes free.

The local slot is the git lane, counted before one is materialised

The dispatch door derives its counted set from the lanes git itself reports under the dispatch root (inspectDispatchLanes, prunable excluded) and reads it before any lane is opened – before both the pool call and resolveDispatchLane. Reusing an existing lane is free; each new open counts against the next member; a member over the bound is refused with a code in the reported result rather than opened, thrown or dropped.

No occupancy fact is persisted outside git’s own worktree list

There is no lease table, no claim file, no per-slot persisted object and no new run-record column – a slot is a count over rows and lanes that already exist, not an object with its own lifecycle. src/run-store.mjs is byte-unchanged from milestone 68.

The mesh control tick is bounded

The tick derives its counted set from assignment rows and tests it before the send; a row over the bound stays assigned for a later tick – the same leave-it-assigned branch the not-connected case already used, so a send that did not go out is retried rather than silently dropped.

Occupancy has one home and every door that opens work reads it

assignmentOccupiesDispatchSlot is the single definition of the accepted/running/needs-input predicate; no other module re-spells it, and no path treats the unpersisted once-guard set as occupancy. Both doors that can put a target back to work – the tick’s directive send and the parked-answer resume – read countDispatchSlotsByTarget against the bound before sending, and the whole-src closure enumerates those doors, so a new one fails the gate rather than bypassing it.

Admission survives a scheduler restart

Occupancy is proved by the row’s own state rather than by in-process bookkeeping, so accepted and running rows still count after the scheduler restarts, and a parked run’s answer re-acquires a slot through the same count or is refused.

69/05 · A blocked run releases its slot

A run waiting on a human ends its process and releases capacity

A pending human-input tool call is detected directly, so the park does not wait out the idle window: the PTY exits, the park is published, and the row leaves the counted set – so a machine no longer sits idle behind a question nobody has answered. createMeshParkResume (src/mesh-park-resume.mjs) is reached from src/mesh-worker-execution.mjs:132.

The park is published once, after a confirmed exit, and releases nothing until the row carries it

No assignment-status frame carrying the needs-input code is constructed from a path holding a live PTY handle; every park publish sits on an exit-confirmed settle path; and the durable assignment.reported reactor names the park edge (running + needs-input) in its admitted set rather than refusing it as non-terminal. A process whose exit cannot be confirmed is not reported as parked, and the reason is recorded.

Answering resumes the same conversation and the same run record

The park’s code is cleared before a resumed process is spawned, never after; the resume is admitted before a process exists; and the run stays running with no new state, no new failure reason and no retry lineage – a needs-input park is the same run resuming, never a second record.

Silence is not a park signal

The explicit detector is the whole of the park. A block the detector misses is observationally just “no tool results”, which is [[69/02]]’s liveness deadline – killed and retried – and exhaustion preserves the worktree under on-max-attempts: pause. One signal carries one terminal behaviour, and no fifth deadline was introduced.

69/06 · The ledger binds the build loop

The build loop writes a progress ledger

Every measurable build round the loop drives appends a deterministic sample – files touched, lines changed, commits made, failing count – to that run’s own runs/[<node>/]<runId>.progress.ndjson sibling, through src/loop-progress.mjs; src/commands/loop.mjs and src/work-loop.mjs are its production importers, and the run record’s key set is unchanged.

A round that cannot be measured leaves no trace and no consequence

A round whose failing count could not be measured appends no sample at all, and a git call, lane or ledger write that fails degrades that round rather than failing or halting the build.

A stalled attempt is reset, and a repeatedly-reset one escalates

At the declared stall bound the loop resets the attempt and carries its summary into the next attempt’s brief, spawning a fresh session rather than resuming the stuck one and leaving the uncommitted work in place; at the declared reset bound it halts on progress-exhausted for a human, with the work preserved for triage and the resets tally reconstructed from the run’s own persisted brief on --resume.

A build that has stopped reducing its failing count halts

decideLoopProgress consults the sample policy first and the failing-count derivative only when the sample policy says continue; consecutive measured rounds with no reduction reaching the bound stop the build on no-progress with cycles still remaining on the engine cap, and the failing count is work:grade’s own cases.failed, with indeterminate read as an absent measurement rather than as a count of zero.

An unmeasured round is dropped from the derivative

An absent round adds no stall and clears none: the bound counts non-reducing transitions between measured rounds, so 9, 9, –, 9 halts on the same two transitions as 9, 9, 9, and a build nothing could measure is an unbounded continue that reports itself unmeasured.

Every framework config ceiling is checked for a consumer, not just a resolver

test/arch/acd-progress-ledger-consumed.test.mjs walks every config: ceiling on every framework loop record and names a loop-ceiling-unconsumed finding for any whose pointer resolves while no production module outside the bound’s declaring home reads it; all four resolve to readers today.

This repository can measure a round

.aof/aof.config.json declares a work.rubric – a runnable command, a report format and a floor – so a build round driven here yields a failing count rather than an indeterminate grade and the ledger is written on this tree. The declaration’s own command and floor are milestone 54’s surface and have already moved once (F-69-V18); what this story delivers is that a declared rubric is consumed, not which command it names.

68 · Loop telemetry

aof measures its own spend on stored keys, end to end

The chain from spawn to answer runs on values written at the time they were true, with no reconstruction step anywhere in it: the driver stamps a session id and OTel resource attributes at spawn (m68/01), the settle copies that session’s own token counts and prices them once (m68/02), and observe resolves an agent run to an item by joining on the stored session id (m68/03). No figure aof reports about its own cost is derived by matching free text.

The four measured defects in this milestone’s objective are each answered, and each answer is a different mechanism

The SPEC opened on four evidenced failures of the transcript miner. They did not share a fix: double-counting is answered structurally, because a run belongs to one session which belongs to one record (m68/03); the overwriting of cited evidence is answered by writing a new timestamped artefact per run (m68/05); the classifier blind to this repo’s toolchain is answered by classifying on what a result EMITS rather than on the command name (m68/03); and the inability to answer for one story is answered by a resolver that reads one level into stories/ (m68/04).

Absence is reported as absence, at every layer that could have guessed

One posture holds across the whole milestone rather than in any one story: a value that was not measured is never folded in as zero and never inferred. spend: null reads not measured and is distinct from a measured zero (m68/00); a run whose session never reported an id stays null rather than being synthesised (m68/01); an agent run with no resolvable session is counted as unattributed rather than assigned or dropped (m68/03); and observe carries unmeasuredSpend as a count beside every total it prints, so a small total and an unmeasured one are distinguishable on the face of the report.

Every production path to the run fact goes through the transition seam

Both of the driver’s production callers – the local drive command and the assignment sink – mint and settle through src/effects/run-transitions.mjs, so every run this milestone added raises its run.started / run.completed event and inherits the declared cascade. This was not true when the milestone’s stories were individually accepted: 68/01 first wired the drive command straight to the store, which turned milestone 42’s two ledger controls red and was caught only at this gate (F-09, ADR-009).

68 records; it does not act

No cap, budget, timeout, reaper or routing decision landed. run-store.heartbeat() is still unwired, nothing branches on exitReason, and no dispatch, lease or slot behaviour changed – the scope refusal ADR-008 declared is intact at the close, and it is what makes milestones 69–72 a measured before/after rather than a rewrite of what they are about to enforce.

68/00 · The spend-bearing run record – a sixteenth key, and a writer that refuses a lie

The run record’s sixteenth key

Every run record aof mints or reads carries spend as its sixteenth and last key – a validated envelope or null – and a fifteen-key record written before this milestone normalizes forward with spend: null, every prior value verbatim.

Not-measured is distinguishable from measured-zero

A record whose spend is null and a record whose spend.costUsd is 0 are two different answers, readable apart from the record alone without consulting anything outside it.

Token buckets are a closed set the writer enforces

input, output, cacheRead and cacheCreate are the only token keys a spend may carry, and run-store.settleRun() refuses – with a typed error, persisting nothing – any envelope that is partial, carries a fifth token key, or holds a negative, fractional or non-numeric count. The enforcement exists in exactly one place: the write path.

Cost is stamped once and carries its provenance

costUsd is written only at settle, alongside costSource (the closed set reported | priced) and a priceTable version that is present exactly when the cost was priced; no read path in src/ recomputes a cost from buckets, so a later price-table correction changes future stamps and never rewrites a settled run.

A closed exit vocabulary that decides nothing

spend.exitReason records how a run ended over a seven-member closed vocabulary, and a value outside it is refused at write. Two runs identical but for their reason differ in nothing else: no retry, kill, bound or state change is triggered by any member.

One phase authority, structurally

Neither the run record nor the spend envelope carries a phase key, and no module in src/ reads one off either – brief.loop.phase is the single authority.

The additive-discipline guard actually runs

test/arch/acd-run-record-node-additive.test.mjs is now spread into scripts/test.mjs’s suite registry, not merely imported, so the record’s key freeze is enforced on every run rather than silently unexecuted.

68/01 · Attribution at spawn

The run record carries the session it ran as

sessionId is populated on the run record at the moment the driver captures it, at both of the driver’s production callers – src/commands/drive.mjs and src/mesh-worker-execution.mjs. The key the record has modelled since milestone 19 now holds a value rather than null on every row.

A session id is recorded or absent, never invented

A run whose session never reports an id reads sessionId: null; no id is derived, copied from a sibling, or synthesised from a path.

The id is written once, and re-presenting it is a no-op

recordSessionId with a byte-identical id rewrites nothing – updatedAt, state, attempt and the retry lineage are untouched – so the mid-run write and the pre-settle write can both run without churning the record.

The attribution write cannot outlive the settle it races

The drive caller awaits the persist inside onSessionIdCaptured, which the driver awaits inside the watch chain that finish awaits before settling. A captured id is therefore on the record before completeRun runs, and no in-flight whole-record write can land afterwards and restore a pre-settle snapshot. The mesh-worker caller reaches the same guarantee by allSettled-ing the persist against the up-channel update.

Neither the attribution write nor the assignment update is conditional on the other

A failing assignment update still leaves the id on the record and still lets the run settle; a failing persist is reported rather than swallowed and does not stop the settle.

work:drive-<phase> mints and settles a run record, through the transition seam

The bare phase-driver commands, which previously minted none, now mint a run, persist the captured session id, and settle done/failed – needs-input settles failed with reason needs-input, because a one-shot local drive cannot service a parked session. The mint and the settle go through transitionRunStart / transitionRunComplete (src/effects/run-transitions.mjs), never the bare store, so each raises its run.started / run.completed event and inherits the declared cascade – the same doors the sibling caller src/mesh-worker-execution.mjs has used since milestone 42.

The settle seam can price a run without publishing it

transitionRunComplete takes projectsDir as an opt of its own, defaulting to workspace.projectRoot. A caller that needs its run’s spend stamped from the transcript tree no longer has to pass workspace, which would also set the event’s workspaceRoot and turn the completion into a global work-snapshot publish. The drive command is priced and does not publish; every pre-existing caller is unchanged.

The attribution build and the race-free session-id persist each have one home

buildRunAttribution (src/otel-attribution.mjs) derives the milestone/story attribution object, and captureSessionIdOnRecord (src/run-session-capture.mjs) is the persist-without-racing-the-settle shape. Both production callers – the drive command and the assignment sink – call them rather than carrying hand-copies, so the F-04 guarantee cannot be present in one caller and absent in the other.

Every spawned session is labelled with the work it belongs to

OTEL_RESOURCE_ATTRIBUTES is set at spawn carrying run.id, story.id, milestone.id, phase, machine.id and worktree.id, alongside CLAUDE_CODE_ENABLE_TELEMETRY; a run with no declared phase carries no phase attribute rather than a fabricated one. The attributes are applied after the existing IDE-attachment scrub, so the scrub still removes exactly what it removed before and removes none of these.

aof ships no OTLP receiver

No module under src/** opens a listening socket for, parses, or serves an OTLP payload. The OTel surface is env-set-at-spawn only, and every figure this milestone produces is correct with no collector running anywhere (FF-6808).

68/02 · Spend ingest at settle

A run’s spend is stamped as it settles

src/run-spend-ingest.mjs reads the session’s own transcript at settle and writes the spend envelope onto the run record. completeRun applies the state transition first and then stamps, so the spend a run reports is written while the transcript that evidences it is still there.

The four token buckets are copied, not computed

input, output, cacheRead and cacheCreate are each the sum across the transcript’s own per-turn usage objects; none is derived from another, and the writer’s mutual-exclusivity convention (FF-6803) accepts the result.

A session’s subagents are part of the session

The ingest walks the whole transcript tree under <projectsDir>/<sessionId>/, so a session’s subagent turns are counted in its totals – the same boundary the driver’s own transcript walk uses, so the two never disagree about what a session includes.

Model, effort, turns and tool calls are recorded from the transcript

The envelope carries what the session actually ran; turn and tool-call counts are counted rather than estimated. A session that ran more than one model records that it ran more than one, and the buckets still total every turn.

Absence stays distinguishable from zero

A run with no session id, an unmatched session id, a missing transcript directory, an unparseable transcript, or a transcript with no usage leaves spend: null – not measured. No zero is fabricated; a genuinely free run is the only thing that records costUsd: 0.

An ingest failure never changes what the run did

Outcome, state, attempt and retry lineage are unchanged when the ingest fails, and the failure is reported (run-store: spend not stamped at settle: …) rather than swallowed.

Spend is stamped once and read verbatim

Settling a second time does not re-stamp, and a later read returns the stored envelope without re-reading any transcript to produce it.

A stamped envelope survives the drive path’s attribution write

A spend stamped at settle is no longer reverted to null by a late whole-record attribution write on the local drive path – pinned by story 68/01’s tasks/02 scenario a stamped spend survives the attribution write, which drives a real transcript through the real drive command.

68/03 · One run, one item

Attribution is a join on a stored key

aof work observe resolves the item an agent run belongs to from the run record’s sessionId, not from the text of the prompt; an agent run whose prompt names another item is attributed to the item its session’s run belongs to.

The free-text matcher does not exist

agentMatchesMilestone has no definition and no caller anywhere under src/**; no attribution path in the codebase tests item identity against free text.

One agent run belongs to exactly one item

Across a whole work stream no agent-run identity appears in two items’ attributed sets, so the per-item totals can be summed without billing the same hours twice.

An unresolvable run is reported, never guessed and never dropped

A run with no resolvable session is counted into an explicit unattributed total that the report and the --json document both state; it is not assigned to an item by fallback and not silently discarded.

Tool calls are classified by what the result says, not by the command name

classifyToolCallResult reads the tool result’s own output for count-bearing and structural test-report markers. The retired TOOLCHAIN_RE command-name pattern – which matched npm test, vitest and jest, none of which this repo is permitted to run – no longer decides anything, and this repo’s real isolated AOF_GLOBAL_HOME=… node --test … invocations classify as test runs.

68/04 · Observe answers for a story and a phase

Story-scoped observe

aof work observe <NN>/<SS> resolves to the story’s own folder under <milestone>/stories/ and reports on it; the bare milestone form answers exactly as it did before.

Refusal of an ambiguous ref

A bare story-shaped ref such as 00 is refused by name rather than substring-resolved to a top-level item, and every refusal states the ref it was asked for.

Per-phase rollup

The observe report breaks its totals down by the phase declared on brief.loop.phase, each phase row drawn only from its own runs and summing to the attributed total.

An explicit no-declared-phase grouping

Runs carrying no declared phase are reported under their own grouping with a run count, never distributed across the declared phases and never given a guessed one.

A stable --json contract for the scoped answer

The registered observe command’s --json document carries the item’s totals, the per-phase breakdown and the per-agent rows under one key set, identical in shape for a story ref and a milestone ref, with unattributed counts and unmeasured spend stated rather than omitted.

68/05 · Append-only snapshots

Observe snapshots are append-only

An observe run writes a new timestamped artefact under observability/snapshots/<ts>/; no write path in src/work-observe.mjs opens an existing snapshot for truncation or rewrite, so a snapshot a retrospective cited still holds the figures it cited after any number of later runs.

Snapshots are ordered without being read

Each snapshot is identifiable by when it was taken, and their order is determinable from their names alone.

A newest-snapshot read path

readLatestSnapshot resolves the most recent snapshot and leaves every older one unchanged.

Pre-68 snapshots are marked, not migrated

An existing pre-68 in-place snapshot gains exactly one derivation header naming the pre-68 miner and the four provenance facts it is subject to – possible double-count, blind toolchain figures, possible overwrite, pre-68 producer – while its figures are left exactly as they are: none recomputed, corrected or removed.

Marked and unmarked snapshots are distinguishable without reading figures

A post-68 snapshot carries no pre-68 header, and marking is idempotent – an already-marked snapshot is never given a second one.

66 · Controls That Run

Structural contract lint at validate

aof work validate parses every .feature under <work.dir> and reports free text in step position as a structural finding naming the opening line; run today it reports one file and is silent on the thirteen under done items.

The acceptance horizon

One exported predicate in src/acceptance-horizon.mjs decides whether a record is still editable; every check calls it, it holds the frozen five lifecycle words, and it has zero imports.

One Gherkin reader

src/feature-parse.mjs is the only module under src/ that recognises a Gherkin keyword; it returns structural findings beside the scenarios it could recognise, and src/work.mjs reaches the grammar only by import.

One declaration grammar

src/declared-id.mjs is the only home of the id namespace, the register-block set and the declaration/citation recognisers; memory’s two parsers build their headers from it, and the records they produce are byte-identical across the extraction.

The controls lane

aof work doctor carries a fifth lane of pure (snapshot, ctx) => Finding[] groups emitting exactly eight frozen codes; it performs no process spawn, no dynamic import() and no filesystem read outside the snapshot, and on this repo it reports 20 errors and 17 warns against work that predates it.

Two-leg control resolution

A declared control resolves when its cited path exists on disk and a declared runner names it; an unlanded control marked pending reports at warn, and an unmarked one at error while its item is open.

The accept gate

No item whose status is done may carry a register declaring a control that does not resolve – enforced over the real stream, marker or no marker, and green today at 2 registers / 16 declarations.

The shipped VERIFICATION.md schema

ACD ships a VERIFICATION.md template carrying four frozen headings, a findings register on seven frozen columns with the id alone in the first cell, and a fitness register whose red probe cell is byte-equal to the constant the check compares against.

Seven authoring asks

src/bundle/ carries the declaration form, the red probe, the runnable path and pending, unnumbered findings, the citation form, the accept precondition, and write-apart – each present in every file whose reader must obey it, and each guarded by a frozen-token assertion.

64 · Green the reds that arrived on main – six arch gates and two racy tests

The inherited fitness lane is green with no gate relaxed

All seven inherited-red gates (acd-graphify-backend-selection, acd-memory-backend-selection, acd-no-new-silent-catch, acd-work-command-route-coverage, command-core-contract’s registry list, acd-bundle-manifest-hashes, and the resume lane) pass because the invariant each asserts holds at the source – no baseline was moved, no carve-out was widened, and config.memory?.backend has exactly one code read at work-memory.mjs:77.

The mesh timing-race class is fixed, not observed green

The three lanes that resolved or advanced without waiting for their observer – the resume lane in mesh-terminal-input-path.test.mjs, and the premature-done and session-tree lanes in mesh-worker-completion-detection.test.mjs – now settle in every interleaving, and every sibling resolve/lever site in the suite carries the same waitFor guard.

The work:* family rule is exercised, not assumed

A newly registered work command lands in WORK_IDS and in BOARD_DEFERRED (or a served route) in the same commit – held the first time it was exercised, by milestone 63’s work:trigger.

63 · Event-driven triggers

aof’s loop can be woken by something other than a person

A trigger is reviewable data that resolves to a work:loop input and the argv carrying it, with no session acting as orchestrator anywhere on the path. Four sources – a cron cadence, a mesh assignment, a CI signal and an inbound feedback finding – all resolve through one compiler and one face. The declaration this tool ships is non-vacuous over itself: every declared source has a trigger that resolves. Stated whole by m63/00, m63/04 and m63/05.

The trigger layer is a caller, and that is structural rather than careful

Nothing under src/work-trigger/ or src/commands/trigger.mjs spawns a process, drives a phase, performs gate arithmetic, holds a clock or a receiver, or authors a slash command – and work:trigger declares no cli.launch, so it cannot launch even by accident. The one launcher 53 left the loop stays the only one.

An unattended run is bounded by a declaration the machinery enforces, not by convention

The frozen set’s fourth enforcement point compiles: gate-order leaves deferred, and an unattended launch resolves only the declared program and leading argv – anything else is a coded refusal carrying nothing spawnable. Every attended launch is byte-identical to what it resolved before. Stated whole by m63/02.

A declared level is a request the gate re-decides at every fire

No trigger caches an admission verdict, no config key or flag admits L3 by the back door, and a refused level is named with its failing half rather than silently downgraded. Stated whole by m63/01.

The one dispatch phase with an orchestrator to remove no longer has one

autonomous resolves to a loop launch carrying a scope; refine, continue and verify send the bytes they always did. Stated whole by m63/03.

An unattended loop run is watched as a process and settles on its own exit

Stated whole by m63/06.

What holds all of this is eight controls, each of which has been seen red

Every declared control has a recorded red probe – the change made, the message observed, the revert proven byte-identical. Three of them are green over trees that never held the defect, and their probes are the only evidence they are armed.

63/00 · The trigger declaration

A reviewable trigger declaration at .aof/triggers.jsonc

Which signal may wake which scope at which level is one JSONC file in the repository, installed from src/bundle/triggers.jsonc through the existing hashed, drift-protected bundle path and byte-identical to its source.

One compiler, and no partially-compiled set

src/work-trigger/declaration.mjs is the only module that parses the declaration; a member that fails validation raises a coded refusal and the whole compile refuses, so no trigger set is ever returned with a member silently skipped.

The cadence grammar is imported rather than copied

parseCadence is an additive function export from src/work-loops.mjs; the trigger family authors no cadence regex, duration-unit table or periodic:/event: literal of its own, and src/work-loops.mjs still exports eleven frozen sets.

A trigger faster than the loop it wakes is a computable contradiction

A trigger and the loop-registry entry it points at are compared on the operands the parsed cadence already carries (ms for a duration, scopeRank for an ordinal), and the faster trigger is reported by name.

Every .aof/** bundle asset is line-ending pinned by a ratchet

.gitattributes pins .aof/**/*.jsonc text eol=lf, and the pin is asserted from the bundle’s own asset list rather than a hand-kept path list, so the next .aof/ declaration cannot arrive unpinned.

63/01 · The level is a ceiling, not an admission

A declared level is a ceiling request re-decided at every fire

src/work-trigger/level.mjs resolves a trigger’s declared level by importing resolveLoopLevel and resolveLoopLevelGate from src/work-loop.mjs and being handed the two gate facts; the same trigger resolved twice across moved gate facts gives two different answers within one process, and no compiled trigger carries an admission verdict.

A refused level is refused by name, never downgraded

A declared level that fails its gate resolves to a coded refusal naming the failing half – the score with its failing check ids, or the components that came back self-referential or stale – and the resolved set contains no entry for that trigger at any level, so a downgrade cannot hide as a successful resolution one rung lower.

An absent level and a refused level are different answers

An absent level takes the loop’s own default; a declared level that fails takes nothing. The two are never rendered as the same outcome, and a gate fact never supplied is its own coded refusal naming which one was missing, distinct from a fact that failed.

Admission stays in one home

The leaf holds no score threshold, no 100, no groundedness predicate, no component-state literal and no level literal beyond what it imports; it performs no filesystem read and no invoke, so it computes nothing the loop’s own gate would have to agree with.

63/02 · The launch envelope compiles

The frozen set’s fourth enforcement point compiles

compileFrozenSet(bundledFrozenSet()).deferred is empty and installed carries gate-order; COMPILED_POINTS equals FROZEN_ENFORCEMENT_POINTS in full, so all four declared enforcement points now produce compiled output.

The unattended launch shape is a declaration, not a convention

gate-order’s rule is { "program": "aof", "args": ["work", "loop"] } in both the bundled source and the installed copy, and the compiled artifact’s program and argv come from that declaration – changing the declared shape changes the compiled artifact. The declaration is the milestone’s one program literal; no other program spelling for this launch exists in src/.

An unattended launch that is not the declared one is a coded refusal carrying nothing spawnable

A mismatched program, mismatched argv, absent program, absent argv, reordered argv or spliced argument each returns a coded refusal naming the envelope member, with no program, no arguments and no environment attached – and it is distinguishable from the answer an unresolvable runtime has always produced.

Every attended launch is byte-identical to what it resolved before

For a human session, a phase-driver session, all three single-phase mesh directives, a resume and a session carrying --model/--effort, the resolved { bin, args, env } is identical key-by-key to the same call with the fourth point compiled out. resolveInteractiveDriverLaunch keeps its 23 dependents, its NEEDS_INPUT sentinel and its cache/telemetry decisions untouched.

The fourth point is traced, not merely counted

The compiled unattended-launch artifact carries the id of the member that declared it, and a declared, owned member at that point that reaches no output is a failure – the same trace the other three points already get.

63/03 · A mesh assignment resolves to a loop call

The autonomous phase dispatches a loop launch, not a slash command

An assignment on the autonomous phase resolves to a launch of kind loop carrying the assigned scope and nothing else. Exactly one of the four dispatchable phases resolves to a non-session launch, driven from ASSIGNMENT_PHASES rather than a literal list, so a fifth phase cannot be added silently.

The other three phases are byte-unchanged

refine, continue and verify produce the exact directive strings a delivered tree produced before this story, including refine --autonomous, and exactly one module in src/ authors an assignment phase’s slash command.

The launch rides the directive additively

The launch travels on the directive beside baseBranch and commit; the assignment record’s key count is unchanged at the frozen ten, and the worker reads the launch in exactly one place.

A story-shaped ref is refused before a directive is sent

The resolver refuses a story-shaped ref on the autonomous phase with a code, and the dispatch tick over such a row sends nothing – while the same ref on continue and verify resolves exactly as before.

No topology, PTY or NEEDS_INPUT machinery moved

Leasing, reclaim, presence, routing, PTY spawn, output chunking, completion detection and the NEEDS_INPUT sentinel behave as they do at HEAD; src/agent-session-driver.mjs holds none of this story’s identifiers.

An old worker handed a loop directive idles rather than doing the wrong work

Observed live on a genuinely skewed pair: a worker built before this story receives the directive, finds no command it understands, and spawns its session with nothing typed into it – no cascade fallback, no loop argv as text – holds the assignment rather than dropping it, writes no commit to its worktree, and settles on the deadline policy it already had.

63/04 · The signals that are not the mesh

Three non-mesh trigger sources that answer only which scope

src/work-trigger/sources.mjs resolves a cadence, a CI signal and an inbound feedback finding to a scope and nothing else – no source carries a level, a cap, a gate or a launch.

A finding-triggered wake never classifies

The family cannot reach a feedback record’s body: neither RAW_FEEDBACK_KEYS’s text field nor FEEDBACK_CLASSIFICATION_KEYS’s verdict field is reachable from it, and two planted captures whose bodies differ and whose attribution is identical resolve byte-identically. Only existence and attribution are read.

A CI signal is read for its ref, never for its outcome

No source reads a build status, a pipeline name or a failure class, asserted over a planted signal carrying all three.

Every scope resolves through the loop’s own forms, or is refused by name

Scope is resolved only through LOOP_SCOPE_FORMS; no scope pattern, range grammar or item-ref regex is authored in the family. A story-shaped signal is a coded refusal naming the driver it belongs to rather than an implicit widening to a whole-stream walk, and a well-formed driver ref bearing no item resolves – the leaf performs no filesystem read at all.

A source that cannot answer refuses, never resolves to nothing

An unresolvable signal produces a coded refusal naming the source and what it could not resolve; the refused and resolved sets are disjoint and together account for every signal, so nothing is dropped between them.

63/05 · The trigger’s face

One registered command that resolves a trigger and launches nothing

work:trigger composes the four src/work-trigger/ leaves and emits, for each declared trigger, the work:loop input it resolves to plus the argv that carries it. No module in the family spawns or execs a process by any spelling, drives a phase, holds gate arithmetic or authors a /aof: slash-command literal – proven through the family’s whole static import closure and by a real run that starts no process.

The projection validates against work:loop’s own declared input

The object the face projects carries work:loop’s input keys plus one identity key naming the declaring member, and the level flag it composes is read back from getCommand("work:loop").cli.spec.flags – so a renamed loop flag cannot leave this face composing an argv the loop refuses.

The gate facts are obtained through the registry and handed in

The two gate readings arrive through invoke at the command boundary, exactly as work:loop gathers them when it fires, and are handed to the leaf uncoerced – no ??, || or ?. fallback stands between the registry’s answer and the leaf. A registry that cannot answer is a reported failure rather than a locally computed verdict.

aof holds no clock and no receiver, and the face writes nothing

No timer, cron evaluator, HTTP server or listening socket is reachable from the family or its closure; the one setTimeout in the closure is renameWithRetry’s bounded write backoff and is pinned as a shape. Two runs over one fixture tree, in both renderings, leave every byte where they found it. The family declares no --strict and no --dry-run, and --json and the human face render from one object.

The exit code is a cause, not a case table

A run that produced a resolution exits 0 however many refusals it carries; a run that produced no resolution states the failure first and exits non-zero. Refusals for an unknown trigger, an unknown source, an unresolvable scope and a refused level are each reported by code with the sources that exist named.

The shipped declaration actually resolves

Over this repository’s own .aof/triggers.jsonc, every declared source has at least one trigger that resolves to a well-formed work:loop input, every resolved scope resolves through LOOP_SCOPE_FORMS, every resolved argv begins work loop and names a registered command, and no resolved trigger carries a level this workspace’s gate would refuse.

63/06 · The loop launch is watched as a loop

An unattended loop launch is watched as a process, not as a session

loopShapedTranscriptWatch(launchOptions) (src/mesh-worker-execution.mjs) returns null for every session launch and, for an unattended loop, supplies both transcript-watch seams as functions resolving null – each the driver’s own documented no-op – so a dispatched aof work loop is no longer bound to the first inner session it writes into its own worktree and killed roughly ten seconds later with done.

An unattended loop run settles on the process exit the loop already has

Settlement falls through to term.onExit and the delivered exit mapping: exit 0 settles done, non-zero settles failed. No thirteenth loop stop, no second completion signal and no halt vocabulary exists in any mesh module; LOOP_STOPS is still 53’s frozen twelve.

Exactly two spawn-bag forwards are launch-conditional, and each takes exactly one expression

watchTranscriptSessionId and watchTranscriptCompletion are injected ?? loopShape; ptySpawn, which, onOutputChunk and onSessionEnd remain bare shorthand, and a fifth that stops being one fails FF-6306. src/agent-session-driver.mjs is byte-unchanged – no PTY, streaming, completion-detection or NEEDS_INPUT machinery moved.

An injected watch still wins over the loop shape

A caller supplying its own session-id or completion watch keeps it for every launch kind, so no launch kind can render a supplied producer inert.

A session assignment is handed exactly what a delivered tree hands it

A session-phase directive supplies no session-id watch and no completion watch at all, so the driver reaches its own session-shaped default unchanged.

The loop-shaped seam is held by a control that has been seen red

FF-6306 carries the ADR-013 §1 leg with its own positive control – the session-shaped default is run first over the same worktree and must bind a planted inner transcript before anything is concluded from the loop-shaped seam refusing to – and the relaxed §3a form’s red probe is the revert to bare shorthand, which reds both the structural and the driven leg.

62 · The self-improvement loop

The hill-climbing loop is closed

The raw material aof accumulates about itself – retrospective lessons, run lineage, observability readings – now has a consumer. aof work tune is the first path from that material to a named harness change, and the loop that was open at exactly the point where it would close is closed at that point.

The pass says something about this repository, and says it completely

Run over its own corpus at accept, aof work tune emits 63 proposals, both lanes populated (62 advisory, 1 tunable on work.loop.reviewRounds), every proposal carrying at least two distinct resolved source documents and a measured distance, at exit 0. This is the one property no suite over fixtures could establish, and it is a standing condition rather than a recorded figure: the control re-derives it from the corpus as it stands on every run.

The proposer ships with no acceptance rule of its own

62 was sequenced after 61 and holds no second opinion about when a change may be believed: the tunable lane’s verdict is obtained by invoking work:acceptor and by no other means (m62/04), and no module in the family carries 61’s ruling vocabulary, thresholds or arithmetic. The configuration the self-evolving-agent literature measures failing – a proposer that can also decide – is not reachable here, and is not reachable by falling back either: a registry that refuses the acceptor makes the lane report a construction failure rather than compute its own verdict.

What stands between a proposal and a commit is measured, not asserted

Across the whole emitted set the answer is 5 distinct obstacles, and the largest is a fact about this harness rather than about the loop: 3 tunable knobs are declared on the arbiter’s tuning edge, and 0 of them reach a decision site – every one is resolved and then discarded, or composed into an object nobody reads. The tuner’s first honest finding is that this repository’s declared knobs are not yet wired to anything that decides.

The generated half is separated from the applied half by a locked level

Proposals are data at every point in the arc. No path applies one, the only write in the arc stays 61’s store, and the L2 diff is rendered for a human to accept (m62/04). What ships is a proposer whose output must still earn its commit.

Nine controls that have each been observed failing

Every structural invariant this milestone declares is enforced by a control that has been broken, run, seen red for its stated reason, and reverted – recorded per control in VERIFICATION.md. Two of those probes found defects no green run had, which is the only evidence the register is armed rather than merely populated.

62/00 · The corpus and its floor

The three-lane tuning corpus

assembleCorpus returns exactly three declared lanes – lessons, lineage and observations – measured at 410 retrospective lesson sections, 61 run records and 6 observation readings over this repository.

A floor beside every count

Each lane is emitted with the root it walked, the population it found and the floor it was measured against; a lane declared without a floor is refused before a count or a report can exist, so “found nothing” and “looked at nothing” cannot be rendered alike.

tune-ran-on-nothing

A lane below its floor emits a finding naming the lane, the root, the floor missed and the scope it was measured under, and that finding differs from the audit lane’s in its code string alone.

Source-owned readers

Every lane reaches its source through the module that already owns it – parseRetrospective, readRuns, readLatestSnapshot – and no raw runs/*.json or snapshots/*/agents.json path grammar exists in the family; scope resolves only through src/work-ref-scope.mjs.

62/01 · The proposal, its lane and its patch

The proposal object

A proposal carries class, target, lane, laneBasis, evidence, patch, applier, reason, finding, provenanceResolution, sourceFacts and distance, and its class comes from a frozen set equal to SPEC’s four.

A lane the registry decides

Lane membership is computed from tunableSet(model)’s keys – the arbiter’s own parameter-tuning: edge – so no module under src/work-tune/ holds a tunable key literal or a class→lane map, and a knob removed from that edge leaves the tunable lane with no edit here.

Routing is not committing

A non-ordinal key declared on the tuning edge is routed to the tunable lane and its refusal comes back from the acceptor; no path in the family decides ordinality or diverts a proposal on the ground that 61 would refuse it.

No computable patch, no applier

A proposal with patch: null carries applier: null and a coded reason, while a patch with no applier is a reachable, rendered state – the asymmetry runs in one direction only. Every applier resolves through the injected resolveCommand to a registered command, and is never a shell string, a file path or a prose instruction.

absent is a value

A patch over a key the config does not carry renders from: absent with a fromSource that says so – at HEAD that is every work.loop.* key and the whole model map – and the shipped bundle default appears nowhere as a from.

A patch renders only on the base its evidence assumed

The four-row base table is driven whole: absent/assumed-absent and held/assumed-that-value render; absent/assumed-a-value and held/assumed-otherwise are moved premises with both readings named. A proposal whose from equals its to is absent from the emitted set and present in findings as already-in-force.

62/02 · Provenance that resolves

Citations checked against disk at emit time

Every citation an emitted proposal carries is resolved when it is emitted – the file is present, and where a line is cited the file has that line. Over this repository 63 proposals are emitted and none carries an unresolved citation.

Demotion rather than a footnote

A proposal carrying one unresolvable citation is absent from the emitted set and present in findings with the failing citation named; the two sets are disjoint. A real run reports 24 such demotions and 506 below-evidence-floor findings, and none of them reaches the exit code.

Both grammars imported, neither re-authored

Path citations come from pathCitationsIn / splitPathLocator (src/work-doctor-controls.mjs, two additive exports) and id citations from qualifiedRefsIn / QUALIFIED_REF (src/declared-id.mjs); no module under src/work-tune/ holds a citation regex, locator pattern or ref/id pattern of its own.

controlPathsIn is byte-unchanged by the split

The doctor’s control lane answers exactly as it did before the two exports were carved out, asserted as a self-comparison over every ## Fitness functions register in wiki/work, with isControlFileName’s test-shaped filter still applied on that path.

A qualified id or nothing

Every id citation matches m?<itemRef>/<ID>; a bare id is never emitted as a citation and no bare-id extractor exists in the family.

An evidence floor counted in documents

The floor is two distinct resolved source documents, so an id citation and a path citation naming one document count once. Over this repository every emitted proposal clears it, at between 2 and 9 distinct documents.

62/03 · The distance to a live proposal

A measured distance on every proposal

Every emitted proposal carries what stands between it and a commit as a structured obstacle list, not a sentence. Over this repository all 63 carry one, across 5 distinct obstacles.

Removals that are the acceptor’s own words

The removal text for a tunable-lane proposal is taken from the acceptor’s refusal records (refusals[].removal); no removal sentence for any member of 61’s ruling vocabulary is authored under src/work-tune/, so a copy cannot drift from the original.

One fact renders once

Where the acceptor reported not-admissible, limb (a) appears only as the measurement beneath that entry and never as a second entry; a standalone limb entry appears only where the acceptor never spoke.

Limb (a) is read, not re-derived

Its sites, inspections and dispositions arrive on the acceptor’s own report – at HEAD, 3 knobs examined, 0 consumed, 3 remaining, with the decision sites named down to file and line. No module under src/work-tune/ builds a unit set or walks src/, so 62 cannot become a fourth tree-walker that disagrees with 61 about one tree.

unknown and not-reported are different answers

A limb the acceptor was never asked about is unknown – over a registry declaring no tunable key the acceptor is not invoked at all – while a limb that was asked and does not stand is simply absent, and neither is reported as zero.

Shrink-only

The limb set only ever gets smaller: one accepted item whose consumed runs are all attributed drops the attribution limb out, and a closure planted for each limb removes that limb alone.

62/04 · The tuner’s face

aof work tune [scope] [--json]

A registered core command whose bare face is a read. It composes the four stage-1 leaves into one report – headline, scope, corpus, formation, proposalEvidenceFloor, proposals, findings, acceptor – and --json and the human face render from one object.

A verdict obtained only by invoking the acceptor

The tunable lane’s verdict comes from exactly one invoke("work:acceptor", …) call site, with the id resolved from getCommand rather than spelled twice. A registry that refuses the id makes the lane report a construction failure; there is no locally computed fallback, which is the second acceptance rule this milestone may not carry.

A read face that writes nothing

Running the command twice over one tree leaves every workspace byte unchanged and accumulates nothing between runs. The two writes it can reach are outside the workspace and named – reportDegrade’s mesh-log append and censusSnapshot’s temp copy inside work:acceptor – and the family opens no journal of its own.

A two-sided exit code

Every refusal, finding, demotion, below-floor candidate and empty result exits 0, so no findings gate arrives by the back door; an unreachable work:acceptor is reported first in --json as acceptor-unreachable and then exits non-zero.

The registry ring stays open

No module in the family statically imports src/command-core.mjs – asserted per module in a fresh process, the only probe that sees this class – and the applier resolver is injected by the face rather than obtained by a leaf.

A lesson’s structured config pointer becomes its target

loopPointersIn (src/work-loops.mjs) lets the corpus join the one unambiguous config: pointer in a lesson section to that lesson, and the section’s path citations to its evidence; embedded pointer-like text inside a URL or a larger token does not promote a target.

62/05 · Candidate formation

A partition, proven two ways

formCandidates puts every record on exactly one candidate, asserted as membership and as arithmetic, so neither a dropped record nor a double-carried one passes. Over this repository it takes 796 records into 593 candidates.

Lossless citation carry

A cluster carries every citation its members carry – 654 citations behind 402 lesson records at the gate, where 154 records carry more than one – so the distinct-document evidence floor downstream is measured against everything that stands behind a candidate, never a sample.

A tie-break that reads content

The tie-break is the lexicographically least source citation; no input index, position or iteration order is reachable from it, and a shuffled input yields byte-identical candidates over both planted ties and the real lessons lane.

A criterion with a declared ordered range

minimum-shared-metadata-fields names its four dimensions, declares a loosest and a tightest admitted value, refuses anything outside that range with a coded error, and defaults to 3.

A default that must re-earn its place

The tradeoff justifying 3 is recomputed against the corpus as it stands on every run – the largest loose cluster must collapse, recurring multi-source classes must survive, and the tightest setting must fragment – with the dated measurement kept beside it as provenance rather than as an expectation.

A pure leaf

formation.mjs takes records and returns candidates: no filesystem, no clock, no argv, no static import of the command registry. A candidate carries exactly sources, citations and a bare-ref target, and attaches no lane, patch, applier, verdict or distance.

61 · The disciplined acceptor

aof has an acceptor, and applied to itself today it refuses everything and says why

Every lane a harness-change proposal must clear is in service: a range each step stays inside (m61/00), an epoch whose criterion cannot be accrued across (m61/01), a census that declares what it filtered (m61/02), an admissibility check worded on consumers (m61/03), one derived commit condition over a ledger that accrues (m61/04), the ruling recorded beside the value it moves (m61/05), and one command that renders all of it (m61/06). Run against this repository, it admits three knobs and refuses all three – each for at least two independent reasons – and reports the reasons with what would remove them. That is the deliverable: not a system that tunes itself, but one that can state precisely why it will not, in terms an operator can act on.

The threshold is unarguable because it is nowhere written down

No number in this machinery is a preference anyone can revisit at eleven at night. The commit level, the bet, the earliest crossing and every later crossing record are arithmetic over the criterion’s own inputs, and the controls forbid any literal equal to them appearing in the engine (m61/01, m61/04). Revising an input moves the whole lattice; a criterion that would make the rule trivial is refused at construction. The one quantity that is chosen rather than derived – the budget – is frozen inside the epoch it is scoring, so extending it mid-flight to reach a crossing is the same p-hack as moving the yardstick, one axis over, and is refused as one.

Report-only is structural, not a default that could drift

The property holds across the whole milestone rather than at any one point in it: the command declares no strictness flag and no board route serves it (m61/06), its only mutating form takes an explicit named request, the evidence that request is weighed against is read from the ruling store rather than accepted from the caller, and the event the machinery raises is named for the ruling rather than the change – so the overwhelming majority of its work, every honest refusal, leaves a record (m61/05). “The acceptor did nothing” is its steady state and is reachable by no other path.

Three enforcement layers, and the load-bearing one is not access control

A permission boundary (the criterion as the frozen set’s sixth member), a writer refusal at the criterion seam, and beneath both the ledger’s own arithmetic. The first two can be walked past by anything able to write a file. The third cannot be walked past by anything, because the leaf that sums evidence never sees a criterion digest: summing across two criteria is not a path it refuses, it is not a sentence it can express (m61/01, m61/04). A criterion edited mid-epoch does not have to be caught.

The instruments the gate trusts are bounded by what this system can actually see

Two milestone-level facts sit beneath every verdict. The evidence journal has no fixture boundary and no workspace identity, so every population the acceptor counts is filtered, folded and declared against a floor, and a sweep that found nothing says so rather than returning a confident zero (m61/02). And what may be proposed at all is read from the loop registry’s own declaration, never from a list this machinery keeps – so widening the tunable set is an edit to the registry, and the acceptor holds no knob key anywhere in its code (m61/03).

The acceptor is a gate, and nothing in the graph can point at it

It is deliberately not a control loop: no node kind, no loop record, no reference of its own, no cadence it sets, nothing it optimizes. Its epoch equals the cadence of the audit loop that checks its instruments, which is a decision rather than a coincidence – an acceptor whose epoch differs from that cadence is trusting instruments audited on a different clock. And no proposal may author or revise what “better” means: actor:operator is the only revisor, permanently.

Thirteen declared controls, every one of them observed failing

The milestone applied its own thesis to its own gates. Each of FF-6101…FF-6113 was run green, then run again with the invariant it guards deliberately broken, and the failure message recorded in VERIFICATION.md. Four of the thirteen extend a guard that was already in service, where the probe is the only evidence the extension is armed at all. Two of the probes are evidence in the other direction: FF-6102 and FF-6110 stayed green when the metric name and the knob key were planted as strings, which is exactly the line those controls draw – a pointer is data an operator edits, and a key quoted in a diagnostic is not a claim of membership.

The gate was applied to this milestone’s own last story, and it refused it

61/06 reached acceptance built, green, and marked in-review by a hand-edit – a claim of review nobody had earned. The record refused it (D-61-3), the review then ran, and it found a Blocker a green suite could not: the reporting face preferred a pair sequence carried on its caller’s proposal to the one accrued in the ledger, so an explicit commit could be granted on fabricated evidence – the one enforcement point this milestone says cannot be routed around, routed around through the command’s own input. The story’s own test drove that path and asserted success, which is why the suite agreed with the defect. Fixed, locked by a regression, and red-probed.

61/00 · The clamp

Every steppable knob resolves inside a declared range

work.loop.buildNoProgressRounds resolves through a ceiling as well as a floor at its single funnel, resolveBuildNoProgressRounds in src/loop-bounds.mjs, so a value past either end comes back inside whichever door supplied it – both stall paths and the config path arrive through that one resolver.

Admissibility is the resolver’s own answer, and there is no second table to disagree with it

A proposed step is in range exactly when the knob’s own resolver returns it unchanged. LOOP_BOUND_VALUE_RESOLVERS is derived from the callables in src/loop-bounds.mjs rather than declared beside them, so a range this machinery believes in and a range the system enforces cannot diverge.

A key that resolves to TWO bounds is refused as a step rather than given an invented range

work.autonomous.maxAttempts resolves to an attempt ceiling and a per-phase drive-cycle ceiling – counted from its own call sites, measured in different units, exhausted by different events. Every step on it is refused by name with step-would-be-compound, and no range is manufactured for it. The key stays proposable: the refusal is about committing, not about proposing.

61/01 · The epoch and the frozen criterion

An epoch is one milestone, and its boundary is any transition INTO done

closesEpoch is exported from src/acceptance-horizon.mjs alone, compares against that module’s own CLOSED_STATUS, and takes the destination status the payload actually spells – status, the field item-status.changed carries, not to, the name the lifecycle table uses and no payload carries. A milestone parked in review before acceptance closes its epoch exactly as one accepted from build does.

A criterion that moved mid-epoch cannot be accrued across, and does not need to be caught

Every ruling carries a digest of the criterion it was rendered under, and rulingsUnderCurrentCriterion – the only module in src/ that selects rulings by digest – returns the maximal trailing run sharing the current digest. It is a suffix, never a filter, so a criterion revised and revised back resurrects nothing. Evidence gathered under a superseded criterion is not refused; it is not summable.

The criterion is the frozen set’s sixth member, revisable only by the operator and only at a boundary

A write through the criterion seam away from a boundary is a coded refusal that names the open epoch and writes nothing. The member compiles at permission denials, is aofManaged, and names both acceptor paths for Edit and Write; all four sites that pin the member set agree with the declaration.

A knob value that moved is reported by name rather than mistaken for a criterion move

A knob whose value changed under an accruing ledger is named in the report, so the two kinds of movement under a running trial read differently.

61/02 · The observation census

Every population the acceptor counts declares what it read, against a floor

Each population is emitted with a declared read record and a floor, driven from the lane registry – so a lane added without one fails rather than passing silently over nothing, and a sweep that read nothing says so instead of returning a confident zero.

Test fixtures are excluded and dispatch worktrees fold into their parent, in ONE home

src/work-acceptor/observations.mjs is the only module in src/ that classifies a fixture itemDir or a dispatch worktree. It derives the dispatch case from src/mesh-worktree.mjs’s exported predicate and slug rather than spelling dispatch-worktrees a second time, and a worktree nested inside a worktree folds all the way home rather than half-way.

A census that filtered nothing is a finding rather than a count

Below its floor the census reports a finding. The read record, the floor discipline and the problems list are imported from the audit family’s zero-import leaf (src/work-audit/reads.mjs) rather than re-spelled, so the acceptor’s shape and the auditor’s cannot drift; only the finding code differs, because that code is the auditor’s.

61/03 · No executed consumer, no proposal

A bound whose resolved value reaches no DECISION refuses every proposal on it

The rule is worded on consumers – a resolved value that reaches a decision – never on readers, and it ships as a second predicate beside unconsumedCeilings rather than as a sibling check, so the existing guard’s legs keep their semantics unchanged. At HEAD it refuses all three declared knobs, which is the correct answer: the same tree that satisfies the reader leg fails the decision-site leg for every one of them. The reported set is a subset of the registry’s declared tunables, so a knob that gains a decision-site consumer drops out without the gate needing an edit.

Where consumption cannot be decided, the answer is refusal rather than a guess

The harness switch is evaluated over the declared document. A prompt names no configuration key, so consumption is not statically decidable for it and every proposal is refused – fail-closed. The report names which condition it fell back on, and the switch re-opens the moment that document names a knob.

What may be proposed at all is the registry’s declaration, never the acceptor’s

No module under src/work-acceptor/ holds a work.loop.* or work.autonomous.* key in code – not in code, not in a string, not in a comment in the admissibility module. The admitted set is resolved from arbiter:speed-thoroughness-autonomy’s parameter-tuning: edge through loadLoops and is identical to it; a key outside that edge is a coded refusal, and an empty declaration admits nothing rather than falling back on a built-in set.

61/04 · The rule and the ledger

The commit rule is ONE object, and its numbers are derived rather than typed

No literal equal to N, to 1 + lambda, to 1 / alpha or to alpha appears in the engine – every one of those quantities is obtained from the criterion, so a criterion carrying a different lambda moves the whole crossing lattice rather than leaving a typed number standing. The commit predicate has exactly one leg; “all favourable” is spelled nowhere. lambda is refused at construction outside (0, 1), so the hard reset cannot arrive through the parameter.

A loss carries rather than resets, and “spent” reads differently from “short”

An unfavourable pair multiplies the wealth by (1 - lambda); no path resets the ledger or drops a proposal. A losing proposal that can still reach a crossing is reported recoverable, and one that cannot is budget-exhausted – which is never reported as evidence-short. B is read from the criterion and refused below N.

The trial metric is a declared pointer, and an unmeasurable arm is neither a tie nor a win

Metric and counter-metric are pointers resolved through a registry derived from callable resolvers, so a pointer naming a symbol that does not exist is refused at construction and a criterion carrying a metric and no counter-metric is refused outright. Both pointers resolve into the same zero-import deterministic-counter leaf, and no second counters home exists in src/. An unmeasurable arm is counted in neither total: no path maps it to a tie or to a favourable pair. At HEAD every arm is unmeasurable, and the machinery reports that rather than concealing it.

One threshold, many baskets – computed per knob, in integer arithmetic

Every admitted knob resolves a declared trialUnit with a unit price, and its basket is the single declared expression, recomputed independently per knob; no threshold or basket constant is applied across knobs. The pair count is ceil(B * d / (d - n)) over a declared rational with integer terms, so the 90% case returns 110 raw pairs and not the 111 the float path gives. A knob priced above the budget is trial-unaffordable: it accrues zero pairs and stays on the surface carrying its price and the ceiling it exceeded.

A step is one knob and one notch

A proposal naming two knobs is a coded refusal rather than a split, and a knob whose values carry no order is refused as a step by name.

The ledger accrues ACROSS epochs and refuses an incomplete ruling at construction

The epoch id is carried and reported but is not a filter on the sum. RULING_KEYS is a frozen set carrying dwell and dwellFrom and no computed dwellExpiry; a record missing any key is refused at construction and again at assembly. The W/L/T sequence is stored in the order it arrived and no code path re-sorts it.

The arithmetic is blind by construction, not by rule

The engine and the summing leaf import nothing, read no clock, and take now on the call. The summing leaf never sees a criterion digest, so summing across two criteria is not a path that has to be refused there – it is not expressible there.

61/05 · The event a ruling raises

harness.ruled is a declared event with exactly one reactor

EFFECTS gained exactly one name and no tenth; its reactor set is one entry carrying a known locus and an async apply, and the eight names already declared are byte-intact. The event is named for what actually happens – the ruling, whether or not anything moved – because report-only is the permanent steady state and an event called “changed” would leave every honest refusal untraced.

The why travels with the change, and an incomplete record is refused

The ruling record carries the key, both values, the epoch, the evidence in the order it arrived, the attained level, the counter-metric reading, the dwell declaration and who rendered it. The knob write is surgical: every other key, its order, the file’s indentation and its trailing newline survive a write. No module in src/ assigns at a declared tunable knob path outside that one seam.

The ledger has ONE writer, and it is git-tracked beside the config it justifies

src/work-acceptor/store.mjs is the only module in src/ that writes .aof/acceptor-ledger.jsonl; the store is reached only from the seam and its reactor, and no command or face reaches it. The ledger path is absent from AOF_GITIGNORE_ENTRIES – it is neither derived nor regenerable, and being tracked is what makes the record reversible from git rather than merely present.

The seam cannot report success with a knob written and no ledger line

The two halves are bound: a write that lands the configuration change without landing its justification is not a state the seam can return.

Redelivery changes nothing

Re-appending the same ruling identity yields a byte-identical ledger rather than a second line, and a different record under that identity is refused – so at-least-once delivery cannot inflate the evidence count the commit rule reads.

An undeclared event name is refused, so the vocabulary’s own comment is true for the first time

applicableReactors refuses an undeclared name with a code. Before this, a misspelled name appended silently and resolved to zero reactors – a consequence quietly owed to nobody.

61/06 · The acceptor’s face

One registered command shows every proposal, its evidence, its verdict and every reason it did not commit

work:acceptor is a member of the same registry every other work command is registered in, reachable as aof work acceptor, and it renders the five lanes rather than recomputing them: the range each step must stay inside, the epoch’s criterion, the census as it was counted, the admissibility answer, and the rule with its ledger. The machine-readable face is the canonical report object itself – the human face is a rendering of that same object, so neither derives a number the other does not carry.

Report-only is the default, and it is permanent rather than a state that drifts

A bare run reports and moves nothing; crossing the level changes what the report says about eligibility and changes nothing about what the command does. Committing is reached only by naming a key explicitly, and an explicit commit on an ineligible proposal is refused carrying every reason. The command declares no strictness flag at all, and its exit status is a fact about whether it could run – never about what it found.

The evidence a report reads is the ledger’s, and a caller cannot hand in its own

A proposal names a key and a step and says nothing about how much evidence stands behind it. Every pair the commit predicate weighs is read from the ruling store and passes through the criterion-digest suffix on the way in, so evidence recorded under a superseded criterion accrues nothing toward the current one. input is caller input; the ledger is read at the composition root.

Every applicable refusal is reported, in a frozen order, each naming what would remove it

The ruling lane is the eight-member vocabulary assembled from the constants at their declaring modules, and the reported codes are the declared order filtered to the knob’s own set – an ordering no discovery order satisfies by accident. A refusal matching no member is refused with a code rather than rendered as free text. Refusals raised while a ruling was being built never enter the lane: they are reported in their own right, and the admissibility grounds travel as detail beneath not-admissible rather than as members of it.

A structural silence reads differently from a short ledger, and both distances are computed

A knob accruing toward its threshold states its standing, the threshold and the rulings still needed; a knob whose observed yield cannot reach the floor states the epochs that yield would need; a proposal that can reach no crossing record inside its budget is named budget-exhausted and is offered no distance at all. The numbers move when their inputs move – a rising yield shortens the distance with no message edited – and a report in which every knob is yield-bound says so as one statement.

Dwell is recorded as declared and is unreachable from the response to harm

The settling period is read from the arbiter that governs these loops and recorded exactly as declared, with the landing epoch beside it and no date, duration or expiry derived from either. An operator’s reversion is refused naming the counter that would have to exist to show the period discharged. A counter-metric degradation withdraws the change at once on a path that reads no dwell value, and no option the command offers delays or waives it.

59 · The audit loop

aof has an auditor, and nothing in the machinery can point at it

The registry admits a sixth kind whose subject is the measuring apparatus (m59/00), and the endpoint vocabulary was deliberately not widened to match: no scheme resolves to an auditor:, so an auditor is a source and never a target. Nothing can supervise it, set its reference, veto it or tune it – the structural form of “the audit does not answer to the loops it audits”, rather than a sentence in a document. It has no vocabulary for acting either: the kind admits no actuator, so the prune reports candidates and removes nothing (m59/03).

The audit executes; the health command still cannot

Milestone 66 froze work:doctor as a command that may not run anything. This milestone built the thing that runs, and put the boundary on a command rather than a convention: no module under src/work-audit/ is reachable from doctor’s spine or from any module in its check registry, over the whole import closure, and the audit’s finding codes are disjoint from doctor’s so neither command’s severity table can decide the other’s meaning (m59/02). The two verbs are siblings that share a finding shape, a scope semantics and a --strict flag whose policy deliberately differs in exactly two cells (m59/04).

Absence has one shape, and a lane cannot report clean without using it

Every lane in the audit returns what it read against a floor, and states what it could not see. Both records – the read (m59/03) and the limit (m59/04) – live in one module, are complete by construction, and are refused rather than defaulted. That is the milestone’s actual mechanism: a dashboard cannot stay green through a report whose shape has no way to express “clean” without also expressing “and here is how much I looked at, and here is what I could not see.”

The recurrence guard for “part of the fitness gate is dead” exists, and it found a live instance on its first run

TECH_DEBT item 5 was repaired by hand in milestone 42 with nothing watching for the recurrence. There is now a watcher, and it is not a promise: registration is decided by membership of the runner’s assembled array inside the runner’s own process, twenty-six de-armed suites carrying 117 test entries were re-armed, six more that only scripts/test-unit.mjs had ever assembled were registered, and two suites that had rotted red while dead were repaired (m59/01).

Recorded evidence is re-executed by something that did not write it

The gap the objective names on the record – ACD’s @manual evidence written by the same agents that did the work – is closed for the fitness register: every row’s cited control is run in a bounded child and judged on the message it produces, with no verdict reachable from the recorded prose (m59/02).

Eleven declared controls, every one of them observed failing

The milestone applied its own thesis to its own gates. Each of FF-5901…FF-5911 was run green, then run again with the invariant it guards deliberately broken, and the failure message recorded in VERIFICATION.md. Four of the eleven extend a guard that was already in service, where the probe is the only evidence the extension is armed at all.

The audit found real defects on its first runs, including two of its own

Run over this repository rather than over fixtures, the audit reports 0 errors and 49 warnings across three lanes, each declaring its population against its floor. Two defects in this milestone’s own delivery were caught at its gate rather than by its stories’ lanes – a limits footer that printed undefined – undefined (D-59-3) and a bundle census that had not been moved by the diff that grew the tree (D-59-6). Both are fixed, and the first is now a control.

59/00 · The auditor kind

The sixth node kind

NODE_KINDS is closed at six – loop, actor, anchor, watcher, arbiter, auditor – and the five kinds that existed before parse exactly as they did, every record shipped before 59 producing zero new findings in the same codes and the same counts.

An auditor that has no vocabulary in which to act

ADMITTED_KEYS.auditor is its four required declarations (audits, measurement, cadence, escalation) plus the edge keys, and it omits actuator, optimizing, controlled, reference, ground, counter, determinism, layer, owner and ceiling; each omitted key is refused by the loader’s existing loop-key-not-admitted-for-kind rather than by a new code.

A declared subject that cannot be the work

An auditor’s audits: is a non-empty list of instrument pointers over module | command | config | loop | watcher | anchor, and an item: endpoint is refused – so “the audit does not review the work” is a checked property of the record, not a promise in prose.

Reporting is an outbound edge, and nothing points at an auditor

EDGE_KEYS gains exactly reporting and no seventh; ENDPOINT_SCHEMES is unchanged, so no record in the registry may name an auditor as a target. An auditor’s admitted edges are data-feed and reporting alone, and target-setting, veto, parameter-tuning and monitoring are refused at the endpoint.

An anchor that says when it was checked

checked: is admitted on kind: anchor alone, is optional there, takes an ISO calendar date, and refuses every SENTINEL_TOKENS member and every reserved field prefix – so an anchor is dated, undated, or refused, and there is no spelling by which one opts itself out of freshness.

59/01 · The instrument census

Registration is runtime membership, not a mention in the runner’s source

test/arch/acd-test-suite-registration.test.mjs imports the assembled tests array and decides registration by name-set membership for every *.test.mjs under test/, test/arch/ and test/integration/. No source-text lane survives in the file, the walked-file floor is asserted before the membership claim so a moved root fails rather than passes over nothing, and a suite whose module exports a runner-shaped array the assembled suite does not contain is reported by name.

The twenty-six de-armed suites, and six more nobody had counted

The suites whose spread sites vanished in 15e0a92 – 117 test entries dark for a month – are members of the assembled suite again, and two of them (mesh-node-identity, mesh-registry-store-seam) had rotted red while dead and are repaired. Six further suites registered only in scripts/test-unit.mjs, carrying 122 test entries that npm test never assembled, are registered in what CI executes.

One bounded seam for every child process the audit family starts

src/work-audit/spawn.mjs is the only way a module under src/work-audit/ starts a process: it carries a deadline, kills the child on expiry, reports a terminal deadline-expired outcome, and hands back the observed exit code. No caller passes a shell string and no second spawn helper exists.

The audit never runs project code inside its own process

No module in the audit family’s import closure contains a dynamic import() or a require, and every static import is a node builtin or resolves inside src/. The runner’s assembled suite is obtained from a child process rather than by importing it.

A sweep that reports what it read

Every lane the census registers returns a population count with a floor; a clean lane result is not representable without one, and a count below the floor is a finding naming the sweep, the root walked and the floor missed – so a gate that ran on nothing says so instead of reading green.

59/02 · Evidence re-run

The fitness register is executed rather than read

src/work-audit/evidence.mjs resolves every FF-NN row’s cited control in scope and runs it in its own bounded child process, one run per (row, control) pair – a row citing two controls is two runs and two verdicts. Every verdict in the lane is derived from a spawn result: an observed exit code and captured output. With the child’s result withheld, every row reports evidence-unrunnable; no verdict is reachable from the recorded prose.

The oracle is the failure message, and no code path reaches a count

A control’s disposition is decided by the message it produces, not by a pass/fail tally. The lane carries no tally identifier, no comparison against the length of a pass/fail collection, and no test-runner surface – an observation varied only in its MESSAGE moves the verdict, and one varied only in its CASE COUNT does not.

A citation that cannot run says what was tried

An unresolvable path, a control no runner assembles, and a control that exceeds its deadline are three distinct reports, each naming the path, the runner and the deadline. “Slow” and “broken” do not collapse into one verdict.

Recorded case count is a separate axis from pass or fail

A recorded size that no longer matches the observed one is reported as evidence-size-drift against the item whose register recorded it, and it changes no confirmed row’s verdict. The observed number is what executed, not what the file’s text declares.

The register’s own claims are re-run against this repository

Over the shipped work stream the lane reads 114 register rows and reports on each: measured 2026-08-30, four evidence-size-drift warnings against milestone 66’s register and 44 evidence-no-register statements for items that declare no controls – the latter reported as declaring no controls, which is not the same as clean.

Doctor still never executes, re-asserted from the executor’s side

src/work-doctor-controls.mjs reaches no child process, no dynamic import(), no node:fs/promises and no wall clock, and no module under src/work-audit/ is reachable from src/work-doctor.mjs or from any module in its CHECK_GROUPS registry – asserted over the whole import closure rather than over direct imports. The audit’s finding codes are disjoint from CONTROL_FINDING_CODES, so neither command’s severity table can decide the other’s meaning.

59/03 · Staleness, silence and the prune

An anchor that has not been refreshed is not an anchor

src/work-loops-checks.mjs judges every kind: anchor node against a window handed in on the call and answers in three states, not two: fresh, stale, and undated. An anchor exactly at the window boundary is not yet stale. Undated is its own answer, so the sixteen anchors shipped before 59 are not red on arrival.

A stale anchor degrades a grounding verdict rather than deleting one

A loop grounded only through stale anchors is not reported unanchored – the edge is declared and the authority resolves. The report distinguishes never grounded from grounded a while ago, which is the distinction an operator acts on.

Silence is one rule across every channel

An instrument that has produced no reading inside its own declared cadence: window is named, and the window comes from the record rather than from a per-channel table – so a new channel needs no new check. A declared cadence with no occurrence count supplied is unjudgeable with reason no-occurrence-count, never reported silent: a verdict about a question nobody asked is the shape ADR-004 §1 forbids.

A metric that has not moved is named, and the threshold has one home

A counter unchanged across the declared number of cycles is reported, and that number appears exactly once in the module – at its declaration. No check states it as a literal of its own.

A loop nobody consults is a prune candidate, reported and never removed

A declared kind: loop node with no inbound consumer and no observed execution is reported by name. Nothing is deleted: the auditor kind admits no actuator, and removing a node is an edit to a governed declaration.

Every audit lane declares what it read, against a floor, in one shape

AUDIT_LANES declares an id, a population description, a basis and a floor for each of the four lanes; a clean lane result is not representable without a read count, and a count below the floor emits audit-ran-on-nothing naming the sweep, the root walked and the floor missed – byte-identical to the census lane’s finding for the same read. The registry is closed and in bijection with the exported assessments, so a lane cannot arrive without a floor.

The checks leaf is still a pure leaf

The module imports nothing, names Date nowhere, reaches no monotonic clock, and holds no date literal and no numeric literal large enough to be a duration in milliseconds – including in exponential, hex, BigInt and product-of-literals notation. Every freshness and silence comparison rides on a now and a window supplied by the caller, and no kind admits a key by which a node could assert its own liveness, freshness, consultation or audit status.

59/04 · The audit face

One command over every instrument

aof work audit [scope] [--json] [--strict] is registered on the same command core work:doctor sits on, with a derived route and a CLI↔registry bijection, and runs every lane in the audit’s lane registry: the census, the evidence re-run and the registry checks. The report says which lanes ran and what each of them read. A scope narrows what is audited and is named in the report; a scope that matches nothing produces an empty report rather than an error.

--strict is the door into the gate, and it promotes nothing

work:audit exits 0 on an error finding without the flag and 0 on a warning even under it, gating only on --strict and an error. That is deliberately not work:doctor’s policy, which gates on an error either way and promotes a warning under --strict: the two tables differ in exactly the cells error/no-flag and warn/--strict. The finding set is identical with and without the flag on both commands, and --json’s healthy agrees with the exit code.

Bad news is addressed to the reference-owner and never to the audited loop

Every finding carries about (the instrument) and to (who hears). The addressee is computed: the instrument resolves to the loops that own it, each owner’s target-setting source is the audience, and any candidate that is itself an owner is subtracted. Over the shipped registry no finding’s addressee is a loop that owns the instrument the finding is about, and addressing does not vary with severity – severity says how bad, addressing says who hears.

An escalation channel the audited loop cannot absorb

A finding whose code is in the escalating set reaches the auditor’s declared escalation: actor as a second addressee, with the owner’s copy still present. Where no reference-owner resolves, the finding escalates rather than being dropped. The bypass terminates at an actor whose ground: is exogenous, never at another loop.

A day-one auditor, shipped and admissible

src/bundle/loops/instrument-audit.md is the framework’s own auditor, written in 59/00’s grammar. Every pointer in its audits: resolves to a file, a registered command or a declared node and none is a work item; its measurement is a registered command and cites no document as its authority; it declares no reporting edge to anything in its own audits:; and the shipped registry produces zero gating findings. Its cadence is declared (event:per-milestone) and nothing schedules it – the audit is runnable on demand and the record says so rather than implying a scheduler that does not exist.

Every declared limit is stated in the face, in one shape across all lanes

A lane’s limit – the sentence saying what this run could not see – is one record (src/work-audit/reads.mjs: LIMIT_KEYS, limitRecord, limitDeclarationProblems), complete by construction, refused at construction and again at lane assembly. The human face renders every declared limit with its own text, attributed to the lane and the sweep it qualifies; the two faces carry the same limits; and a clean lane still states its limit, which is the case the limit exists for.

58 · Supervising loops

The loop graph has an ABOVE, and it is queryable

Before this milestone the registry was a flat set of loops with edges between peers; “who may change this loop’s target” had no answer a command could give. It now does, at every declared loop, through one edge kind (target-setting) whose admissible sources are frozen at three – a slower loop, an actor, or a frozen-rule anchor. The hierarchy ACD always had as documents is now an edge set the checks walk and the face renders. The seven day-one edges are m58/01/the seven ownership edges; the vocabulary that admits them is m58/00/every declared loop has a named owner.

Three of milestone 52’s six checks stopped describing and started refusing

reference-ownership, actuator-arbitration and timescale are the checks 52 shipped that had nothing to decide over: no ownership edges existed, no arbiter kind existed, and six of seven loops have no clock. All three now gate at error severity – m58/02/the structural codes stop the run – and the reason they can is that 58/01’s records gave them something true to be true about. A check that reports and a check that refuses are different artifacts, and this milestone is where three crossed over.

The ordering that made the gate safe to switch on

The promotion to error waited for the records: measured over this repository’s own registry, the sequence is 39 findings → 36 after 58/01 → 32 after 58/02, 0 errors throughout. Promoting first would have delivered fifteen error-severity findings through aof:validate’s hard loop lane for work that was merely unfinished. A gate that arrives red for unfinished work is a gate somebody switches off, and this milestone’s own ordering edge is the reason this one arrives green.

Supervision is computed from records, never self-declared

No node kind admits a key by which a node asserts its own supervision, layer authority or arbitration – no supervised-by, no arbitrated-by, no independence – and the checks module that decides all of it still imports nothing. Every supervision fact in this system is derived from what records declare about their own fields, which is what makes the graph auditable rather than aspirational.

A node kind that records a trade-off and cannot act on it

arbiter is the fifth kind, and the load-bearing part is an absence: it admits no actuator, no measurement, no cadence and no ground, so a node resolving a conflict between loops has no vocabulary in which to say it also pulls one of the actuators it arbitrates. The refusal costs no new code – the loader’s existing loop-key-not-admitted-for-kind fires – and no new finding code. Enforced end to end: m58/00/an arbiter has no vocabulary, cleared only on an arbiter by m58/02/only an arbiter clears a shared actuator, and visible as its own glyph by m58/03/every declared kind renders as its own shape.

The registry’s citations are now read by something

The registry’s entire value is that an authored edge declares itself authored and a discovered one cites the artifact it was read from. Until this milestone closed, nothing read those citations – 52/ADR-013 had routed prose-body line citations out of its census as not-black-box – and twelve of fifteen defining-line claims were wrong, by up to 355 lines, with two records inside one diff giving different lines for the same export. FF-5810 now asserts in-range and defining-line over src/bundle/loops/** on every build. This is a milestone-level fact rather than 58/01’s, because the gap it closes predates 58 and spans every record 52, 55 and 57 shipped.

Four kinds of failure this milestone had to survive, and did

Six architecture defects found at contract-authoring, five partition defects found by the developer sweep, one false acceptance criterion refused by QA, and two blocker findings at the verify gate. All ruled or repaired before accept, each minuted at the § it amends. The count is the outcome: a milestone that declares ten controls and seven ADRs generates defects at a rate its own review stages have to be able to absorb, and this one measured that they can.

58/00 · The supervision vocabulary

arbiter is a declarable node kind

NODE_KINDS is five frozen literals – loop, actor, anchor, watcher, arbiter – and ENDPOINT_SCHEMES admits arbiter: as an endpoint, so a record whose whole purpose is to resolve a conflict has a kind of its own rather than being spelled as a loop that happens not to run.

An arbiter must say which conflict it resolves, in what order, and for how long an adjustment stands

resolves, priority and dwell are required for the kind. resolves follows counter’s rule exactly – one branch, not two copies – admitting a non-empty phrase and refusing every SENTINEL_TOKENS member and every RESERVED_FIELD_PREFIXES entry, so resolves: prose:…, resolves: config:… and resolves: unknown are each loop-bad-value, while controlled keeps its distinct pointer-or-phrase rule. dwell admits exactly cycles:<n≥1> and none, and not unknown.

An arbiter has no vocabulary in which to say it also acts

ADMITTED_KEYS.arbiter omits actuator, measurement, cadence and ground. An arbiter that claims to pull one of the actuators it arbitrates over is refused by the loader’s existing loop-key-not-admitted-for-kind – no new code and no new finding code.

A loop declares the timescale it runs at, and only a loop may

layer: is an optional scalar on kind: loop alone, taking the frozen three-member enum operational / management / governance. It is optional by design: with no layer: declared anywhere, milestone 52’s timescale behaviour is byte-for-byte what it was.

The ordering the checks compare has one home, and it is the loader

src/work-loops.mjs owns the layer→rank and cadence-trigger→scope-rank maps and hands the ranks over already computed on the parsed fields. src/work-loops-checks.mjs spells no layer name and no trigger token, so nothing derives a duration from either.

The widening deleted nothing

All fourteen records shipped before this milestone parse with zero new findings, measured against the signature milestone 57 froze: the same codes in the same counts (12 loop-field-prose-only, 6 loop-owner-unknown, 18 in total, no error anywhere), and every record keeps the kind it had – seven loops, two actors, two anchors, three watchers.

A registry fixture copies a subset closed under its own endpoints

test/support/registry-fixture.mjs is the one helper that copies records out of src/bundle/loops/ into a temp registry, and it transitively adds every record a copied record’s endpoint names. No fixture it builds reports loop-graph-dangling-endpoint, and no test file reaches the shipped registry by any other route.

The six check ids have one authority

CHECK_IDS and src/work-doctor-loop-ready.mjs’s COMPOSED_CHECK_IDS are asserted identical in members and order, so a seventh check cannot be added to the checks module while doctor silently scores six. The loader is never asked what the checks are called.

58/01 · The reference hierarchy and the arbiter

Every declared loop has a named owner for its reference

All seven kind: loop records in src/bundle/loops/ carry an inbound target-setting edge, and loop-unowned-reference reports zero over the installed registry where it reported five on the day this story opened. Revising a loop’s target is now a cycle with a named owner rather than an edit somebody makes.

The seven ownership edges, and what each one is

actor:operator sets the references of loop:autonomous-cascade, loop:mesh-assignment-reclaim, loop:retrospective-memory-ingest and arbiter:speed-thoroughness-autonomy; actor:product-owner sets loop:verify-triage-accept’s; loop:autonomous-cascade sets those of loop:build-to-green and loop:review-fix-rereview – the slower loop above the faster one, which is the structural move the milestone exists to make; and anchor:run-lifecycle-policy sets loop:run-resilience’s.

An authored edge says in its own record that it was authored

Every record declaring an edge this milestone decided states so in its own body and offers no citation for the relation it decided, while separating out the discovered fact underneath it. Every record declaring an edge the repository already stated names the artifact it was read from. Which kind an edge is can be answered by reading one record, without opening another file.

The one reference no cycle revises is carried by an anchor

anchor:run-lifecycle-policy has ground: frozen-rule and observes: module:src/run-store.mjs#isLegalTransition, so the authority behind loop:run-resilience’s reference is a thing that resolves rather than a sentence written into a loop record to fill a gap. It is the first frozen-rule anchor the registry carries.

Every loop declares the timescale it runs at

Seven declared layers – one governance, two management, four operational – each corroborated by its cadence’s scope rank, with at most one uncorroborated and that one named and defended in its own record. This is what made 52’s timescale check able to decide anything: six of the seven loops have no clock, so before the layer axis the check had nothing to compare.

One arbiter owns the standing speed-versus-thoroughness-versus-autonomy trade-off

arbiter:speed-thoroughness-autonomy resolves the named conflict, vetoes all four contenders for the three shared agent actuators, declares a priority that is a permutation of exactly that veto set, and a dwell. Every config: endpoint of its parameter-tuning is cited as a ceiling: pointer by one of the loops it vetoes. loop-shared-actuator-unarbitrated reports zero where it reported three.

The records ship as bundle assets and land in a project

All 16 loop records are registered in src/bundle/manifest.json, and aof work update installs them: .aof/loops/ holds 16 records byte-identical to src/bundle/loops/, with 16 matching entries in .aof/aof.lock.json. A record that lives only in the source tree is in no project’s registry, and the gate 58/02 turns on reads the installed copy.

Every citation a shipped record writes resolves

59 in-repo path citations are in range, and every `<symbol>` at `<module>:<line>` claim names the line that symbol is actually exported on – asserted by FF-5810 over src/bundle/loops/** on both legs, each probed red. The registry’s value is that an authored edge is honest and a discovered one cites; a citation nothing checked was the half that had already rotted.

58/02 · Layer separation, arbitration and the gate

The timescale check decides on the layer where the clocks cannot

checkTimescale compares declared layer ranks whenever both ends of a target-setting edge carry one, and falls back to the clock only where they do not. A supervisor at no slower layer is loop-layer-inversion; one reaching more than a single boundary down is loop-layer-skipped. Over this repository’s registry the check now decides seven loops across three layers, where before this story it had reported nothing since the day it shipped.

The layer axis is additive over the cadence axis

With no layer: declared anywhere, the check’s output over the closed cadence cross-product, the ratio-3 boundary, the four single-node fixtures and the non-target-setting case is identical to what milestone 52 shipped. Both behaviours are asserted by one authority: FF-5802 extends 52’s own comparability guard rather than standing a sibling beside it.

Nothing derives a duration from a trigger or a layer

Where neither axis can answer – no clock at both ends, no layer at both ends – the finding is loop-timescale-not-comparable at warn, an honest absence rather than a fabricated conversion. The layer→rank and trigger→scope-rank maps exist only in src/work-loops.mjs; src/work-loops-checks.mjs contains no LAYER_VALUES or EVENT_TRIGGERS member and receives every rank already computed on the parsed fields.

The separation ratio is a frozen literal that no configuration can lower

MIN_SEPARATION_RATIO === 3, exported so a control can assert the number rather than grep for a digit, and resolved by no config key. A project cannot lower it to 1 and thereby declare a supervisor that runs as fast as what it supervises.

Only an arbiter clears a shared actuator

checkActuatorArbitration clears a contended actuator only on a non-contending node whose kind is arbiter. A vetoing actor, loop, anchor or watcher no longer clears it – the finding names the pretender and its kind. Every arbiter’s priority must be a permutation of its own veto endpoint set, with no duplicate, extra or omission, and no arbiter may declare target-setting: the kind that records the trade-off is the kind that cannot act on it.

An arbiter is a node the checks can see

isGraphNode accepts every member of NODE_KINDS, so a kind: arbiter record is a member of the graph all four structural traversals walk. This is what took loop-shared-actuator-unarbitrated from 3 to 0 over the shipped registry, and it carried the two grounding movements with it. A sixth kind added to the loader without being admitted here fails CI instead of being silently filtered out of every check.

A target-setting edge from an inadmissible source is reported, not accepted

Ownership resolves only from a slower loop, an actor, or an anchor whose ground: is frozen-rule. Anything else is loop-target-setting-not-admitted, and a loop with no inbound edge at all is loop-unowned-reference.

The structural codes stop the run

GATING_CODES is a frozen thirteen-member set – 57’s five plus this milestone’s eight – and severity is derived from membership in it rather than from any hardcoded constant. Nothing inherited moved: every grounding, anchor and watcher-census code and all seventeen loader codes retain the severity they had. The two codes that report a preference (loop-layer-skipped) or an honest inability to decide (loop-timescale-not-comparable) stay warn.

The exit decision lives only on the face

src/work-loops-checks.mjs returns findings and decides nothing about the process; the face reads the summary and chooses the exit. run() returns an identical result whether or not anyone is gating on it. src/work.mjs is not edited and no seventh doctor lane exists.

Supervision is computed, never self-declared

No kind admits a key by which a node asserts its own supervision, layer authority or arbitration – there is no supervised-by, arbitrated-by, dead-band or independence key in any admitted set, and owner is admitted on kind: loop alone. The ownership, layer and arbitration requirements are emitted as findings rather than required as keys. The checks module still imports nothing.

The gate turns on green

Measured over this repository’s installed .aof/loops/ at acceptance: 0 errors, 32 warnings, exit 0, with reference-ownership, actuator-arbitration and timescale each running and reporting nothing.

58/03 · The supervision face

Reading a loop names the layer it runs at and the node that sets its reference

aof work loops show --id <loop> renders the declared layer and the node holding the inbound target-setting edge on the same line as the node itself – measured live over this repository’s installed registry: loop:build-to-green · loop · Build executable work to green · layer operational · reference set by loop:autonomous-cascade.

An absence is stated in words where it is a defect, and nowhere else

A loop nobody supervises and a loop that declares no layer each say so; a kind that has no layer axis renders no layer at all and is named only when something sets its reference. The face does not print an empty field to keep its columns even.

owner: and the reference-setter stay two different facts

The face never conflates who is accountable for a loop with who sets its target, a loop pointing a target-setting edge at itself is not reported as its own supervisor, and a setter declared by a kind that may not set one is still named rather than quietly dropped – the face reports what the registry declares, including where the registry is wrong.

Two setters are both named, in id order, and the two faces agree

Where more than one node sets a reference the face lists every one of them deterministically; the machine face is a shape a caller can depend on; the human and machine faces carry the same answer node by node; and reading one node agrees with reading the whole registry.

Every declared kind renders as its own shape

renderLoopGraph draws five distinct glyphs for five declared kinds – ["…"] for a loop, (["…"]) for an actor, (("…")) for an anchor, {{"…"}} for a watcher, {"…"} for an arbiter – where before this story an anchor, a watcher and an arbiter were the same picture as each other and the same picture as a dangling reference. The glyph table lives inside the exported pure renderer, so the glyph set can be asserted without standing up a workspace.

The fallback for an endpoint nobody declared is untouched

[/"…"/] still renders command:, config:, module: and dangling loop: endpoints, and it is also what a record whose kind: the vocabulary does not admit falls to – so an unadmitted kind borrows no declared kind’s glyph. No declared kind’s shape equals it.

The sixth kind fails CI until somebody gives it a shape

FF-5808 asserts glyph-to-kind parity in both directions, and its second leg is the one that makes the guard real: a sixth kind handed the fallback shape would still yield six distinct shapes across six kinds, so cardinality alone is satisfied by the exact collision this control repairs. The two legs together mean the next kind cannot be added without a glyph of its own.

The picture is deterministic

The same registry renders byte-identically twice over, in a second process, and whatever order the records were discovered in. Node keys and edge lines are unchanged by the new shapes.

57 · Paired loops

Every optimizing loop aof runs is watched, and an unwatched one stops the run

aof work loops validate over .aof/loops/ reports 14 records including 3 watchers, 0 errors, 39 warnings and exit 0, with zero loop-unpaired-optimizer – where the same command over the same registry reported three before this milestone.

The pairing is a fact about aof’s own tree, not a rule aof ships to other repositories

The three watcher records are both bundle members and installed records: .aof/loops/ carries them byte-identically to their src/bundle/loops/ sources, so the framework satisfies the rule it enforces, and FF-5313 fails if a shipped loop record is ever left uninstalled.

A counter-metric that reports nothing reports that it measured nothing

Both counter-metric commands refuse a number rather than inventing one – work:counters returns status: "unmeasurable" with no count key at all, and work:ratchet returns ratchet-base-unresolved with no legs computed and exit 1. Two stories arrived at the same refusal independently, so a watcher that has never observed anything cannot satisfy the pairing gate with a zero.

Day one carries no judges

All three shipped watchers declare determinism: counter, and loop-watcher-is-judge – a permanent, non-gating census – reports zero on the shipped table.

Independence cannot be declared by anything, including the framework itself

No node kind admits an independence key and no loop may name its own watcher, in the shipped grammar and in every record installed: the property is computed from parsed records on four legs, each with its own finding code. See m57/01 for the legs.

Every structural invariant this milestone declared is enforced and has been seen to fail

Eight declared controls, all landed, none carrying pending, each red-probed – 49 probes, each restored byte-exactly with its control re-run green on the restored bytes. Two of the eight extend guards already in service rather than adding siblings.

The framework’s own build loop has a counter-metric that a machine produces

command:work:ratchet resolves to a registered route, and each watcher’s measurement pointer resolves to a registered command or an exported symbol – a declared counter that pointed at nothing would be FF-5707-red rather than merely unhelpful.

57/00 · The watcher node

kind: watcher node class

The loop registry admits a fourth node kind beside loop, actor and anchor, requiring id, kind, title, counter, determinism and measurement, and carrying the same five edge keys as every other node.

A watcher has no vocabulary for acting

ADMITTED_KEYS.watcher omits actuator, and with it controlled, ceiling and optimizing; a watcher declaring any of them is refused by the loader’s existing loop-key-not-admitted-for-kind – no new check, no new finding code, no special case.

determinism: – two literals, and no default

DETERMINISM_VALUES is counter and judge, a frozen set in src/work-loops.mjs; any other value is loop-bad-value and an absent one is loop-missing-field that leaves the key off the parsed node, so a record that does not say how its number is produced is incomplete rather than assumed.

counter: is a phrase, and only a phrase

A watcher’s counter: admits free prose alone – the three pointer schemes, prose:, the three sentinel tokens and the empty string are each loop-bad-value – so the quantity being counted is named in words while the instrument that produces it stays in measurement:.

The pairing is declared by the watcher, outbound, on the edge that already existed

EDGE_KEYS is still 52’s five literals: the pairing is a monitoring edge on the watcher naming the loop it watches. A loop carrying a watcher: key is refused as loop-unknown-key, and loading a watcher’s edge synthesises no reverse field or edge on the watched loop.

The eleven records shipped before the widening parse unchanged

Every record milestones 52 and 55 delivered loads with a finding set identical file-for-file, code-for-code and key-for-key to its pre-widening signature – 11 nodes, 18 findings, zero new.

FF-5701, armed

test/arch/acd-watcher-taxonomy-additive.test.mjs fails when NODE_KINDS drops a prior kind, when a record milestone 52 or 55 shipped stops parsing, when ADMITTED_KEYS.watcher gains actuator, when DETERMINISM_VALUES gains a third literal, and when determinism stops being required for the kind – each leg observed red and restored.

57/01 · Independence computed, and the gate

An optimizing loop with no watcher fails the run

loop-unpaired-optimizer is error, and aof work loops validate exits non-zero when any error-severity finding is present. The check that has existed since milestone 52 now stops a run instead of adding a line to it.

Watcher independence is a computed property of the records

Four independence legs read the parsed model alone and each carries its own code: loop-watcher-shares-measurement when a watcher’s measurement pointers intersect the loop’s, loop-watcher-shares-actuator when a judging watcher’s prose authority is also the watched loop’s actuator, loop-counter-equals-controlled when a counter restates the loop’s controlled variable after normalization, and loop-counter-not-deterministic when a record declaring determinism: counter cites anything a machine cannot run. No node kind admits an independence key and no loop may name its own watcher, so independence cannot be declared – only computed.

Every watcher that leans on a model is visible, and never gates

loop-watcher-is-judge reports every watcher declaring determinism: judge, independent or not, at warn permanently. It is a census, not a gate: whether a metric admits a deterministic computation is not decidable from a record.

Severity is a property of the code, and the exit lives only on the face

GATING_CODES is a frozen five-member set and finding() derives severity from membership in it rather than from a hardcoded constant. src/work-loops-checks.mjs returns findings and decides no exit code; loopsValidateCommand.cli.exit is the sole exit decision, and reading it leaves the run result byte-identical. The pure check module still imports nothing.

Nothing inherited turned red

The five promoted codes are the only check codes whose severity moved. Every other inherited check code remains warn and every loader code retains the warn/error severity it already had. Measured on this repository at the gate: 0 errors and 39 warnings across nine inherited codes.

aof:validate runs the loop registry as its own deterministic step

The validate procedure runs aof work validate, then aof work loops validate, then aof work doctor, in that fixed order. The loop gate is a separate workspace-wide step – not folded into work validate, not a doctor lane – and its non-zero exit is surfaced to the operator. All three installed runtime faces (.claude, .opencode, .codex) carry it.

57/02 · Examples rows in the parser

Every parsed scenario carries its Examples blocks

parseFeature returns a sixth key on every scenario – examples, an ordered list of { header, rows, line }, one entry per Examples:/Scenarios: block in the order the file declares them.

The row count is data rows, never the column header

A block’s rows is the number of pipe-delimited lines beneath its caption excluding the first, so an Outline is reported with exactly as many cases as a runner would execute – 1,217 blocks and 5,333 data rows across this tree, where the count was previously 0.

A block’s boundaries are the ones a reader sees

Blank lines and #-commented rows inside a table contribute nothing and do not close the block; a step, a tag, a docstring, or the next structural keyword does close it; and pipe-delimited lines inside a docstring are data, not a table.

An absent table is an empty list, never an absence

Every scenario carries an array – an Outline with no Examples: block and a plain Scenario both report [] – so a consumer counts a zero rather than branching on a missing key.

The five keys the parser already reported are byte-identical

Over all 803 feature files in the tree, name, outline, lane, verification and line, the tag resolution behind them, and the firstViolation/freeTextLines litmus verdict are deep-equal to the pre-story parser’s output, with examples the only difference; key order is name, outline, lane, verification, line, examples.

The parser’s three production consumers are unchanged

src/work.mjs, src/commands/tasks.mjs and src/work-doctor-rubric.mjs are unedited by this milestone and name the new key nowhere; executableScenariosOf still defines an executable scenario without reference to rows.

FF-5704, armed

test/arch/acd-feature-parse-examples-additive.test.mjs fails when the widening moves an existing scenario key, when it moves the litmus verdict without touching a key, when examples stops being an array on any scenario, and when any of the three consumers reads the new key – each leg observed red and restored.

57/03 · The contract-integrity ratchet

aof work ratchet <ref> reports contract movement as four legs

A registered CLI command returns contract, closed-set, marker and compensating-assertion, each carrying fired / discharged / unclassified / clear and the findings behind it; --json and the rendering carry no precision claim of any kind.

The baseline is the item’s own record, and an unresolvable one produces nothing

The base commit is the first --first-parent commit whose version of the item’s record document carries status: in-progress; a shallow clone, an unborn ref or a record never committed in-progress yields ratchet-base-unresolved, exit 1, no legs computed and no comparison against HEAD~1. --base <commit> overrides it and is recorded in the output as supplied.

An acceptance criterion is a scenario or an Examples row, counted item-wide

Leg (a) compares @executable scenarios plus Examples rows across all of the item’s task features as one total, so deleting a row shrinks the contract and moving a scenario between task files does not.

A closed-set assertion may not be relaxed on a file that existed at the base

Leg (b) fires on closed-to-open only, over files present at the base commit; a closed-set assertion added to the same file in the same change clears it as leg (d); anything the frozen vocabulary cannot classify is reported ratchet-unclassified and counted as neither fired nor clear.

A skip, only or todo added to a pre-existing test is a fire

Leg (c) fires only when the marker is added and only when the test existed at the base commit; a marker inside a comment is not a marker, a removed marker is not a fire, and a newly authored todo is not a retreat.

Discharge requires an authority the optimizer could not have written

A fired leg clears only against an ADR that (i) is cited by the artifact as it stood at the base commit, (ii) names the owning item – 57/ADR-007, m57/ admitted, a bare ADR-007 never – and (iii) resolves as a heading in that item’s ARCHITECTURE.md at the base commit. The owning ref is built from the numbered path segments between the work dir and the register-bearing ancestor, so a milestone register is 57 and a story-level one is 57/03.

The justification comment is structurally outside the inputs

Citations are harvested from the artifact’s base text, so a comment written with a weakening does not exist in the text the ratchet reads. The prohibition holds by construction rather than by a rule the code is trusted to follow.

The engine is pure and every repository read is at the command boundary

src/work-ratchet.mjs imports exactly ./feature-parse.mjs and ./work-doctor-rubric.mjs and reaches no filesystem, git, process or clock; the diff, both trees’ feature texts, the owning item’s register at the base commit and its ref all arrive as parameters assembled by src/commands/ratchet.mjs.

FF-5705, armed across three legs

test/arch/acd-ratchet-pure-and-discharge-scoped.test.mjs fails when the engine gains a repository read, when the boundary stops handing it a completed observation, when the no-base refusal stops exiting non-zero, when citations are harvested from head instead of base, when a bare ADR id is admitted, and when an unresolved owner stops blocking discharge – each leg observed red and restored.

57/04 · Escape and intervention counters

aof work counters <ref> reports both counter-metrics for one item and its direct children

A registered CLI command returns { ref, escape, intervention } for a milestone (itself plus its stories) or for a single story, rendered as two lines and available verbatim under --json.

A finding raised after an item was accepted is an escape, attributed to the item it escaped from

The escape counter compares each kind: "raw" feedback entry’s at against the acceptance moment of items reading status: done, reports a per-item count with the escaping entries’ ids, and totals them across the scope it was given; classification records are not in the denominator.

Acceptance is read at day granularity and resolved conservatively

A date-only stamp is treated as T23:59:59.999Z, so a finding raised on the same day as acceptance is not an escape; full ISO instants keep their exact ordering.

An intervention is a retry, a resume, an exhausted attempt ceiling or a non-retryable failure

The intervention counter reads run records already on disk and classifies per run: a record carrying retryOf is one intervention – resume when the run it retries carries resumeAfter, otherwise retry – and a terminal failure that was never retried is attempts-exhausted (at or above the configured ceiling, or attempts_exhausted by name) or non-retryable-failure.

The count never exceeds the run total it is reported against

A retried run contributes exactly one intervention regardless of how its own attempt later terminates, so count, runs and rate are a coherent triple and rate is always in [0, 1].

A counter with no evidence reports that it cannot measure, and carries no count key at all

status: "unmeasurable" with a reason of accepted-items-absent, feedback-absent or runs-absent is returned instead of a number, and the result object has no count property – so a consumer cannot read a zero that was never measured. A genuine zero returns status: "measured" with count: 0, and the two are distinguishable without inspecting the number.

A partly measurable scope reports what it measured and names what it could not

measuredItems, unmeasuredItems and missing accompany every measured result, and the rendered line appends ; N item(s) unmeasured.

The arithmetic is a pure leaf and every record read is at the command boundary

src/work-counters.mjs imports nothing and reaches no filesystem, process or clock; src/commands/counters.mjs holds the only reads (readFeedbackRecords, readRuns, the record’s frontmatter) and no write API of any kind. Both properties are enforced by test/arch/acd-work-counters-read-only.test.mjs.

Nothing new is written to disk by either counter

No instrumentation was added, no record gained a field, and both counters are readers over records milestone 20 and the feedback path already produce.

57/05 · The pairing table

Every optimizing loop aof ships is watched

src/bundle/loops/ carries three kind: watcher records – watcher:build-to-green-watcher, watcher:review-fix-rereview-watcher and watcher:autonomous-cascade-watcher – one monitoring edge each at loop:build-to-green, loop:review-fix-rereview and loop:autonomous-cascade, the only three loops in the registry declaring optimizing: true.

The watchers install by the ordinary update path, and are framework-owned

The three are bundle.json asset members targeting .aof/loops/, rendered verbatim for both the claude and codex runtimes; each carries the # aof-generated: true marker, so aof work update classifies an operator-edited copy drift-warning and preserves it rather than overwriting it.

Every watcher’s number is produced by a machine, and the pointer resolves

All three declare determinism: counter with a single command: measurement pointer – command:work:ratchet on the build watcher, command:work:counters on the other two – each naming a route registered in the command registry. No watcher declares prose: measurement, and none is a judge.

The independence legs hold on the records rather than being asserted about them

Each watcher measures a different artifact from the loop it watches, counts a different quantity from that loop’s controlled:, and declares no actuator – the kind admits no such key. The shipped registry loads with zero error-severity findings while the inherited loop-owner-unknown and loop-field-prose-only warnings remain warnings.

aof’s own registry satisfies the rule aof ships

.aof/loops/ holds 14 records including the three watchers, byte-identical to their bundle sources. loadLoops over it reports 14 nodes, 3 watchers and zero error-severity findings.

A shipped loop record that is not installed is now a red control

test/arch/acd-registry-framework-owned.test.mjs (FF-5313) asserts every .aof/loops/ bundle member is present on disk and byte-identical to its src/bundle/loops/ source – the byte comparison the .aof/loops/*.md text eol=lf pin in .gitattributes was already written for.

55 · Anchors & the frozen set

aof’s own loop graph carries a measured groundedness verdict

aof work loops groundedness answers, for this repository, that 11 components stand at 5 anchored / 2 exogenous-only / 4 self-referential, with loop:autonomous-cascade, loop:retrospective-memory-ingest, loop:review-fix-rereview and loop:verify-triage-accept named as anchored by nothing – the taxonomy (m55/00) and the report (m55/01) joined into a reading of the real graph rather than of a fixture.

“Ungrounded” is a computed property of the declared graph, not a judgement about it

A component’s verdict is derived by flooding from every node bearing an admitted ground: value and decomposing the graph 52 declared, so the answer changes only when the registry changes – no reviewer’s reading enters it, and the same registry and checkout always produce the same verdict.

L3 executes, and the gate that opens it is computed from this milestone’s own measurements

Milestone 53’s locked rung is unlocked (m55/05) and admitted only on a Loop-Ready score of 100 together with a groundedness report carrying no self-referential and no stale component – so the rung the ladder declares and the ground the loops settle against are one mechanism, not two.

This workspace has not earned L3, and the refusal says which half and why

aof work doctor reports 50% (5/10) with grounding, anchor-grounding, pairing, reference-ownership and actuator-arbitration blocking, and four self-referential components stand in the report – so unattended self-driving is open in the ladder and closed here, by measurement.

Every claim aof records is stamped or refused, and every human input is captured verbatim first

The two integrity rules are structural rather than emphasis: a claim without {node, run, commit, at} is refused at the write seam and never back-filled (m55/02), and a capture path that accepts a classification argument does not exist (m55/03) – each enforced by a control, not by prose.

The frozen set names its own anchors, so the milestone’s two halves close on each other

src/bundle/frozen-set.jsonc’s six members include anchors alongside locked-contract, litmus, tag-vocabulary, gate-order and test-isolation (m55/04), so the taxonomy this milestone declares is itself a frozen member compiled to the enforcement boundary aof owns.

Eight architectural controls, every one watched to fail in the form it shipped

FF-5501…FF-5508 are landed, registered in the runner’s own labelled milestone-55 blocks, green, and each carries a red probe performed against a real source edit restored byte-exactly afterwards – the two that act on a guard already in service (FF-5503 extends, FF-5508 supersedes) probed on the extension and the replacement rather than on the surviving legs.

55/00 · The anchor taxonomy

kind: anchor node class

The loop registry admits a third node kind beside loop and actor, requiring id, kind, title, ground and observes, and carrying the same five edge keys as every other node.

The six-member ground: taxonomy

GROUND_VALUES is process-exit, build-stamp, landed-commit, live-soak, frozen-rule, exogenous – a frozen set in src/work-loops.mjs, admitted on kind: actor and kind: anchor and on no kind: loop node at any value.

observes: – the registry’s one no-prose field

An anchor’s authority parses through the pointer grammar alone: a module:, command: or config: pointer is admitted, and prose:, unknown and free text are each a loop-bad-value refusal rather than the declared gap they remain on every other key.

The nine milestone-52 records still parse byte-unchanged

Every record the previous milestone delivered loads with zero error-severity findings after the widening, and actor:operator’s ground: exogenous is now a member of the taxonomy rather than its only value.

Two framework anchors, delivered through the bundle

anchor:rubric-process-exit (ground: process-exit, observing src/commands/grade.mjs#reportObservation) and anchor:run-liveness (ground: live-soak, observing src/run-store.mjs#isStale) ship as bundle assets for both runtimes, install byte-identical to source into .aof/loops/, and declare three data-feed edges between them – each edge cited in the record’s own body.

FF-5501, armed

test/arch/acd-anchor-taxonomy-additive.test.mjs fails when the enums drift from their literals, when a set stops being frozen, when ground becomes admissible on a loop, when observes accepts prose, and when any milestone-52 record stops parsing.

55/01 · The groundedness report

Grounding is seeded by ground:, on any host permitted to carry one

analyseGrounding floods from every node bearing an admitted ground: value – kind: anchor and kind: actor alike – rather than from the single actor + exogenous predicate milestone 52 shipped, and every grounded component carries the groundClasses that supported it.

A fourth verdict, stale, distinct from both its neighbours

A component’s verdict is one of anchored, exogenous-only, self-referential or stale, and an anchor that resolved once and does not now reports stale – a different token from a component that never had an anchor, distinguishable without reading any message text.

A loop nothing anchors is named, not merely absent from a list

Every kind: loop node with no inbound data-feed edge from a kind: anchor node appears in unanchoredLoops and raises a loop-anchor-absent finding naming it; the count is reported beside the names. In this repository that is four – loop:autonomous-cascade, loop:retrospective-memory-ingest, loop:review-fix-rereview, loop:verify-triage-accept – against 11 components at 5 anchored / 2 exogenous-only / 4 self-referential.

work:loops-groundedness – a registered command with a frozen --json document

The report is reachable as aof work loops groundedness --json and in-process through invokeRegistered("work:loops-groundedness"), returning {source, present, state, components, unanchoredLoops, authorities, findings, summary}, byte-identical across runs on an unchanged registry and repository. It has no board route: loops-groundedness sits in BOARD_DEFERRED beside loops-show / loops-graph / loops-validate, so the loop family opens no /api/work door.

The three pointer schemes are resolved, not merely parsed

resolveAnchorAuthorities answers {pointer, resolved} per anchor: a module: pointer resolves when the named symbol is genuinely exported from a file inside the owning root, a command: pointer when the registry registers that id, a config: pointer when the workspace config declares that dotted key – and each returns false rather than throwing when it does not.

The checks stayed pure, and the guard that says so was widened to cover it

src/work-loops-checks.mjs imports nothing and performs no filesystem, process or clock read; resolution arrives as a map argument built at the command boundary. 52’s purity guard was extended to cover the resolution map rather than joined by a sibling.

The verdict is returned, never written

Producing the report writes nothing to disk – no verdict, no resolution result and no staleness record is persisted by this story.

FF-5502 and FF-5503, armed

test/arch/acd-anchor-grounding-seed.test.mjs fails when the seed narrows back to a kind predicate, when the SCC decomposition body moves from its pinned digest, when the verdict set drifts from its four literals, or when a sixth edge key appears; the extended test/arch/acd-loop-checks-pure.test.mjs fails when any I/O re-enters the checks.

55/02 · Provenance at write time

The claim provenance envelope

Every recorded claim carries a four-key stamp – node, run, commit, at – compiled by src/claim-provenance.mjs, a leaf module that imports nothing and reads no clock, filesystem, process, git checkout, transcript, log, mtime or directory listing.

Refusal in place of back-fill, at the durable write seams

mintRun and recordAnchorReading validate the stamp before the store is read or written, so a claim offered without a complete one is refused with claim-provenance-missing naming what was absent, and no writer completes a partial stamp from anything it could have inferred.

Anchor readings that ride the run record

recordAnchorReading appends a reading to the run’s brief under the work item’s own runs/, so readings accumulate rather than overwrite, no sidecar store exists, and nothing writes a reading into the bundle-delivered .aof/loops registry.

One impure gathering edge

gatherClaimProvenance at the grade command is where the facts are collected; the compiler is handed them, which is why the same observation always compiles to the same record.

55/03 · Raw capture before classification

A verbatim feedback ledger, written before anything classifies

Each capture appends the raw {kind, id, text, actor, refs, at} record to the item’s FEEDBACK.ndjson before the human-readable STATE.md bullet is projected, and the ledger’s module holds no writeFile, rename or truncate – a raw record cannot be opened for rewrite because nothing that owns it can rewrite.

A capture path with no menu, at both doors

work:feedback accepts exactly ref, note, actor and refs in its command schema and in its CLI flags, the bundled /aof:feedback command offers no AskUserQuestion, and an unknown field is refused with feedback-classification-deferred before the item is resolved or anything is written.

Classification as a strictly later, separate record

A triage is appended as its own record referencing the raw one by id, so re-triage leaves the verbatim text byte-identical, an untriaged capture is complete on its own, and a classification cannot exist without a raw record to point at.

One production raw writer

Exactly two modules reach appendRawFeedback – the capture transition and the ledger itself – so every capture path in the system, CLI or board, lands the same verbatim record first.

55/04 · The frozen set, compiled

The frozen set as a versioned declaration

src/bundle/frozen-set.jsonc declares six members – locked-contract, litmus, tag-vocabulary, gate-order, test-isolation and anchors – each naming the enforcement point it protects, and it installs into a consumer as .aof/frozen-set.jsonc.

A compiler from declaration to enforcement point

compileFrozenSet emits hook entries, permission denials and agent tool scopes that each carry their declaring member’s id, so no aof-authored rule exists at any enforcement point without a declaration behind it; a member that cannot reach its enforcement point refuses the compile atomically rather than partially applying.

A surgical permissions merge

.claude/settings.json is merged array-wise through splicePermissions, with permissions removed from the patch before the top-level spread, so every operator entry survives a compile with its value and its position, and a compile that changes nothing writes nothing.

Tampering as a coded event, with the human’s way out intact

Drift on a frozen member emits frozen-set-tamper carrying the member id – distinct from the pre-existing preference-drift warning – while an entry whose ownership marker has been removed is neither edited, retracted, re-marked, nor reported as a tamper, and the report states that escape where the person will see it.

The test-isolation guard as compiled output

The rule that blocks an unisolated test run is now the compiled output of the test-isolation member, delivered as .claude/hooks/aof/guard-test-isolation.mjs, and it distinguishes an actual test invocation from a read or from quoted prose.

55/05 · L3 unlocked

An L3 rung that executes

LOOP_LEVELS is ["L1","L2","L3"] and LOCKED_LOOP_LEVELS is empty; milestone 53’s L3 lock is removed rather than narrowed, and the arch-test proxy that asserted no module could branch on L3 is deleted rather than left passing on a premise that is now false.

Admission computed from earned facts alone

resolveLoopLevelGate admits L3 only on a Loop-Ready score equal to L3_SCORE_THRESHOLD = 100 together with a groundedness report carrying no self-referential and no stale component. No config key, environment variable or flag anywhere in src/** admits it.

Both halves gathered through the command boundary

resolveInvocation invokes work:doctor and work:loops-groundedness through the registry before the gate is consulted, so the loop reads the registry the way every other consumer does, and a refused request spawns nothing and records nothing.

A refusal that names the failing half and its parts

A refused L3 request carries loop-level-gate with failingHalves, the score against its threshold with every blocking check named, and the groundedness half’s components verbatim – machine-readable, and a refusal rather than a crash.

The anchor check inside the readiness score

COMPOSED_CHECK_IDS carries anchor-grounding beside milestone 52’s five, composed from the same report rather than re-derived, and an unreadable registry yields not-applicable – which does not admit L3 – rather than a pass.

54 · Verification as a feedback loop

aof grades a work item, and the grade is a record rather than an exit code

aof work grade <ref> --run compiles a GradeRecord – verdict, codes, per-case counts, the runner’s verbatim failures – from a report a declared runner emitted. The whole path refuses to mint a pass from an absence: nothing yields pass without a parsed report, and the evidence floor is measured against the cases that actually RAN. (The vocabularies, verdict rules and evidence floor are stated whole by m54/00; the declared-argv rubric, its single bounded spawn and its read-only bare face by m54/01.)

The loop decides on deterministic evidence before it spends a model turn

This is the milestone-level fact none of the stories states alone. Before 54, a loop’s only judge of “is this item correct” was a model review turn. There are now three deterministic rungs ahead of it – work:validate, work:doctor, work:grade – in declared cost order, and the loop’s next act is derived from their answers. A story whose suite is red re-drives without a review turn being spent on it. (The ladder and its admitted scope/severity: m54/02. The verdict-derived routing: m54/03.)

A red gate hands the maker what went wrong, instead of the fact that something did

The grade’s record rides the run it re-drove and reaches the next session through 70/04’s existing fix transport, and a loop that exhausts its cap halts carrying the accumulated record of what it could not close rather than an empty list. (Stated whole by m54/03.)

An unconfigured repository is unchanged, byte for byte

Every capability above is inert where no work.rubric is declared: the LoopState, every report line, every driver input, the driver’s spawn argv and every run record are indistinguishable from the pre-54 shell. rubric-unconfigured is a named indeterminate that proceeds exactly as before, and no code path maps it to a pass or to a halt. This is what makes the milestone adoptable rather than a migration.

aof declares what it does not know, and refuses to guess it

The rubric is the project’s declaration – an argv, a report format, an evidence floor – and aof runs precisely what was declared. It never infers a runner, never derives a case identity or status from a runner’s free text, and reports unjoined rather than guessing a pairing. (The emitted-not-inferred rule and the @executable→case join: m54/04.)

Ten fitness functions, each observed failing

FF-5401 … FF-5410 are declared, resolve to files on disk, and every one has a red probe recorded in VERIFICATION.md against a planted defect. aof work doctor 54 reports zero control-unresolved and zero missing red probes at any severity.

54/00 · The grade record – green is positive evidence

The grade record

src/work-grade.mjs compiles a GradeRecord – {ref, verdict, codes, runner, report, cases, failures, gradedAt} – whose key set is exact on every branch and whose JSON round-trips.

A closed verdict triple

GRADE_VERDICTS is a frozen exported ["pass", "fail", "indeterminate"], and no fourth verdict is reachable.

A frozen nine-code vocabulary with a producer for every member

GRADE_CODES is frozen, set-equal to ADR-005 §3’s nine, reported in its own declared order rather than the order observed, and every one of the nine is reachable through the real compiler from a committed fixture.

pass costs four pieces of positive evidence

A pass is reachable only when the declared report exists, parses in its declared format, enumerates named cases at or above the evidence floor, and every case carries a status; the exit status is read first and then discarded as insufficient – it can veto a pass and can never buy one.

The evidence floor measures cases that RAN

casesThatRan (total - skipped) is the single expression both the declared floor and the ratchet’s bar are measured on, so a report of nothing but skips is report-vacuous at indeterminate at every floor and with any history, while cases still reports the observed counts.

A ratchet scoped to the item, needing no configuration

The floor is raised by the highest cases-that-ran of the last recorded pass for the same ref, filtered by ref inside the compiler; only a pass raises it, and another item’s history moves it neither up nor down.

TAP normalisation proven against captured output

Both TAP dialects this repo produces normalise to the cases their producer actually emitted – ordinals and plan lines optional, # section headers and summary lines not cases, suites distinguished from tests by the producer’s own type field, failure messages carried verbatim – and each supported format is backed by a committed capture carrying a provenance line, never by a hand-written specimen.

Two armed fitness functions

acd-grade-green-needs-evidence (FF-5402) and acd-grade-record-envelope (FF-5403) are registered in the runner and each has been observed failing against a planted defect.

54/01 · The declared rubric

The rubric is a declaration in its own home

work.rubric is a config subtree of its own – command (an argv array), optional args.ref, env, and report {format, path, floor} – read by declaredRubric and by nothing else. work.controls.runners answers its unchanged question over its unchanged readers; neither key is derived from the other, and removing either leaves the other’s answer identical.

The declared argv runs element for element, with no shell anywhere

rubricSpawnOptions passes shell: false, so the array reaches the child unsplit, unjoined and unre-quoted, and an argument carrying shell metacharacters is one argument with its characters verbatim – no redirection, no pipeline, no second command.

A present-but-unusable declaration halts, and is never mistaken for an absent one

A work.rubric whose command is a string, an empty array, a non-program, or absent yields indeterminate / runner-spawn-failed and launches nothing – never rubric-unconfigured, the one indeterminate that proceeds. The two are distinguishable by code alone, without reading a message, and no tenth code was coined for either.

An unconfigured repo is an honest no-op at every door

No work.rubric yields indeterminate / rubric-unconfigured naming work.rubric as the key to set, with runner and report null and the observed case counts zero rather than absent. It is never read as pass and never as fail, through the read face, through --run, for a story or a milestone, and on a repeat invocation.

Scope is the project’s choice, and a whole-suite run says so

An args.ref is appended to the argv exactly once, followed by the item’s own ref. Absent, the plan carries no scope argument and reports whole: true beside scope: null – a reader can tell the whole-suite run was the declaration’s choice rather than aof’s.

aof work grade <ref> – a read face that executes nothing

The bare verb reports the planned argv, its cwd, the report format, the absolute report path, the floor and the deadline, plus the last recorded grade, and spawns nothing; the declared report path is neither created, read nor deleted by planning. --run is the only door to execution. A never-recorded grade reads as no grade recorded yet and is never reported as pass.

The command is registered once, CLI-reachable, and served nowhere

work:grade appears exactly once in the registry with a cli adapter and no launcher seam, is reachable as aof work grade, and is a documented BOARD_DEFERRED member with its reason recorded beside it – no /api/work/grade route exists, so a board page load launches no process. board-ui, ui/ and src/bundle/commands/** are unedited.

One bounded, fully-captured spawn per --run

stdio: ["ignore", "pipe", "pipe"] – a runner that reads stdin gets EOF rather than hanging, and both streams are captured, so a runner whose failures go to stderr (this repo’s own shape) cannot read as an all-green report. The exit status is read before the report is: a non-zero exit is never overridden by a green report. launched is 1 for a run that passes, fails or times out, and no retry is attempted inside a single grade.

The deadline force-kills, and is resolved rather than chosen

timeout + killSignal: "SIGKILL" bound the child; a run that reaches it yields indeterminate / runner-timeout reporting the deadline that bound it and the duration observed, with nothing from that run left alive. The value comes from 69/ADR-001’s single home src/loop-bounds.mjs via work.loop.startToCloseMs – 30 minutes by default, the declared value when one is set, the default again when the declaration is not a number. No second home for the bound exists in src/**, and work.dispatch.concurrency and work.autonomous.maxAttempts resolve exactly where they did.

Re-entrancy is refused structurally

Every spawn sets AOF_GRADE_RUNNING in the child’s environment – the only variable aof contributes that the project did not declare – and a --run that finds it already set refuses with runner-spawn-failed before any other check, launching nothing. The invoking process’s own environment is unchanged, so a later --run from it is not refused. A rubric that invokes aof cannot fork a grader tree however deep it is attempted.

The child inherits its environment, with the declaration overlaid

The ambient environment reaches the runner; the declared env overrides same-named ambient variables; everything else stays visible. What is forbidden is aof inventing a variable the project did not ask for, not the child having a PATH.

Five armed fitness functions

acd-grade-never-imports-the-suite (FF-5401), acd-grade-unconfigured-is-additive (FF-5404), acd-grade-read-face-never-executes (FF-5405), acd-grade-bounded-single-spawn (FF-5406) and the extension of acd-controls-never-execute (FF-5407) are registered and each has been observed failing against planted defects – eleven across the five, with FF-5406 re-probed at accept on the accepted bytes.

54/02 · Fitness in the gate

A declared five-row cost ladder

GATE_ORDER is a frozen five-row declaration – drive continue, gate work:validate, gate work:doctor, gate work:grade, drive verify – in strictly increasing cost, and every gate row names a command the registry resolves.

The fitness half of the gate, wired

invokeGateLadder invokes work:doctor after work:validate, so an item whose declared controls do not resolve is stopped by a deterministic check before any model turn is spent.

Each rung short-circuits every rung after it

A work:validate that returns findings returns from the ladder without invoking the doctor, and the ladder’s answer is its own envelope – { gate, findings }, naming the rung that answered – never one rung’s result wearing another rung’s findings.

The doctor rung runs at the driven item’s own scope

It is invoked as { scope: ref }, exactly as work:validate already was, so an un-authored register anywhere else in wiki/work cannot stop a loop that is not driving it.

An admitted code set derived, never restated

DOCTOR_GATE_CODES is Object.freeze(CONTROL_FINDING_CODES.filter(…)) over 66’s frozen array minus its two verification-* members and control-runner-unchecked; no admitted code appears as a literal, so neither verification code can enter the gate at any severity and a ninth control code cannot silently join it.

Severity is taken, never re-derived

The gate admits severity === "error" and nothing else; the loop shell neither calls nor re-implements severityFor, does not import the acceptance horizon, and reads loopReady nowhere in the ladder.

Two armed fitness functions

acd-doctor-gate-scope-and-severity (FF-5410) is registered and has been observed failing against a planted defect, and FF-5409’s GATE_ORDER clause on acd-loop-probe-contract has been observed failing against two.

54/03 · Feedback rides the re-drive

The verdict decides the act

gradeRoute() is the single derivation from a grade to a loop act – pass proceeds, fail re-drives, indeterminate halts, and rubric-unconfigured is the one named exception that proceeds as today. The routing reads the record’s verdict; a fail carrying no failures re-drives and an indeterminate carrying no codes halts, neither of which depends on the record having detail.

The grade rides the run it re-drove

brief.grade is written through the same transition seam that already writes brief.loop, so a fail verdict’s record lands durably on the run record of the re-drive it caused, and the failures reach the next session through 70/04’s existing pendingFixes → ctx.loopDrive.fix → ## REVIEW FINDINGS transport. No second transport exists.

The driven row carries the grade

drivenRow() emits the verdict, the code list and the observed case counts per drive, attributed by run id rather than by position. LoopState’s ten top-level keys and actShape()’s whitelist are byte-for-byte what they were.

A cap-exhausted halt reports what it could not close

Both cap-exhausted sites – the gate-decision halt and the pre-drive loop-cycle-cap halt – report accumulatedRecord(), the union of the grades carried by the loop’s own runs keyed by loopRunId, with the exhausting cycle’s grade carried inline because it has no successor run to ride.

The union survives a retried build attempt

accumulatedRecord() contributes one entry per distinct grade: a run whose retryOf lineage already contributed the same gradedAt is skipped, so a cycle whose build attempt was retried is counted once rather than twice.

An unconfigured repository is byte-identical

With no work.rubric declared, the loop’s LoopState, every report line, every driver input, the driver’s spawn argv and every run record are indistinguishable from the pre-54 shell – measured by neutralising the whole grade path and diffing full observable traces, not asserted.

FF-5409’s second clause is armed

acd-loop-probe-contract now drives a real loop to a grade-indeterminate halt and asserts the stop and its producer before the key-set shape it guards, and has been observed failing against two planted defects.

54/04 · Scenario traceability

A declared @executable → case join

src/work-doctor-rubric.mjs pairs an emitted case with an @executable scenario when the case’s name CONTAINS the scenario’s name, over scenarios the one feature parser resolved – @manual and @uat scenarios are never offered to the join, however their names read.

A miss is reported, never guessed

A case naming no scenario is case-unjoined; a scenario named by no case is scenario-unjoined; both report at warn and move no verdict, and no module on the grade path carries a levenshtein, edit distance, fuzzy, similarity or best-match fallback.

The honest no-op names the key that would activate it

With no work.rubric.report declared, the lane emits rubric-join-unchecked once per item that declares at least one @executable scenario, naming work.rubric.report; an item declaring no such scenario gets no notice at all, and no unjoined finding is ever emitted from a report nobody read.

A pure work:doctor lane that runs nothing

rubricTraceabilityGroup is a horizon-scoped CHECK_GROUPS member reading the last report as snapshot text, exactly as the controls lane reads its runner texts; it spawns nothing, and it joined the scanned work-doctor* family the day it was written.

The milestone-15 traceability gap filled outside the god-node

The join lives in a new lane leaf; validateWork and its 256 dependents are untouched, src/work-doctor-controls.mjs is not edited, and src/feature-parse.mjs is read and not edited – it remains the one feature parser.

A frozen three-code vocabulary of its own

RUBRIC_FINDING_CODES is frozen at case-unjoined, scenario-unjoined and rubric-join-unchecked, and RUBRIC_REPORT_CONFIG_KEY is the single spelling of the key its message names.

One armed fitness function

acd-grade-subject-is-emitted (FF-5408) is registered and has been observed failing against two planted defects – a prose classifier and a fuzzy fallback.

53 · The loop as a CLI artifact

aof work loop <ref|range> – the code-owned loop shell

The loop’s sequencing, phase dispatch, gate, bounded retry and stop conditions are code in src/commands/loop.mjs over the pure engine src/work-loop.mjs; no prose document decides them.

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: true – build-to-green, review-fix-rereview, autonomous-cascade – are all unpaired, because no record declares a monitoring edge at all.

The instruments

Nine fitness functions (FF-5201…FF-5209, 18 legs in test/arch/acd-loop-*.test.mjs) hold the structure – that the registry is not an item type, the module import boundary, the thirteen frozen vocabularies, the real registry parsing, check purity and determinism, timescale domain, command-surface route-only, Mermaid byte-freezing and the finding envelope. Six behavioural suites (139 cases over test/support/loop-registry-fixture.mjs) hold the behaviour, and test/work-loops-coverage-ledger.test.mjs re-derives the feature-to-suite traceability on every run – 323 scenarios and 461 rows across 28 tables, each with a deciding assertion or an exclusion whose pointer is looked up in the gate module rather than transcribed.

50 · Session launcher – start a session on a node, bound to a repo

The session-spawn wire kind

kind:"session-spawn" is a named directive on the mesh stream with its own frame builder, ack frame and worker receive lane, carrying no lifecycle phase and no work item.

A second named write route on the fleet face

POST /api/mesh/session accepts {nodeId, workspaceId, itemRef?} behind same-origin + application/json admission, mints a crypto.randomUUID() session id and answers it in the 200; the fleet face’s write allowlist is exactly two named entries, and the two fitness functions that pin it assert the count rather than a pattern.

Cross-process dispatch over the shipped relay

The fleet face in aof mesh ui reaches the directiveTargets registry owned by aof mesh serve over the existing loopback relay bridge; session-spawn is a third named lane on that bridge beside terminal-input and terminal-resume.

Worker-side PTY spawn and session registration

A worker receiving the directive opens a PTY in the chosen workspace – the operator’s default shell, not an assistant CLI – registers it through milestone 48’s session index, bridges its output onto the terminal frame producer, and tears it down on stop with no orphaned shell and no armed timer.

A coded outcome lane for a spawn the 200 could not answer for

GET /api/mesh/session-outcome?nodeId=&sessionId= answers {state, code, at} for every tuple and never a 404; a dispatch the router cannot route, or one that throws mid-send, becomes a synthesised refusal on that same lane rather than a success.

The new-session affordance on the terminals home

A New session ▾ trigger sits in the surface slot at every breakpoint and opens a bounded panel with Node / Repo / Item (optional); the picker annotates every node the payload carries and filters none, and the route – not the picker – issues refusals.

An operator-facing failure vocabulary owned by the browser

Fourteen refusal codes render as operator sentences from the UI’s own REFUSAL_MAP, each naming the machine the fault is about, with a stated fallback for a code the map does not know.

relaying as the session record’s seventh key

The session projection emits relaying on every live session as a strict record.relaying === true, so a record written before the key existed projects false rather than absent.

49 · The terminals home

/ is the terminals home

The application’s root route is a routed surface inside the shell’s crash containment that renders the live terminals of the fleet; ui/src/app/Landing.tsx no longer exists, SHELL_RENDERED_ROUTES is ["not-found"], and the four legacy query addresses (?mode=fleet|board|assets|wat) rewrite to their paths by replaceState, preserving every other parameter and the fragment byte-for-byte.

The grid of live panes

Every addressable session in the mesh renders as a tile carrying node, repo, work item where one exists, agent state and a live terminal; tiles are ordered (nodeId, repo, sessionId) in the browser, and each subscribed tile constructs a real ws://…/ws/terminal-view?nodeId=…&sessionId=… socket through the product’s own mount chain.

The pane is a fourth host of the one terminal control

HOST_GRID_PANE = "grid-pane" declares all eight affordances – including each absence with its reason – and ui/src/home/session-mount.mjs is the single author of a grid pane’s posture, which is derived from the feed axis and fails closed to a labelled read-only.

Typeable panes on the fleet origin

A grid pane whose session can accept input is POSTURE_INTERACTIVE and types through m42’s existing tuple-bound seam; entry stays content-blind and byte-bounded by MAX_TERMINAL_INPUT_BYTES, delivery stays session-exact through liveSessionInputs, and a pane can only type into the session its own socket names.

Invariant 4, amended rather than deleted

acd-fleet-terminal-input-constrained part 1 is now a surface→posture-home table covering all three surfaces with per-surface floors and discovery-by-sweep; parts 2 and 3 are untouched, every prior assertion survives verbatim, three are added, and no exemption list of any shape exists.

The feed axis – a pane says whether anything will ever arrive on it

Pane truth is composed from a feed axis derived from wire fields the fleet already polls, beside m46’s subscription axis, with the precedence fixed in one place; a session that is addressable but has no producer renders idle with a top-left no live output line and opens no socket.

A bounded live-socket ceiling

MAX_LIVE_PANES = 16 is arbitrated by one pure function over the whole row set, taking the currently-subscribed set as an argument; priority allocates free slots and never evicts, a pane beyond the cap is unsubscribed rather than refused, and retained tiles are counted against the cap rather than hidden from it.

Layout as a filter, persisted per operator

A per-origin browser preference behind one pure module that takes storage as an argument filters the live index; it is never a source of rows, adds no route, no mutation and nothing on the wire.

needs-input reaches the wire

projectAssignment appends code under sessionId’s own guard and WorkAssignment declares code?: string, so the agent state the worker already produced survives the hop to the fleet face – assignment-scoped, with no new reader and no rendering leaked into ui/src/fleet/api.ts.

One repo, said once

The current-work line groups on the raw repo string and prints each distinct repo once with a count – working · aof ×2, demo (session) – in the JS formatter and the Rust view-model alike, with a raw U+00D7 sign in both shipped sources and a captured fixture that makes the cross-language gate red when either half diverges.

One live region for the whole grid

The terminals home announces state through a single polite region whose role follows the host’s own hostAnnouncesState declaration; the gate counts implicit role="status" regions beside explicit aria-live ones.

The pulse honours reduced motion

One unlayered @media (prefers-reduced-motion: reduce) block silences .animate-pulse beside the .aof-pending the stylesheet already named, so all twelve animate-pulse sites across ui/ stop animating under the preference while every state stays distinguishable by word and colour.

Claude sessions reach the index

The distributed bundle wires the three session-lifecycle events for Claude, so a workspace that runs aof work update records its sessions into the index instead of leaving the terminals home empty everywhere but this repo.

The harness can drive a grid

The test harness mounts N controls from a caller-supplied entry with a per-pane driver, declares the shell on the bundle the control reads, moves real focus and dispatches real keystrokes; an entry must bundle the real control unless the caller declares terminals: false, and declaring it withholds the pane driver entirely.

48 · Routable session identity

The session id of record

A session record is keyed by (nodeId, workspaceId, assistant, sessionId) and carries the assistant’s own id in a frozen ordered seven; the id is read through the ordered ladder --session → payload.session_id → env.CLAUDE_SESSION_ID and is null when no channel supplies one – never generated, never normalised, stored byte-identical.

One live session, one record

Two concurrent sessions on the same (node, workspace, assistant) are two files on disk, and ending one leaves the other’s record and its presence entry intact.

The orphan reaper

A TTL-expired session record is removed from disk by its owning node at the startSession/pingSession write seam, under the same isStale predicate the rest of the mesh uses; a reap fault degrades loudly and does not fail the start or ping.

The session on the presence wire

readLiveSessions projects every live session to the frozen ordered six { sessionId, workspaceId, repo, assistant, lastPingAt, workspaceHasRun }, ui/src/fleet/api.ts’s PresenceSession declares exactly those six, and the control stream remains an entry-level pass-through with no per-field whitelist.

A complete wire: run subsumption moved to the formatter

The presence producer no longer drops a session whose workspace has a run – every live session reaches the wire stamped with the fact workspaceHasRun, and fleetCurrentWorkLines applies the display rule at the render, strictly on workspaceHasRun === true. What the fleet draws is byte-identical to what it drew before, on both the JS and Rust surfaces.

The fleet-side session index

buildSessionIndex in global-mesh-query.mjs answers “what live sessions exist across the mesh” as an (nodeId, sessionId) lookup plus a deterministically ordered array – a pure, rebuildable projection that performs no I/O, reads no clock, stores nothing, and returns null on every miss. It is served as the additive top-level sessions key on the global status payload, and a duplicate (nodeId, sessionId) is resolved by a total order (latest lastPingAt, then workspaceId/repo/assistant), never collapsed by filesystem arrival order.

Attribution without a second authority

workItem is derived onto a session at the index alone, joined on (target_node_id, session_id); no session record carries a work-item field and no session write path reads global_assignments. A session with no assignment is first-class: workItem: null, present and never dropped.

A session index that is live on the real fleet

A deployed node publishes the six-key entry with a real routable id, and aof mesh status --json on the control node returns it – measured on a three-node fleet, not only in fixtures.

47 · /fleet with a repo filter

The repo filter

/fleet narrows to one workspace via ?repo=<workspaceId>, applied at one seam above the region fan-out, and every collection on the GlobalMeshStatus wire is either narrowed there or declared machine-wide with its reason.

Filter and scope composing by intersection

?scope= and ?repo= are two independent questions answered together: scope narrows at the server, repo narrows the result at the client, and the composition is an intersection taken per collection.

The partial intersection as a third answer

A filter whose work is out of scope but whose machines are not renders populated – work regions at 0 of N, the node region naming the machines that carry the repo, and a notice stating the cause in words.

Five empty states, told apart by their cause

empty, unknown-filter, out-of-scope, filtered-empty and empty-mesh are separate renders selected by one predicate; the out-of-scope state exists because “zero workspace rows survived” cannot otherwise distinguish the scope excluded it from the mesh does not have it.

A filter that survives the address bar

The narrowing is a deep link that survives refresh, poll and a second browser profile, and one popstate listener re-derives both narrowings from the address, so Back and Forward re-narrow the page.

The filter control and its banner

A disclosure trigger in the shell’s surface slot occupying a fixed 150px slot at sm and above, and a “filtered by” chip row hoisted above the page’s state ternary so it renders in the populated and empty states alike.

Region 5’s relief under a filter

A filtered milestone card drops the workspace-name column every card would otherwise repeat, and the attention cluster’s yield order is a discrete ladder keyed to cluster arity: the drill-in and the secondary token surrender their words whole to pinned glyphs, and the assignment target is the last element to yield.

The board drill-in that opens a board

The drill-in resolves through GET /api/mesh/board-url, which refuses a workspace whose checkout is gone rather than answering 200 over an empty board; the local-shape boards branch unreachable since m34 is deleted rather than repaired.

A slot that degrades by truncating rather than by overprinting

The shell’s surface-bar slot is flex-nowrap with shrinkable occupants, so a row that cannot fit yields its one designated element instead of wrapping into the chrome above and the content below.

45 · UI app shell & path routing

Real URL paths

/, /fleet, /board and /config are the four addresses of the application; the render-root ?mode= ternary no longer exists. One frozen table in ui/src/app/routes.mjs is the only thing that maps an address to a surface, and acd-ui-single-route-table is the only place that fact lives.

A shared application shell

Every surface mounts inside <Shell>, which renders five named regions (notice rail, top bar, surface bar, content, overlay), publishes its measured height as --aof-shell-chrome-height, and owns one z-ladder, one skip link, one banner and one <main>. Chrome measures 48px at 1280 and exactly 88px at 768 and below; at the desktop app’s 760×520 window the content region is exactly 432px.

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.