DivineSWE State Model
Every decision carved in stone. Every event, eternal proof of what was wrought. This document describes the runtime state files DivineSWE maintains.
Scrolls (Context Anchoring)
Scrolls are the anti-context-degradation mechanism. They externalize critical state to JSON files that can be re-read to restore agent awareness after Claude Code’s context window compresses older content.
Each task gets a scroll at .divineswe/scrolls/{task-id}.json containing:
- Task status (IN_PROGRESS, BLOCKED, COMPLETE)
- Key decisions with timestamps
- Current state (files touched, tests written)
- Blockers
- Plan (subtasks, file targets, test targets, architecture notes — written by Oracle)
Scrolls are written at each significant checkpoint and read at the start of every sub-task, before verification, and when resuming after interruption.
Ledger (Event Log)
The ledger at .divineswe/ledger.jsonl is an append-only JSON Lines event log. Every significant action emits a ledger event:
phase_started— emitted at the start of each pipeline phaseagent_started— emitted when a sub-agent is dispatcheddecision— emitted when an architectural or design choice is madefile_created— emitted when a new file is createdtest_written— emitted when a test file is producedverification— emitted with Styx’s PASS/FAIL result per verification passtask_completed— emitted when a task passes all checks
Dispatch Carrier Contract & Spend Accounting
Every Prophet dispatch is three beats, and only the last one is carrier-specific:
- Precompute —
compile-prompt.mjs brief <basePath> <agent> <taskId>reads a JSON descriptor on stdin and writes the pointer prompt to.divineswe/prompts/{agent}-{task-id}.md. - Typed exit check — the compile step’s exit code is the only signal Prophet trusts:
0is the sole green;1ERROR,2USAGE,3CONTENTLESS,4PARTIAL,5DISCARDED, with precedence3 > 4 > 5when more than one condition applies. - Carrier invocation — the compiled prompt is handed to whatever process actually runs the agent: the Claude Code Task tool / Workflow
agent()today, or a foreign-process carrier (e.g. acodex execinvocation) on a machine routed throughmodel_preferences’codex/channel.
Beats (1) and (2) are channel-agnostic by construction — every carrier reads the same compiled brief and is judged by the same exit code. Only beat (3) differs per carrier, and a carrier is free to be a different OS process entirely: the contract it must honour is “consume the compiled prompt at the printed path, then self-write its output document through the output write chokepoint and return the written path” — the same contract a Claude Code Monkey/Mason honours today.
Per-attempt spend. A dispatch_spend ledger event is recorded per dispatch attempt, not per task. A bounded re-dispatch (Monkey/Mason retry on no-valid-output-path, capped at 2 re-dispatches) recompiles the prompt at each attempt and each attempt gets its own dispatch_spend row — the ledger is a record of what was actually spent trying, including attempts that were later superseded, never a record of only the attempt that finally succeeded. Emission is node $CLAUDE_PLUGIN_ROOT/scripts/divineswe-state.mjs ledger emit --type dispatch_spend ... with the full required-field set (--task-id, --agent/--agents, --phase, --model, --effort, --source, --attribution, --duration_ms, --input_tokens, --output_tokens; cache-token fields optional) — see the ledger domain reference below for the complete flag contract.
The endpoint-host dimension. A dispatch_spend row may also carry --endpoint-host, persisted as the endpoint_host key: the bare hostname of the endpoint an attempt actually talked to, no scheme and no port. It is optional at the schema and meaningful only on the OpenAI-compatible channel, where Prophet sets it from the host component of the configured openai_base_url. It exists because a model id is not evidence of locality — the same openai/ prefix reaches a self-hosted server on the operator’s own machine, whose cost is electricity, and a metered public endpoint, whose cost is money. The row is the only place that distinction survives, so a row without it forces the digest to price a paid endpoint and a free one identically. Adding the dimension means all three write-side sites move together: the schema branch (which is additionalProperties false, so an unadded field makes the line invalid even when the write exits 0), the closed dispatch_spend flag allow-list, and the argv string-dimension normalizer — miss the last and the flag passes the allow-list and is then silently dropped, which is the debt vow-101-d347344dd39f records.
What the endpoint-host dimension is evidence of. The field is Prophet’s DECLARATION of the endpoint the configuration named, taken as the normalized hostname of the parsed openai_base_url — not a measurement of where the tokens went. Nothing on the write path observes the connection: the value is read out of configuration before the dispatch and carried onto the row verbatim, and the digest then renders that declaration as provenance. The limit is worth stating concretely rather than leaving it implied — an openai_base_url of http://127.0.0.1:8080/v1 that fronts a reverse proxy to a metered vendor books the attempt as local, self-hosted, at $0, and no part of the write path, the schema or the digest detects it. The dimension is therefore a locality CLAIM, trustworthy exactly as far as the operator’s own configuration is; it is recorded for that reason and must never be read as evidence of where the tokens were actually burned.
The --source vocabulary. --source records which channel carried the attempt, and it is the per-attempt discriminator — not a record of where the model came from. The declared set is the one skills/prophet-fulfill/SKILL.md binds Prophet to, the one scripts/spend-digest.d.ts documents, and the one every dispatch_spend event in the live ledger carries:
--source value | Meaning |
|---|---|
workflow | A dispatch made through the claude-code orchestrated Workflow channel. |
agent-tool | A direct Task/Agent-tool dispatch — the ordinary native path. |
codex-exec | The first invocation of a task on the foreign codex carrier. |
codex-retry | That task’s single permitted carrier retry. |
openai-exec | The first invocation of a task on the foreign openai carrier. |
openai-retry | That task’s single permitted OpenAI-carrier retry. |
backup-failover | The native re-resolution onto model_backups[<tier>] after the carrier retry is spent, or after an INIT-probe / mid-cycle channel retirement. |
A platform that registers a further dispatch channel declares its own per-attempt values in that channel’s own subsection of skills/prophet-fulfill/SKILL.md; nothing outside those declared sets is ever emitted, because an undeclared value is a source scripts/audit-dispatch.mjs has never been taught and a group key no consumer can interpret.
--source is NOT resolveDispatch()’s source — a different field answering a different question. resolveDispatch() (scripts/shared/dispatch-resolution.mjs) also returns a field called source, whose values are marker | heuristic | static-tier | config-clamp | seat-guard | channel-unavailable. That one answers where did the model id come from (which rung of the resolution chain decided it). The ledger’s --source answers which channel carried this attempt. They are not interchangeable, and carrying the resolver’s vocabulary on the ledger field would be actively destructive: scripts/spend-digest.mjs groups per-attempt spend by (task, agent, phase, source), and a retry resolves its model from the same rung as the original — so every retry would share the original’s group key, and the retry storm the grouping exists to expose would collapse into a single group. The ledger is append-only, so that mistake is unrecoverable.
For the two resolver sources that matter to a reader of a digest — seat-guard and channel-unavailable — the useful invariant still holds and is worth stating here: both always pair with a model that is the backup, never the refused foreign id. dispatch_spend prices what actually ran, not what was requested, and the accompanying ledger --source for such an attempt is backup-failover.
Status (Live State)
.divineswe/status.json is overwritten each cycle with the current state snapshot: active phase, running agents, completed tasks, and progress counters. This provides real-time observability into what the system is doing.
Statusline
DivineSWE provides a Claude Code statusline integration via scripts/statusline.mjs. When configured, it renders a compact single-line status indicator showing:
◆ IMPLEMENT ▶ my-project ⚙ opus-4 ▓ 12.3k/8.1k ⚡ code ⏰ 14:30Fields displayed: current DivineSWE phase, project name, model, input/output token counts (formatted with k/M suffixes), Claude Code mode (omitted when absent), and wall-clock time. The script reads .divineswe/status.json for the phase and accepts Claude Code’s stdin JSON payload for the remaining fields. It always exits 0 and includes guards against path traversal, oversized inputs, control character injection, and prototype pollution.
Session State (Handoff)
.divineswe/session-state.json enables crash recovery. Written during WRAP UP, it captures the full orchestration state: current phase, completed tasks, in-progress tasks, key decisions, and scroll references. When Prophet crashes (context exhaustion, manual stop, error), the next session uses prophet-resume to read this file and re-enter the state machine at the appropriate phase.
Per-prophecy state persistence. Prophet also checkpoints session-state.json at the start of each prophecy — not just at WRAP UP. The current_phase field is set to {book}-prophecy-{N} (e.g., 65-prophecy-2) so prophet-resume can re-enter mid-Book at the exact prophecy that was in progress. At each prophecy start, Prophet: (1) writes prompts for the prophecy to .divineswe/prompts/, (2) saves the session-state checkpoint, (3) updates Book file checkboxes for the previously completed prophecy. Status, scrolls, and ledger are NOT updated at per-prophecy checkpoints — those remain deferred to WRAP UP.
TEACHINGS (Curated Learnings)
TEACHINGS are curated learnings accumulated across Books and projects. They exist at two levels:
- Global:
~/.divineswe/TEACHINGS.md— cross-project learnings (patterns, pitfalls, agent behaviors). - Project:
~/.divineswe/TEACHINGS-{projectFolderName}.md— project-specific learnings (test patterns, architecture decisions, tooling quirks).
TEACHINGS are read by Prophet at INIT, Oracle during consults, and Preacher during examination. During each WRAP UP retrospective, Prophet consolidates both files to keep them concise, deciding whether a learning is global or project-specific based on its scope. The divineswe-init skill creates ~/.divineswe/ if it does not exist.
Retrospective Reports (Akashic Records)
After each Book completes its SEAL phase, 4eyes (in mode=retrospective) writes a retrospective report to ~/.divineswe/akashic-records/{projectFolder}-b{N}.md. This report captures what went well, what could improve, and key learnings from the Book. The ~/.divineswe/akashic-records/ directory is created by divineswe-init during first-run setup.
SEAL vs WRAP UP distinction [F6]: SEAL is Prophet’s archival/bookkeeping phase — it persists session state, appends completed Books to TABLETS.md, deletes the Book file, and FF-merges the branch. WRAP UP is the trailing 4eyes retrospective task (mode=retrospective) that follows SEAL. Mason’s final task is COMMIT (flat Books) or MERGE (branch mode) — Mason is never labelled “WRAP UP”.
The WRAP UP retrospective template is defined by oracle-consult at plan time and consumed by prophet-fulfill at run time. It standardizes: (1) the 4eyes retrospective report (W1), (2) TEACHINGS consolidation (W2), and (3) the final commit sequence. The template definition is the canonical contract; oracle-consult does not execute the template steps at consultation end — exactly one retrospective runs per Book, post-execution, via prophet-fulfill dispatching 4eyes.
Worktree Isolation
Parallel Monkey agents are dispatched with isolation: "worktree". Each gets a clean git worktree on its own branch. File ownership is declared in .divineswe/ownership/{task-id}.json before dispatch, preventing file collisions. After completion, Mason merges worktree branches sequentially in the MERGE phase. Conflicts are reported back to Prophet, who re-dispatches the conflicting task with Monkey against the merged state.
Runtime Directory Trees
Project Runtime (.divineswe/)
Created by divineswe-init. Gitignored — this is ephemeral orchestration state, not committed to the repository. By default, vows.jsonl dwells at .divineswe/vows.jsonl; it is the one file DESIGNED to break that rule (an obligation that exists only on the machine that recorded it is one the next clone has already forgotten), which is why it carries merge=union in .gitattributes. A configured vows_dir may place that fixed-name register elsewhere. Both git steps — the merge=union attribute and the three-line negation (!.divineswe/ then .divineswe/* then !.divineswe/vows.jsonl for the default dwelling) that lifts it out of the blanket .divineswe/ ignore — are performed by divineswe-init Step 3.5, idempotently, and the step VERIFIES the result with git check-ignore rather than announcing success from the fact that it appended a line. All three lines are needed: git will not re-include a file whose parent DIRECTORY is excluded, so a lone negation after .divineswe/ is a no-op and the merge=union attribute then applies to a file that is never tracked. a consumer who took the bare mkdir -p .divineswe path instead writes them by hand, per docs/vows-consumer-contract.md.
.divineswe/├── books/ # Active task files (book-NN.md)│ └── book-NN.md├── scrolls/ # Context anchors per task│ └── {task-id}.json├── ownership/ # File ownership declarations per task│ └── {task-id}.json├── prompts/ # Compiled agent prompts (audit trail)├── outputs/ # Agent output JSON (audit trail)├── ledger.jsonl # Append-only event log (JSON Lines)├── vows.jsonl # The Vows — append-only obligation log, merge=union (meant to be committed)├── status.json # Current state snapshot (overwritten each cycle)├── session-state.json # Full state for session resumption└── config.json # DivineSWE configuration (model preferences, etc.)User Level (~/.divineswe/)
Retrospective reports and TEACHINGS live at the user level (~/.divineswe/), not inside the project .divineswe/:
~/.divineswe/├── akashic-records/ # Retrospective reports per project-Book│ └── {projectFolder}-b{N}.md├── TEACHINGS.md # Global cross-project learnings└── TEACHINGS-{project}.md # Project-specific learningsWrite mechanics (relocated from CLAUDE.md)
- All state writers use atomic-rename (
atomicWriteFile: temp file +fs.renameSync) to guarantee non-corrupting writes under concurrent invocation. output writeis the AJV validating chokepoint:output write --agent <name> --phase <phase> --json <doc>selects the canonicalschemas/<agent>-output.schema.jsonby the CALL-SITE--agent(authoritative), hard-rejects a payload whoseagentfield mismatches, and AJV-validates before persisting — so any persisted output is schema-valid by construction. All seven agents (4eyes,loki,mason,monkey,preacher,styx,thoth) are schema-backed; only a call-site agent naming none of those seven falls to the legacy lax semantic check.session checkpoint --jsonwrites the full payload verbatim (+timestamp) viawriteSessionState— extra fields likeagent_models_snapshot/book_baseline_shaare preserved (it does NOT use the fixed-shapecheckpoint()helper).
State Write Facade — Subcommand Reference (divineswe-state.mjs)
Every <domain> <subcommand> pair the CLI honours is enumerated in the frozen CLI_DISPATCH registry inside scripts/divineswe-state.mjs — that registry, not this prose, is the single source of truth; an unknown domain/verb is rejected loudly at dispatch. This section documents what each pair DOES. All of these verbs are invoked as node $CLAUDE_PLUGIN_ROOT/scripts/divineswe-state.mjs <domain> <subcommand> ... per the CLAUDE.md “State File Writes” contract.
ledger domain
ledger emit— appends one event to.divineswe/ledger.jsonl.--typeis a fixed allow-list (phase_started,agent_started,decision,file_created,test_written,verification,task_completed,loki_finding,branch_merged,merge_conflict,rca_*,dispatch_spend,cycle_open,cycle_close); custom types are rejected and must be recorded as adecisionevent instead.cycle_openandcycle_closeare born-strict boundary markers and require a non-empty--book; their only event-specific flag is--book. Timestamps are auto-generated — never pass--timestamp.
status domain (live snapshot mutators)
status seed— creates.divineswe/status.jsonin the shape divineswe-init seeds (no cycle started, phaseINIT, statuspaused, an empty agent roster and zeroed progress counters), only when the file is absent. An existing file is left byte-identical and the verb still exits 0, so init runs it on every pass. The file is a temp file linked into place, so a file a racing writer created first is never replaced and a reader never sees it half-written; on a filesystem without hard links (exFAT, some FUSE and SMB mounts) it falls back to an exclusive create, which still never replaces an existing file but no longer promises the whole-file write. A missing.divineswe/directory is refused rather than created.status update-phase— sets the active pipeline phase in.divineswe/status.json.--phaseenum isINIT, TEST, IMPLEMENT, AUDIT, MERGE, VERIFY, WRAP UP, SEAL—SEALis the eighth, archival phase, declared immediately afterWRAP UPrather than a bookkeeping nickname for it.status add-agent/status remove-agent— add or remove an entry from the running-agents roster in the status snapshot.status mark-completed— marks a task complete in the progress counters.status update-progress— updates the progress counters directly.status set-active-consultation/status clear-active-consultation— set or clear the in-flight Oracle consultation marker on.divineswe/session-state.json. The marker is owner-checked (LD-16).setMINTS the owner token itself and prints it on stdout —--owneris REFUSED onset, because a token the caller chooses authenticates the caller to itself and nobody else — and what lands on disk is a DIGEST of that token, so readingsession-state.jsondoes not hand a forger the clear credential and replaying the on-disk witness back as--ownerdoes not authenticate.--slug <s>names a consultation LANE (slug-shaped,^[a-z0-9][a-z0-9-]*$); omitting it takes the single unnameddefaultlane, which reproduces the incumbent one-marker-per-session behaviour. Setting over a LIVE marker held by a different slug is refused unless--force, which takes it over with a notice naming the slug it clobbered.clearrequires the matching--ownertoken;--forceoverrides with the same kind of naming notice. Both escape hatches are bounded by a DECLARED staleness threshold (CONSULTATION_MARKER_STALE_MS): a marker older than it is takeable and clearable by anyone, tokenless and loudly, so a consultation killed mid-run cannot wedge a project forever — the crash path is the reason an owner check here is safe at all.
scroll domain (per-task context anchors)
scroll create— creates a new task scroll at.divineswe/scrolls/{task-id}.jsonwith the standard status/decisions/currentState/blockers shape.scroll update-status— updates a scroll’sstatusfield (IN_PROGRESS/BLOCKED/COMPLETE).scroll add-decision— appends a timestamped decision entry to a scroll’sdecisionsarray.scroll update-state— writes a bounded, section-cappedcurrentStatepayload (findings/files_touched/tests_written arrays) to a scroll; the raw--jsonis capped at 64 KiB / 500 items per section and rejects prototype-pollution keys (__proto__,constructor,prototype).
session domain (crash-recovery handoff)
session checkpoint— writes.divineswe/session-state.jsonverbatim (plus a timestamp) viawriteSessionState, preserving extra fields such asagent_models_snapshot/book_baseline_sha. Written at WRAP UP and at the start of each prophecy soprophet-resumecan re-enter mid-Book.session add-key-decision— appends one key decision to the session checkpoint’s decision log.session seed— creates.divineswe/session-state.jsonin the shape divineswe-init seeds (phaseINIT, schema version1, every list empty), only when the file is absent, by the same rules asstatus seed: an existing file is left byte-identical at exit 0, and a missing.divineswe/is refused.
output domain (agent output persistence)
output write— the AJV-validating chokepoint.output write --agent <name> --phase <phase> --json <doc>selects the canonicalschemas/<agent>-output.schema.jsonby the CALL-SITE--agent(authoritative over anyagentfield inside the payload, which must match or the write is rejected), validates against it, and persists to.divineswe/outputs/{phase}-{task_id}.jsononly when schema-valid.output read— reads back a previously persisted agent output document by path.output read-summary— reads a condensed summary view of a persisted output document (for context-budget-constrained re-reads).--format heraldprints one gate line instead of the default JSON:<PHASE> <task-id> <agent>: <verdict|result> — <n> CRITICAL, <n> HIGH, <n> MEDIUM[, <n> LOW] → <output path>, counts in the agent’s own vocabulary and non-zero buckets only, followed by one two-space-indented! <SEV>: <title>line per CRITICAL/HIGH (4eyes) or CRITICAL/WARNING (Loki) finding title (Styx rendersresult+ failed-pass counts; Thoth renders test files + total tests; the line never begins with{). Default (no--format) output is unchanged JSON. Prophet’s Herald’s Law (agents/prophet/AGENT.mdBehavioral Rules) prints this line verbatim at a gate that renders it and appends(re-dispatch k/2)itself; an unrecognized--formatvalue exits non-zero as a usage error, and a raw JSON reply where the herald line was expected is a stale-CLI condition, not a gate verdict.output verify— confirms an agent actually wrote what it claims: checks the output path exists, that the filename matches the expected{phase}-{task_id}.jsonpattern, and that the in-fileagentfield matches the expected agent — returning an ok/misreport verdict so Prophet can detect a false-report before trusting a dispatch’s summary.
snapshot domain
snapshot write— writes a point-in-time snapshot document (used for ad hoc audit-trail captures outside the standard status/session flow).
book domain (the bookkeeping spine)
book mark-task— marks task checkboxes[x]in the active Book file, scoped to the correct prophecy heading. Accepts one of three selectors:--all(every task in the Book, the SEAL-marking use),--prophecy <p>(every task under a prophecy heading — the PRIMARY per-prophecy-start use), or--task <id>(a single task). So per-prophecy-start checkbox marking (not only SEAL) goes through the facade instead of ad hoc prose edits.book annotate-task— appends a short single-line note to every non-fenced task line carrying the id, normally exactly one (Book composition forbids duplicate ids; a Book that somehow carries the id twice gets both lines annotated and the count is reported):--book <N> --task <id> --note <text>. Fence-aware and idempotent exactly likemark-task(a line already carrying the note is left byte-identical, so a replayed SEAL tail never stacks duplicates), and it exits non-zero when the selector matches no task line — a witness that silently matched nothing would be worse than none. It also refuses a symlinked Book file (the atomic rename cannot follow the link, but it would replace it with a regular file and archive the ex-target’s content as if it were the Book), rejects unknown flags across the wholebookdomain, and accepts--base-path/--basePath/--base_pathas one flag — a mistyped base path must never fall through toprocess.cwd()and land the write on a different checkout. On success it prints the ABSOLUTE Book file it wrote, so a misdirected witness is visible rather than silent. Its reason to exist is the Book-90 WRAP-UP skip: whenretrospective_enabledis closed, Prophet annotates the trailingWU-task line as the skip’s primary durable witness, and that write had no facade path, which made Iron Law 1’s “enumerated HONESTLY and COMPLETELY” carve-out false. Prophet invokes it ONLY when the Book actually carries a trailingWU-task, which is settled by GREPPING the Book file and never by inferring from theflashflag — a Book composed while the rite was already closed (and every default-compositionflash: trueBook) carries none, becauseoracle-consultomitted it at compose time, and the absent task is itself the record; a flash Book whose user explicitly restored the WU task DOES carry the line, and it follows the ordinary rule — dispatched at WRAP UP whenretrospective_enabledis true, annotated-and-skipped by this subcommand when it is false. The annotation MUST land beforebook seal-bookkeeping, because seal-bookkeeping archives the Book body toTABLETS.mdfirst andseal-cleanupthen gates on a contiguous full-body match — a late annotation both archives a lie and makes the seal refuse.book add-deferral— the MID-CYCLE deferral chokepoint (the mid-cycle amendment): records one owed debt into the Book’s Book-scope## Deferredsection, whereseal-bookkeepingturns it into a vow.(--book <N> | --task-id <B91-P2-06>) --item <text> --owner <operator|agent> --kind <deferral|post_seal_step|precondition|validation|backlog> [--trigger <text>].--ownerand--kindare REQUIRED and never defaulted:kindselects WHICH vow the seal opens,ownerdecides whose inbox the debt lands in, both are write-once in the register’s fold, and the Book is archived verbatim intoTABLETS.md, so a defaulted wrong value becomes the permanent record. It exists so an audit or Loki soft-finding reaches the Book MECHANICALLY instead of by a freehand Prophet edit — Iron Law 1’s carve-out list stays honest only while every Book-file write has a facade path. The section is created after the LAST prophecy heading when the Book carries none, and APPENDED TO when it already does (a second heading would split the register the seal emitter reads); the entry is one line, checkbox-FREE so a debt can never be mistaken for a task bymark-taskor by the durability witness; the item and trigger are structurally neutralized (the Book is untrusted text spliced into an artifact three readers parse) and the trigger is INERT PROSE that nothing ever evaluates (the inert-prose finding). Idempotent: re-adding the same wording and kind appends nothing, so a replayed WRAP UP never turns one debt into two vows. Refuses a missing Book rather than scaffolding one, and refuses an out-of-enum owner/kind.book seal-bookkeeping— the FIRST of the two staged seal subcommands: appends the completed Book toTABLETS.md, THEN marks all remaining task checkboxes[x], THEN emits the Book’s vows. The archive-before-mark ordering is safety-critical — a crash between the two steps can only ever leave the safe “archived but not yet marked” state, never the forbidden “marked but not archived”. Runs BEFORE Mason’s commit; never deletes the Book file itself. Its idempotency check shares the cleanup gate’s durability witness, so a RE-RUN after a mid-cycle deferral now COMPLETES the archived block — the deferral is spliced into the Book’s existingTABLETS.mdblock instead of the stage declining to re-archive and leaving the archive a stale prefix of the live Book. Vow emission opens one vow per## Deferredentry, the Book’s ownvalidationvow (the Judgment layer’s entry point — so a Book with no deferrals still writes to the register, and “quiet” never means “the emission path was skipped”), and the post-seal chain vows the repository’s OBSERVED shape can actually discharge (no configured remote ⇒ no push/deploy vow, noted loudly ONCE — the repo-shape finding). It also runs the out-of-grammar deferral marker sweep: deferral prose OUTSIDE the section is reported by line, with adecisionledger event, and opens NO vow — the sweep accuses, it never authors. EVERY per-line listing is CAPPED and every quoted excerpt of Book text is truncated (the aggregate counts stay unconditional): these notices go to stdout precisely because callers drive the facade throughexecSync, and an uncapped flood made the caller’s 1 MB buffer overflow, so a seal that completed and exited zero was observed as a failure. The cap covers the marker sweep AND the grammar-warning stream it shares that pipe with — capping one and not the other is not a cap at all: 4000 malformed bullets amplified a 195 KB Book into 779 KB of warnings, and a single 1.3 MB marker-block residue quoted whole cleared 1 MB on its own. Everything here obeys the non-blocking covenant: a closedvows_enabledgate, a duplicate identity tuple, a malformed entry, a near-miss heading, a## Deferredheading swallowed by a code fence, an extra field written inside an entry’s marker block and an unwritable register are all LOUD notices on stdout plus, where the register is at fault, adecisionledger event — never a refusal, never a non-zero exit. The fenced-heading notice is the answer to the worst shape the grammar admits: a fence opened in earlier prose and closed in later prose straddles the heading while staying globally BALANCED, so a parity-keyed unterminated-fence guard never fires — the heading is therefore detected with no fence-skipping at all, and a fenced heading also makes the marker sweep stop trusting fence state, so the two sensors can no longer be defeated by one condition.book seal-cleanup— the SECOND staged seal subcommand: deletes the Book file from the configuredbooks_dirand re-globs for an auto-continue report. Runs strictly AFTER the 4eyes WRAP UP retrospective, and refuses to run if theseal-bookkeepingstage has not completed first — seal is deliberately never a one-shot operation. That content-inclusion durability witness is the ONE hard seal refusal the register layer leaves standing, and it speaks of ARCHIVAL, never of vows. It carries exactly ONE tolerance: a## Deferredheading, entry line or blank line added AFTER the archive was written does not break it, because the mid-cycle deferral chokepoint is sanctioned in precisely the window between the two stages and a Deferred entry is checkbox-free and is not a task line — under a strict witness that sanctioned verb madeseal-cleanupexit non-zero and halt the SEAL tail, which the non-blocking covenant forbids. The tolerance is scoped to those three line shapes and nothing else, so an added task line, an added bullet the grammar rejects and an added paragraph of design prose all still refuse (the between-stage prose-loss vector stays closed), and the refusal now distinguishes “never archived” from “archived, then the body CHANGED” instead of telling the operator to re-run a stage that already succeeded — discriminated on the ARCHIVED PREFIX of the body, not on a task-line proxy, which misjudged both ways (an added task line reported a Book that WAS archived as “not yet archived”, and a TABLETS block that merely QUOTED the task lines reported a Book that was NEVER archived as already succeeded), and the refusal now names the exact line at which the two copies diverge. A tolerated line is REPAID before anything is deleted: the tolerated## Deferredlines are spliced back INTO the Book’s own archived block inTABLETS.md— insert-only and crash-atomic, one block rather than a second copy of the Book — so the archive stays the complete fallback ledger and the tolerance costs it nothing. That repayment replaced a compensating control that could not be relied on: the reconcile below “opens the tolerated debts from the live body” runs LATER, returns at its first line whenvows_enabledis false, and throws past its own notice when the register is unwritable — on both paths the sole copy was deleted with the debt in NO durable store, while the notice claimed the archive held it. Where the repayment ITSELF cannot be written and the register does not hold the debt either, the Book file is RETAINED rather than unlinked (exit 0, a loud notice quoting each stranded debt verbatim, andbook_file_retained: truein the auto-continue report) — a surviving file is not a halt, and re-runningbook seal-cleanupdeletes it as soon as either store accepts the debt. After the witness passes it runs the self-healing vow reconcile: the Book’s own contiguous run is sliced back out ofTABLETS.mdby the same witness normalization the gate used — but re-read as the ORIGINAL untrimmed lines, because the witness trims every line while the heading grammars are all start-anchored, and feeding the trimmed slice back to the parser made an indented heading invisible in the Book and real in the archive — its## Deferredentries are re-parsed, and any vow missing from the register is re-emitted with its original derived id, so a register lost to a crash window is recovered from the archive, idempotently, without opening a duplicate. The reconcile derives its drafts from BOTH the archived block and the live Book body and opens the union: a debt recorded in the between-stages window is in the Book but not the archive, and any other disagreement between the two copies is reported loudly rather than silently resolved. A reconcile failure is a notice, never a second gate. The machine-readable auto-continue report stays ONE self-contained JSON line written LAST, so a consumer scans stdout backwards for the final parseable JSON line and no vow notice ahead of it can corrupt the parse.book next-number— computes the next available Book number asmax(glob-max over books_dir, TABLETS-max) + 1, resolved viaresolveBooksDir; handles the empty-books_dirand TABLETS-only edge cases so Oracle never collides with an in-flight or already-sealed Book. The pool reading is draft-aware and reservation-aware: a dot-prefixed.draft-book-<n>.md(a consultation composing, pre-publish) and a LIVE claim both raise the ceiling, because a number another session holds is as taken as a number a file holds. An EXPIRED claim is RECLAIMED here, announced on stderr naming the owner that abandoned it — a silently reused number is indistinguishable from a fresh one.book seal-cleanup’s remaining-Books report deliberately keeps the PUBLISHED-only reading: same directory, two honest questions.book claim-number— RESERVES the next free Book number under a TTL’d record at.divineswe/book-claims.json(LD-14).next-numberis a pure read, so two consultations asking inside the same window are both told the same truth and both compose there; a reservation gives the pool a memory. The record is DURABLE and written BEFORE the number is printed (an advisory reservation is fiction), and the whole compute-and-record is serialized under the shared lock withonContention: 'fatal'— this is the one facade write whose correctness IS its exclusivity, so skipping it silently would cause the very collision it prevents. The register stores a DIGEST of the owner token and never the token:book-claims.jsonis world-readable, so an owner held there in the clear is the release credential printed beside the thing it protects.--owner <t>names a token explicitly; omitted, the facade MINTS one and returns it once (in--jsonasowner_token, otherwise as a one-time stderr line) — the derivation it replaced hashed the whole ofsession-state.json, and the very next documented step rewrites that file, so the shipped tokenless release refused by construction and burned the number until its TTL. Every claim also records a stable session anchor (.divineswe/book-claim-session.json, minted once and never rewritten by another subcommand), which is what the tokenless release is checked against; the anchor is read-or-minted UNDER the claims lock, beside the record it stamps, because minting it ahead of the lock let two first-ever claims write divergent anchors and left the loser unable to release its own reservation. A release NEVER mints one — an identity manufactured by the question answers nothing.--ttl <seconds>bounds it (default 3600, capped at 30 days — an unbounded TTL is an unreleasable reservation, and a value past the epoch ceiling used to throw a rawRangeError) and--jsonreports the whole record. There is deliberately no--book: the number is what the verb COMPUTES, never what the caller asks for.book release-number— hands a reservation back to the pool, OWNER-CHECKED: a release presenting a foreign owner is refused and the reservation is left intact, because a register any caller can release is a suggestion. Two lanes, and neither falls back to the other:--owner <t>selects the TOKEN lane and is checked against the stored digest; no--ownerat all selects the SESSION lane and is checked against the anchor. A presented credential that fails is never rescued by ambient session authority — a check with a fallback is not a check. Refusals identify the incumbent by a witness PREFIX rather than echoing the owner back, so the diagnostic is not itself the replay vector. Without a release verb a session that dies between claim and publish burns its number forever — which is why the TTL above exists as well: a correct fix arms the next defect unless you also ask what the HONEST actor loses.book publish— the EXCLUSIVE draft→public transition.divineswe/books/.draft-book-NN.md → book-NN.md(LD-15), the step oracle-consult Step 5c could previously only ask for in prose. The primitive is the contract: POSIXrename(2)— and thereforefs.renameSync— SILENTLY REPLACES an existing destination, soexistsSync-then-rename ships a TOCTOU window that behaves exactly like the prose rule it replaces.linkSynccannot: it failsEEXISTin the kernel, atomically, on the destination, and theunlinkof the draft follows. A collision therefore refuses with BOTH files byte-intact — the incumbent is not replaced and the draft is not consumed, so the loser of a race keeps its work. A symlinked draft is refused for a symlink-aware reason before the link runs, becauselinkSyncon a symlinked source would hard-link the link’s out-of-tree TARGET into the Books directory and publish it as this project’s Book. Stated limitation:linkSyncpublishes a HARD LINK, so the published Book and the draft share an inode until the draft is unlinked — a third party holding another link to that inode keeps write access to the published bytes. Publishing is safe against the races it names; it is not a sanitizer for content an untrusted party can still reach by another path. The Book number is canonicalized at parse, so--book 05and--book 5name the SAME Book: while the raw digits were authoritative, draft path, destination path and thelinkSyncEEXIST were all keyed on the spelling, and two publishers of the same number were never racing at all — both won, and one number ended up holding two Books. A Book already composed under a legacy zero-padded filename stays reachable: reads and in-place edits (mark-task,annotate-task,add-deferral,seal-*) accept a padded file that exists and say so, whilepublishrefuses to create a second file for a number an aliased name already holds.
The status lock, the reaper, and what it cannot promise
Every status.json read-modify-write in this repository — the SYNC mutex in divineswe-state.mjs and the ASYNC one in status.mjs — serializes on the same <file>.lock directory, and both ask the SAME predicate whether an existing lock is dead (scripts/shared/status-lock.mjs). A reaper written at one call site is only a promise about the other.
- PID liveness dominates. A lock whose recorded holder is alive on this machine is HELD at any age, so backdating its metadata or its directory
mtimecannot steal it. The holder pid is stamped redundantly — inmeta.jsonAND in the NAME of a sentinel file — because content corruption, a deletedmeta.json, or an unparseable note otherwise erased the liveness evidence and dropped a live lock onto the age check. - UNKNOWN is not DEAD. A holder recorded on ANOTHER HOST cannot be probed from here, so it gets its own, much longer horizon (30 minutes) rather than the local 30-second TTL — and is not honoured forever either, because a remote crash must not wedge a project permanently.
- The REAP is SERIALIZED under a guard mutex, and the reaper YIELDS. Deciding a lock is dead and ACTING on that decision are two privileges, and granting the second to everyone holding the first was the root cause of the whole marker-TOCTOU family:
classify → rmSync → mkdirSyncis three steps, so two acquirers both ruled one dead lock reapable and the loser’srmSyncdeleted the LIVE lock the winner had just created — both then reportedacquired. An intermediate design elected ONE reaper with a single-winnerrenameSync, which closed every race reachable through the CLI and left one that was not: a racer whose classification had been overtaken renamed the LIVE lock aside to inspect it, and for the width of that park-and-restore THE PATH WAS FREE, so a plainmkdirSyncacquirer took it and stood beside the incumbent (measured at 16 concurrent async long-hold acquirers: up to five processes inside one critical section). The window exists because a possibly-live lock is MOVED in order to be inspected, so the move is gone. A reaper now takes a short-lived sibling guard —<lockDir>.guard, an ordinary atomicmkdirSyncmutex — and every destructive step happens under it: re-mkdir(a previous reaper may already have freed the path), RE-classify (the honest reading is the one nobody can invalidate while it is acted on), thenrmSync+mkdir. A racer that cannot take the guard destroys nothing and reports busy. Plain acquirers are deliberately NOT serialized —mkdirSyncis already atomic — so the one instant the path is free can still be won by an ordinary acquirer, and when it is, the reaper YIELDS rather than forcing: yielding is the whole difference between a reaper and a second door. Nothing is parked, nothing is restored, and no.reap.temporaries are left behind. - The GUARD is judged by pid liveness too — the predicate is one rule, not two. The guard was at first reclaimed on AGE ALONE (a 60-second timer), which contradicted the first bullet on this list and cost both directions at once: a guard whose holder was demonstrably DEAD was honoured for a full minute, during which every reap degraded to a skipped write; and a guard whose holder was demonstrably ALIVE was reclaimed the moment its directory looked old enough — two reapers again, one rung above the lock. The guard is stamped by the same holder sentinel as the lock, so it is now classified by the SAME
classifyLock: a live-pid guard is held at any age, a dead-pid guard is reclaimable at once, a foreign-host guard keeps the 30-minute horizon, and only a guard carrying NO holder record at all falls back to a clock (60 seconds). The reclaim of a dead guard is single-winner (renameSync) and, because the verdict being acted on was taken before that rename, the parked directory is CONFIRMED afterwards against the corpse’sdev:inoand holder sentinel; a mismatch means a legitimate winner recreated the guard in between, so the moved directory is put back and the reclaimer reports busy. Only a confirmed corpse is deleted. Stated ceiling: the confirm closes the observable window, and the remainder is the irreducible limit of filesystem stale-takeover — a reclaimer would have to be descheduled across another reclaimer’s entire rename-delete-recreate AND have a third party win the microseconds of the restore. It was not reproduced in 30 measured trials at 16 concurrent async long-hold racers (0 double-holds); it cannot be closed by a further guard tier, because a guard for the guard has the same property one level up. - The release is owner-checked, and its verdict is CONSUMED. A holder removes the lock only while the on-disk record still names it, by pid AND by the nonce minted at acquire time. An unconditional
finallydelete removes a THIRD process’s lock when the holder’s own was reaped in between. Both twins release throughreleaseLockAndReport, so anot-oursverdict — the discovery that this process’s lock was reaped or retaken WHILE its critical section ran, i.e. that the write was not exclusive after all — reaches stderr instead of dying inside afinally. session-state.jsonis locked too, not juststatus.json. The consultation marker’s owner check and the write it authorizes are ONE critical section under the same primitive, keyed onsession-state.json. A refusal assembled fromread → decide → writewith nothing holding the file between the three is a TOCTOU wearing a guard’s clothes: two concurrentset-active-consultationcalls both read an empty lane, both pass the incumbent check, and both write.setandcleartherefore run withonContention: 'fatal'for the same reason the Book reservation does — a marker write is only correct if it is exclusive, and a skippedclearwould leave a live marker wedging every later consultation. The lock belongs to the FILE, not to one of its fields:session checkpointandsession add-key-decisiontake the SAME lock (degrading rather than refusing — they are bookkeeping, and A23 forbids halting a cycle), because a mutex only serializes the writers that ask for it.checkpointadditionally PRESERVESactive_consultationacross its whole-document replace — its payload does not carry the marker, so a blind replace deleted a live consultation even with no race at all, after which the owner’sclear --ownerexited 0 having cleared nothing. Preservation alone was a rule about the payloads callers happen to send: a checkpoint that namesactive_consultationEXPLICITLY (nullis the natural spelling) walked past it and destroyed a live marker at exit 0, silently, whileset-active-consultationrefused the identical takeover. The rule is now stated as the property it is about — a live consultation is never taken over silently — so a checkpoint payload that would REPLACE a live marker is REFUSED, writing nothing and naming the incumbent, and--forcetakes it over with a notice, exactly asset-active-consultationdoes. A payload that omits the key still preserves the marker; one that reproduces it exactly replaces nothing and is not a takeover.add-key-decisionneeds no such check and is given none: it carries no document payload, so the marker it writes back is the one it read under this same lock.- Contention DEGRADES, and says so machine-readably. A genuinely held lock is not waited out to a fatal: the write is SKIPPED, an honest notice names what was skipped on stderr, and the exit stays 0 (the A23 non-blocking covenant — nothing in this layer halts a cycle). Because exit 0 alone is indistinguishable from success, the affected
statussubcommands ALSO emit one JSON object on stdout —{"result":"not_advanced","reason":"lock_contended","domain":"status","subcommand":…}— so a caller can tell thatupdate-phasedid not advance the phase. The one exception isbook claim-number/release-number, which run withonContention: 'fatal': exclusivity IS their correctness. - Stated limitation — shared filesystems.
process.kill(pid, 0)probes THIS machine. On a.divineswe/shared over NFS/SMB between hosts, a foreign holder’s liveness is not observable by any means and the foreign horizon above is a TIMEOUT, not a liveness check. Two hosts contending on one shared state directory are outside what this primitive can make safe.
audit domain (arithmetic-only reconciliation, and the full audit’s report)
audit reconcile— computes a severity-max over EXPLICITLY DECLARED expected audit streams (shape A/B aware, i.e. 4eyes-only vs. 4eyes+Loki dispatch shapes). Hard-fails with no verdict if any declared expected input is absent — it never guesses. This subcommand performs arithmetic only; Prophet retains full reconciliation authority, the missing-output fallback, and escalation on any proven functional break.audit write --slug <s> --file <path>— lays down aprophet-full-auditreport at.divineswe/audits/<slug>.md, byte-identical to the file, and prints one JSON line{slug, audit_path}. The slug is the audit’s summon slug, held to the Avatar slug grammar. It refuses, writing nothing, a report missing a## Executive Summary,## Findingsor## Forge Promptsection or carrying one twice (headings inside code fences do not count), a Forge Prompt with no line that starts with the Avatar’s invocation in any platform’s spelling (/divineswe:avatar-forge,/avatar-forge,$avatar-forge— the skill is respelled per platform, the script is not) or reads(none), an unreadable file, an unknown flag, a.divineswe/auditsthat is a symlink rather than a directory, and a slug that already has a report: the file is linked into place, so an audit is written once and never overwritten (scripts/audit-report.mjs, spec forge-full-audit-summon M4, M5).
ownership domain (facade-gap closure)
ownership create— writes a.divineswe/ownership/{task-id}.jsonfile declaring the owned/frozen file lists for a dispatched Monkey, closing the gap that previously required hand-authored ownership JSON and making the CLAUDE.md “all.divineswe/state-file writes go throughdivineswe-state.mjs” iron law literally true (no more exception for ownership declarations).
avatar domain (THE AVATAR — the live seat, summoned ad hoc)
The Avatar is the door through which the model already in the conversation summons a pantheon seat AD HOC, outside a Book cycle — the nearest kin is Hermes’ ad-hoc carrier (skills/hermes-dispatch/SKILL.md), and the one thing that tells the two apart is that the Avatar DOES leave a record: a real scroll under a synthetic task id AV-<slug>-<nn>, and a decision-only ledger event per verb. It never touches status.json or session-state.json — nothing this domain writes may leave the Scry believing a Book cycle is in flight. The grammar is AV_TASK_ID_RE, one anchored constant read by both verbs (Prophecy 2’s avatar_scrolls Scry sensor reads the same constant rather than a second regex) — AV-hero-1 must never pass where AV-hero-01 is meant, and a payload smuggling a second id behind a legal one on its own line must never ride through.
The seat allow-list, AVATAR_SEATS (thoth | monkey | 4eyes | loki-break | loki-exploit | loki-doubt | occam | styx | mason), is its own frozen constant, declared here and nowhere borrowed — the obvious ROSTER_SEATS import from scripts/shared/dispatch-resolution.mjs is EXACTLY INVERTED for this door (it holds seats the Avatar must refuse — oracle, prophet, atlas, preacher — and lacks the loki forms the Avatar exists to dispatch; Occam is admitted only at its pass=audit position, over a Loki document). loki-break, loki-exploit and loki-doubt all resolve to dispatch seat loki, with form derived from the Avatar seat name; loki-doubt is oracle-envision’s one design-review pass over a draft spec, filed at AUDIT.
Two verbs, and both refuse from a DETECTED agent context through the shared detectSubagentContext / formatAttestationRefusal pair scripts/shared/duty-guards.mjs gives vow judge and decree advance --to done — a summoned seat that could itself summon would be the pantheon dispatching itself, which is the one recursion the roster forbids. Stated at the honest tier rather than overclaimed: the ambient DIVINESWE_AGENT marker is a DECLARED contract nothing in the harness sets today, so the detector reports no agent for a genuine subagent exactly as it does for a main session, the limitation binds every seat rather than this door alone, and no permission prompt or divineswe-init ask-rule covers either verb — what these two deliver is raised cost and a reconstructible record, never a mechanical impossibility.
avatar summon --seat <thoth|monkey|4eyes|loki-break|loki-exploit|loki-doubt|occam|styx|mason> --slug <s> --task "<text>" [--loki <path> [--foureyes <path>]] [--base <rev> | --scope <pathspec>] [--implementer <model>] [--model <codex/…|openai/…|claude/…>]—--model(spec forge-make-books-unnecessary M18–M21;claude/by spec forge-claude-realm) carries the summons through Hermes: it takes only acodex/,openai/orclaude/id (aclaude/id only for a read-only seat: 4eyes, loki-exploit, loki-doubt), is refused wherehermes-dispatch.mjsand itscodex/codex-agents/personas are not both installed besideavatar.mjs(the claude-code package carries them; the copilot, codex and opencode packages do not) and forstyxandmason, holds a named 4eyes to the reviewer floor (refused below it, never raised), and prints acarrierargv in place ofagent_type/form/pass— every refusal before any write.--implementeris 4eyes’ alone and required for it: the model that implemented the unit, against which the reviewer floor (clearsReviewerFloorinscripts/audit-dispatch.mjs, spec forge-make-books-unnecessary M24–M26) holds the resolved 4eyes — at or above the implementer’s family onMODEL_RANK(equal is enough; an unordered reviewer stands over an ordered implementer; a model off the ladder passes only as the implementer’s own) — raising a short reviewer to the implementer’s own model and refusing, before any write, a missing or malformed--implementerand a raise target that is foreign or off the ladder.--loki(required) and--foureyesare Occam’s alone, each a documentoutput writepersisted under.divineswe/outputs/;--baseis 4eyes’ and loki-exploit’s alone, anchors a NEW slug’s baseline at an ancestor of HEAD so committed work can be audited, and is refused on a slug that already has a scroll.--scopeis likewise 4eyes’ and loki-exploit’s alone and never rides beside--base: it names one git pathspec whose tracked files the seat audits in full, with no diff range — the brief’s**Scope:**line carries it — so a clean tree can be audited; a pathspec that matches no tracked file or that git cannot read, and an empty, option-shaped, whitespace-padded, control-byte or comment-delimiter (<!--,-->) value, are refused before any write, so the brief names exactly the pathspec the guard matched. It mints the nextAV-<slug>-<nn>id (an exclusiveO_CREAT|O_EXCLcreate against the scrolls directory, retried onEEXIST, so two summons racing one slug never let the second clobber the first’s live scroll), creates the scroll viacreateScroll(scripts/scroll.mjs), resolves a summons that names no--modelthroughresolveDispatch/canonicalSeat(scripts/shared/dispatch-resolution.mjs) — a--modelsummons skips that lookup and takes its tier’s default effort — and compiles the summoned seat’s brief by spawningscripts/compile-prompt.mjs briefwith the descriptor on stdin. Emits exactly onedecisionledger event and prints one JSON line carryingtask_id,brief_path,agent_type,form(loki) orpass(occam),phase,model,effortand, when a foreign route was swapped for its native backup,replaced_route, and when the reviewer floor raised 4eyes,reviewer_raised(the model it replaced) — the caller’s one contract for what to dispatch and where its brief lives.avatar close --task-id AV-<id> --outcome <completed|failed|abandoned>— the terminal write:updateScrollStatus(scripts/scroll.mjs) sets the scroll toCOMPLETEDfor acompletedoutcome andCLOSEDforfailedorabandoned, then emits one completiondecisionledger event. Likesummon, it refuses from a detected agent context before writing anything.
vow domain (the Vows — the obligation register)
The Vows are DivineSWE’s memory of owed work. Unlike every other domain above, the register is meant to be committed: by default, .divineswe/vows.jsonl is an append-only JSONL event log carrying merge=union in .gitattributes; vows_dir may place the same fixed-name register in another dwelling. The state a reader sees is the fold of those events, never the file. That is what lets two worktree agents append concurrently and lets git concatenate their lines in any interleaving without a conflict and without a wrong answer — the fold is ordered by a pinned total order (ts → per-writer monotonic seq → writer id), so it is a function of the event SET and never of the line order. The domain writes; scripts/shared/vow-store.mjs decides what the events MEAN, and it is the only place that decides.
This section is the VERB REFERENCE, field by field. A consumer driving the register from their own repository — enabling the gates, backfilling what they already owe, migrating off the retired home follow-up ledger, rendering a judgment, reading the --json surface — is served by vows-consumer-contract.md, whose every shell recipe is executed against this CLI by tests/integration/consumer-contract-wiring.test.ts. That doc is a contract, not a tutorial: a recipe it publishes and the binary refuses is a test failure.
Whole-domain rules, stated once:
- Per-subcommand flag allow-lists. Every
vowverb rejects an unregistered flag before it resolves any path, exactly as thebookdomain does — a mistyped--base-pathmust never append one project’s debts to another project’s register. - AJV at the write seam. Every event is minted and validated against
schemas/vow.schema.jsonbefore any byte is appended, so no line reaches the register unless the whole batch was mintable and schema-valid. The I/O cannot promise as much —appendFileSyncdelegates to awriteSyncloop, which can leave a prefix on disk under ENOSPC/EDQUOT/EIO or a signal — so a batch of more than one transition stamps itself with a sharedbatch_idand a declaredbatch_len, and the fold checks what arrived against what was promised. It checks the set, not the count:batch_idis${writer}:${seqBase}, so the batch’s members are exactly the seq valuesseqBase … seqBase+batch_len-1, and verifying that contiguous range is what makes a duplicated line unable to stand in for a member that never landed. Counting entries was defeatable with no forgery at all — two byte-identical copies of the one surviving line of a 1-of-3 batch pushed the count to 3 and the PARTIAL warning vanished, which a concatenative recovery or a re-applied rebase hunk over amerge=unionfile reaches by accident. A half-applied attestation would otherwise fold perfectly cleanly: the vows that got their line reading as judged, the rest reading as never judged, with no diagnostic at all. - The append never glues. Before writing, the seam checks that the register ends in a newline and inserts one if it does not. JSONL has no framing beyond that byte, so a register left torn — by an interrupted append, an editor, or a checkout of a file committed with
\ No newline at end of file— used to swallow the next transition whole: the new event was concatenated onto the residue, the tolerant reader correctly refused to launder two fragments into one event and skipped BOTH, and the CLI reported success. Exactly one obligation was lost per tear. - Every verb reports the register’s condition. The fold’s diagnostics — a torn line, an orphan transition, a short batch, a transition suppressed by terminal-transition-wins — go to stderr on every verb, reads and writes alike. stdout carries the answer and stays machine-parseable. A write against a corrupt register must never print a bare success line.
- A three-sided evidence gate. A
closed/waivedtransition needs an evidence pointer, and a placeholder (n/a,-,tbd) is not one. The write gate is bypassable by construction — a union merge or a text editor can put any line into an append-only log — so the gate runs AGAIN on every read. Evidence does not inherit: the gate weighs the pointer carried by THE terminal transition, never the vow’s accumulated one. An earlier non-terminal event may legitimately carry evidence (ajudgedrendering a REJECTION cites its proof and leaves the debt owed), and if that pointer stayed folded, the next evidence-less close would inherit it and satisfy the gate built to refuse exactly that line. - Three status spellings, one meaning each.
declared_statusis what the register was TOLD — the LAST status any event ASSERTED in the pinned order, whether or not the fold applied it, so a declaration terminal-transition-wins REFUSED is still findable through--declared-status;effective_status(and itseffectiveStatuscamelCase twin) is what it BELIEVES, with the read gate applied;statusis the safe-by-default spelling — the declaration on the raw fold, the belief on thevow list --jsonprojection. Both survive into that projection, and that is the point: an unbacked close readingdeclared_status: "closed"besideeffective_status: "open"says somebody moved to dismiss this debt and showed nothing. Publishing only the belief would erase the very signal the gate exists to raise, leaving an attempted dismissal indistinguishable from a debt nobody ever touched. Takingdeclared_statusoff the SURVIVING status would do the same thing one level down: it would report only the declarations that happened to be applied, and leave the documented audit filter blind to exactly the class the guard creates. - Terminal-transition-wins guards more than
status. A field does not have to be spelledstatusto be status-equivalent.evidenceis what the gate weighs,verdictis what a human reads instead of the status, anddue/snoozed_until/snoozed_rationaleare the levers that take a debt off the overdue list and out of the ambient count. Inside the SAME guard sit the two PROVENANCE fields,actorandnote— not because they decide whether a debt is owed, but because they say who decided and what they said about it. Left outside it as descriptive prose, a secondvow judgeon a settled vow had its verdict and evidence correctly suppressed while it silently relabelled who the register says rendered the judgment that took effect: the A1 forensic record erased at exit 0 by the party it exists to catch. They all move together inside the guard or not at all — a row reportingverdict: rejectedbesidestatus: closedis the register contradicting itself, and a discharged vow whose deadline or judge was rewritten afterwards is a discharge nobody can audit. A suppressed transition is reported, never dropped, and the warning names EVERY field it suppressed: a warning naming a subset is a false reassurance about the rest. - The attestation does not inherit either, and a second discharge SUPERSEDES rather than bounces. The guard above fires on a later NON-terminal event. A later TERMINAL one is let through on purpose: last-terminal-wins is the only post-discharge correction path — an operator who closed a vow on a wrong remark fixes it by re-running
vow closewith the corrected--evidence/--note, and refusing the second discharge would take that away. What a discharge may never do is publish somebody else’s attestation as its own.closeandwaivedeclare noverdictand have no--actorflag at all, so with onlyevidencereplace-or-deleted, the documented rework loop —vow judge --verdict rejected, thenvow close— folded to a row readingstatus: closedbesideverdict: rejected, carrying the rejection’s actor and note over the close’s evidence, with no adversary and no warning; and onevow waiveover a settled vow rewrote the discharge while attributing it to the operator who had judged it. So the whole ATTESTATION —evidence,verdict,actor,note— is REPLACED from the terminal transition and CLEARED where it declares none, the same ruleevidencealways had. The superseded attestation is never lost: it stands on the append-only log, the supersession is NAMED on every read with the fields it moved, and the write verb says a second terminal transition superseded the first instead of the flatly false “it moved nothing” it used to print while replacing four of them. - A field any gate READS is not descriptive. The rework-close witness (
assertReworkCloseWitness) readsrework_ofoff the fold to decide whether the rework-close witness runs at ALL, andreworkAnchorBookreadsdeliverableFIRST to decide which Book’s seal may discharge the debt — while both, plusrejection_evidence, sat on the DESCRIPTIVE roster under a docblock saying they “decide nothing about whether a debt is owed”. True when it was written, false the moment the gate landed: one appendedupdatedline rewritingdeliverableto an earlier archived Book turned a REFUSED close into a successful one, and"rework_of": ""disabled the witness outright so--evidence "trust me it is done"discharged the debt (both reproduced, AUDIT round 2). This is the exact mirror of the round-1seal_shafinding one roster over, and the rule the two remediations jointly imply is the heading. All three are DERIVED BY THE SPAWN —vow judge --verdict rejectedcomputes them from the rejection it is recording, and the caller can neither forget nor choose them — so a later DIVERGENT declaration is a forgery by definition: they are write-once, refused, named, and recorded on the row as a contest, and they are additionally on the SUPPRESSIBLE roster so a post-terminal declaration is NAMED (a restatement of the identical value is a no-op and is not named, because a warning about a line the register never had to hold is noise).reworkstays descriptive: it is a pointer TO the spawned debt and no gate reads it. - Identity and host are write-once.
item,kind,bookandprophecyARE the id (they are the tuplevowIdhashes), andowner/sourcesit just outside it. The first event to declare one establishes it; a divergent later declaration is refused and named. Without that, one correction leftvowId(row) !== row.idand the A23 TABLETS reconcile — which re-derives ids from archived text — would open a duplicate of that vow on every pass, forever; and a secondopennaming a different host produced one hybrid row carrying the agent’s name over the operator’s deadline. Write-once resolves that collision in favour of whoever wrote FIRST, which is arbitrary with respect to who actually owes the work — so the refusal is also carried ON the row ascontested: [{ field, refused }].vow list --ownerreads it, so a disputed debt appears in BOTH hosts’ inboxes rather than in neither, andvow surfaceflags the rowCONTESTEDuntil a human settles it. - Gated by
vows_enabled(default TRUE, PROJECT-ONLY dead switch). A closed gate makes writes a NO-OP (exit 0, zero bytes, an existing register left byte-identical) — loudly: the discarded transition is named on stderr with thevows_suppressed_by_gatetoken, on the same principle the read path applies, because “recorded” and “silently discarded” must never be byte-identical answers either. Reads are count-suppressed rather than empty — see Configuration. An argv mistake stays exit 1 in BOTH gate states; the gate suppresses the write, never the diagnosis.
The verbs:
-
vow open— records a new obligation:--item <text> --kind <deferral|post_seal_step|precondition|validation|backlog> --owner <operator|agent> --source <seal|retrospective|consult|operator|backfill|audit|rca>, plus optional--book,--prophecy,--trigger,--precondition,--due,--priority,--if-absent.--priorityis how soon the debt should be paid: a whole number from 0 (the highest priority) to 10000 (the lowest). Anything else is refused at exit 1 and nothing is appended, and a vow opened without it is unprioritized. The id is DERIVED from the normalized tuple[book, prophecy, kind, item], so re-running a seal across a crash window folds to ONE vow, and a Book task line and its checkbox-ticked TABLETS-archived form derive the SAME id. Prophecy scope is part of the tuple on purpose: two prophecies of one Book may defer identical wording for two different hosts, and a Book-scoped key would collapse them and lose one in silence.owneris deliberately NOT in the tuple — an archived Book line carries no host, so an owner-bearing id could not be re-derived from the A23 fallback ledger at all. Prints the assigned id. Because the id is derived, anopenmay RESOLVE to a vow that already exists; it never looks like creating one. Against a still-owed vow it says so on stderr (ADOPTED the existing vow …), and against a discharged one it REFUSES by name: anopenedevent is not a terminal transition, so terminal-transition-wins makes it inert, and recording one would report success while the re-incurred debt quietly stayed settled andvow surfacetold every downstream agent “nothing owed”. A genuinely re-incurred debt is recorded as a distinct obligation — a--prophecy/--bookscope naming the occasion, or wording that describes the new instance.--if-absentis the REPLAY contract, and it exists because those are two different callers. An operator typingvow openis ASSERTING a debt: if the register will not record it, that invocation FAILED and exits 1. A replay — a re-run seal, the A23 TABLETS reconcile walking an archived Book — is asking whether the obligation is on the register at all, and a settled one is a legitimate no-op for it; answering that with exit 1 aborts aset -eloop at the first discharged line and leaves every REMAINING obligation unreconciled, which is the same missing-debt failure wearing a loud exit code. With--if-absentan existing tuple is never fatal and never appended against (so a replay does not grow the register by one inert line per pass), and stdout says WHICH case it was —vow open skipped (already discharged|already open): <id>besidevow opened: <id>— so a reconcile can tell a skip from a failure without parsing a message. -
vow update— appends a correction or a status move (--id, plus--status in_progress,--due,--priority,--trigger,--precondition,--note). It APPENDS; the opening line survives byte-identical, because the fold — not a rewrite — carries the state. An unknown id is refused. Two refusals are deliberate and both name their remedy:--itemis registered but refused, because the wording IS the identity and a vow re-worded in place would keep an id hashing text it no longer carries (close it with evidence and open the corrected wording as its own obligation); and moving an existing--duerequires--note, because pushing a deadline out clears an OVERDUE flag with no evidence and no stated cause, which is the snooze rationale gate walked around through a differently-named door.--prioritysets or changes a priority (0 the highest, 10000 the lowest; the latest change wins) and needs no--note: a priority ranks a debt and hides none. -
vow close/vow waive— discharge a vow:--id <id> --evidence <pointer>.closeis “done”,waiveis “deliberately dropped, on the record”. Both are TERMINAL: once a vow is settled, only another terminal transition ordering later may move it, so a clock-skewedopenedor a forgedupdatedis inert against it. -
vow judge— the ATTESTATION chokepoint:(--id <id>[,<id>…] | --book <N>) --verdict <validated|rejected> --evidence <pointer>. Exactly one selector;--bookjudges that Book’s still-open validation vows and never reaches another Book’s. The id list is COMMA-SEPARATED rather than a repeated--idbecause the facade’s argv parser keeps only the last value of a repeated flag, and a multi-id verb that judged one vow while reporting success would forge a verdict for the rest. Avalidatedverdict is an evidence-backed close; arejectedone leaves the vow owed.--bookis a mass discharge whose blast radius is invisible in the argv, so the resolved subject list is printed on stderr, by id, before anything is appended. Every judge event records provenance (actor): the OS identity the process runs as, or whatever--actornames. That is a record, not a proof — a local process can claim anything, and the permission prompt is the chokepoint (L3) — but “agents never render judgment” is only enforceable if the record says who did, and only auditable after the fact for the same reason. The per-process nonce it replaced had identical shape whether a human or an agent invoked the verb, which left the forensic fallback empty at exactly the seam that most needs one. -
vow snooze—--id <id> --until <ISO> --rationale <text>. A deferral with an expiry, not a disappearance: the rationale is REQUIRED in substance (weighed against the same placeholder list asevidence, so--rationale n/ais not a one-flag deferral) and it lands in its OWN guarded field,snoozed_rationale, never in the sharednote. Borrowed, the stated cause sat in a slot any latervow update --note .could overwrite with a single period — no gate, no evidence, no second rationale — leaving the vow out of sight until its window elapsed with nothing on the record about why. The gate is on the FIELD and the field moves with the window it explains. The vow leaves the ambient open count and is reported on its own snoozed line, and it returns by itself when the instant passes — auto-unsnooze is the absence of any state change. -
vow list— the SOLE supported machine projection:--jsonplus the fixed filters--owner,--book,--kind,--status,--declared-status, which AND together and reject an out-of-enum value loudly (a filter value that merely matched nothing would report a clean empty register — the most dangerous wrong answer this surface can give). Rows are the folded vows with the read-side evidence gate already applied, newest first. A prioritized row carriespriority(andP<n>beside the id on the prose row), but the order stays newest first: onlyvow surfaceranks by priority.--statusfilters on what the register BELIEVES and--declared-statuson what it was TOLD, matching the two names on the row exactly — which is what makes an unbacked close findable rather than merely visible: it answers--declared-status closed(somebody said so) and--status open(the register does not believe them). Without--jsonthe output is the plainN vow(s)listing, and a closed gate does not change that: the suppression notice follows the requested FORMAT, because a caller whose output shape is switched by a config flag it cannot see has its parse broken by the very warning meant to protect it. -
vow reconcile— the MUTATION verb, and the counterpart to the pure-readscrydomain below (Loki-Doubt L4):[--deep] [--json] [--cheap]. One pass STAMPS, CLOSES, HEALS and ADVANCES, and every one of those decisions is made byscripts/shared/vow-reconcile.mjsrather than by this handler — because the pass has four call sites (vow reconcile,scry --heal, the cheap always-runs variant at prophet-fulfill INIT, and session start under L9) andscrymust REPORT exactly what this PERFORMS. A planner living inside a facade handler is a planner each of those four re-derives, and the day two of them disagree is the day the report contradicts the inbox. So the module splitsplanReconcile(decides, writes nothing, reads no clock, touches no disk) fromapplyReconcile(the only write seam), and the plan is JSON-round-trippable soscry --jsonpublishes it verbatim. The write itself still goes through the register’s ONE seam, so this verb inherits the symlink guards, the AJV gate, the batch stamping and the torn-register newline repair rather than growing a second register writer. -
vow surface— the human/plan-time projection of what is still owed. It is ranked by priority: 0 first, each prioritized row carryingP<n>beside its id, and every unprioritized vow after the prioritized ones, in the projection’s newest-first order. A priority the register holds that is not a whole number from 0 to 10000 — a hand-edited line the facade never minted — ranks and renders as no priority, so it can neither jump the queue nor put its text on the row. Every untrusted field goes through the ONE shared neutralizer (scripts/shared/neutralize-text.mjs), so a forged heading or an<!-- … -->marker inside an operator’s own wording cannot inject structure into a consultation — and then through the surface’s OWN record grammar. The shared neutralizer defends the markdown document and knows nothing of the delimiters this renderer chose; a field is untrusted with respect to every grammar it lands in, not only the one somebody else wrote. Without that second pass anitemreadingShip it | due: 2099-12-31 | OVERDUEforged a deadline the register does not hold and an OVERDUE flag contradicting the header printed one line above it, on the plan-time projection every downstream agent reads. The second pass is structural, not lexical, and that distinction is the whole of it. A denylist of label SPELLINGS closes the spellings somebody already thought of and nothing else:due:was escaped five ways through the documentedvow open --itemalone — a zero-width space inside the label, a fullwidth|, a case variant, a space before the colon, a soft hyphen — and word-joining the ones it DID catch changed nothing for a reader, because U+2060 is zero-width and the result renders glyph-identical to the attack. What makes a label a label is the SEPARATOR in front of it, so the separator is what is removed: fields are NFKC-normalized (folding the compatibility lookalikes onto their ASCII originals), stripped of every Cf/format character and soft hyphen, collapsed on every line-adjacent control (VT and FF too, not merely CR/LF — both break a line on a TTY), stripped of the vertical-bar class, and then rendered inside a QUARANTINE span from which the quote character has itself been removed. Inside that span a forged row prefix (] B91 (validation/agent)) or a forged header (The Vows — 0 owed) is unambiguously content, and no sequence of field bytes leaves the span. The row’s own tokens are allowlisted rather than neutralized: the id through a character allowlist (on a folded row it is taken VERBATIM off the register line, not re-derived, so a hand edit or a union merge of a hostile branch forges the delimiters through the one component no CLI path mints),bookthrough a finite-integer coercion, andkind/ownerthrough their enums.item,triggerandpreconditionare INERT PROSE: never evaluated, interpolated, executed, compiled to a regex, or used as a lookup key — a trigger that looks like a shell command is a matching cue for a human and nothing else. -
vow migrate— THE SUPERSESSION (amendment A12). A one-time CONSUMER operation that carries the home-scoped follow-up ledger (~/.divineswe/follow-ups-<project>.jsonl, the deferred-debt register this release RETIRES) into the configured Vows register (by default,.divineswe/vows.jsonl), then stamps the home file superseded — never deleted. One flag,--json. The documented mapping:open→open;closedwith an evidence pointer →closed, pointer carried verbatim;supersededwith a pointer →waived; and an unbackedclosed/superseded→open, named on the report. The unbacked cases are the point: the retiring register’s own read gate already resurfaced an unbacked dismissal as open, so trusting the declared status here would LAUNDER a hand-written dismissal into a permanently discharged vow — a debt the source still surfaced becoming one no surface ever shows again, with the migration as the step that did it. Every migrated debt is recorded as adeferral(that ledger held nothing else) from sourcebackfill(an obligation recorded retroactively off an outside record), owned byagentunless the row declared otherwise, withitemandtriggercarried BYTE-FOR-BYTE — neutralization is a render concern, and a store that rewrote them would have changed what the obligation says. Four properties make it safe to run twice, or to interrupt: the destination is the only authority (what to mint is decided against the register’s own events, never against the presence of a stamp, so a home file carrying a stamp AND unmigrated rows is migrated rather than greeted as finished); the vows are written first and the stamp last (vows-then-crash converges on re-run, stamp-then-crash would not); an unreadable source reports UNKNOWN, exits non-zero, and is never stamped (stamping a file nobody read declares a migration that never happened and hides every debt behind the watermark); and the consumed register’s NAME is recorded on every migrated event asorigin_ref, because the home register is keyed on the project DIRECTORY NAME and a worktree, renamed clone or second checkout would otherwise re-derive a different name, find nothing, and report clean forever. A NAME, never a path (ruling P7-E): this register is COMMITTED andmerge=union, so a path recorded in it is attacker-supplied input to a read on every clone — and an absolute$HOMEpath is unresolvable on every machine but the one that wrote it.vows_enabledoff SUPPRESSES the whole pass — migrating into a register that discards every transition and then retiring the source is the two-write operation completing exactly the half that destroys.retrospective_enabledoff does NOT block the read (a closed gate must never make a live obligation look like an empty file) but WITHHOLDS the stamp and names itself: the pass may not declare a rite retired while the rite is switched off. -
After the migration, the vow read surfaces still watch the file.
vow surfaceandvow list(prose and--json, where it arrives ashome_residueplus ahome_follow_up_residuetoken inwarnings[]) scan the superseded home register on each read and name, LOUDLY, every entry it holds that this repository’s register does not (L14).scrydoes not — it never reads the vow register. The scan is computed ABOVE both gates, because the home read is ungated by ruling P7-A constraint 1 and putting its RESULT behind the destination’s gate would reinstate the suppression one layer up. What it scans is the UNION of the register the convention derives today and every register in the READER’S OWN~/.divineswe/whose NAME a past migration recorded — matched against a listing of that directory, never joined onto a root and never opened by path, so nothing in the committed register can point a read anywhere and a pointer written on another machine still resolves under the reader’s home. A recorded name with no register of that name here is stated on the migration report and changes neither the status nor the exit code. At most 25 registers are opened per read, and a bounded read says how many it did not open. That window is real: a lagging plugin elsewhere on the machine still appends to a file this repository has stopped writing to, and the file is deliberately not deleted. The scan is a raw-line set difference anchored on the DESTINATION, and every cheaper reading was tried and refused — a follow-up row carries no timestamp, so “entries newer than the stamp” has no field to stand on; a positional watermark is defeated by PREPENDING a row above the stamp; a tuple-keyed diff is defeated by reusing a migrated row’s label with a different trigger (a different obligation, coming due at a different time), which is exactly what the retiring reader’s own dedup makes invisible; and anything keyed on the stamp’s claimed time, or on the file’s mtime, is switched off forever by one line in a file every stale writer can edit. An UNMIGRATED repository gets the same surface with every home entry on it, so the retarget can never bury the debts the retirement exists to carry.
vow reconcile, clause by clause. The verb above is ONE pass — it stamps, closes, heals and advances together, because a second pass is a second chance to skew — and every clause below is a Loki-Doubt disposition rather than a convenience:
- The stamp (L5). The first post-commit pass records the seal commit sha — the commit that INTRODUCED the Book’s TABLETS line run — onto the Book’s
post_seal_stepvow asseal_sha. Deliberately not HEAD, and deliberately not the merge commit: a--no-ffmerge re-introduces the same content to the trunk, so a first-parent walk names the merge and every later unrelated commit looks equally plausible to a HEAD-stamping implementation. The walk is git’s own path-scoped history (which skips a merge TREESAME to the parent it took the content from) and every candidate is then verified by content, because the BINDING between the sha and the Book’s archived lines is what makes the sha evidence at all. A vow already carrying that sha is left alone: the cheap variant runs at every INIT and every session start, and a re-stamp per run turns the register into a log of its own heartbeat. - The close has TWO arms (ruling P5-H). L5 read literally (“anything weaker closes as approximate”) auto-closes every push vow the moment the network drops, which is the exact opposite of A9. Ruling P5-A answered that with THREE arms, the middle one closing on a remote-tracking ref of the configured push remote; ruling P5-H, amending P5-A and L5 after the Prophecy-5 AUDIT round 2, deleted that middle arm outright. A local tracking ref is a LOCAL FILE any process can write, so it is never affirmative observation of a remote — which is what P5-A required of
approximate— and round 2 reproduced the discharge with one command:git remote add origin /tmp/nope.gitplus onegit update-ref refs/remotes/origin/anything HEADsatisfied all three round-1 admissibility conditions at once, because the push target is SELF-SELECTED by the sensor, “a configured remote carrying a URL” is satisfied by a URL pointing nowhere (which is that arm’s OWN precondition, since it only runs when the remote will not answer), and the third check is a directory name. Examined honestly no sound source remained for the arm: anls-remotetip equal to the stamped sha is VERIFIED, and anls-remoteanswer whose containment is undecidable is UNKNOWN. So:proven— P5-H’s verified arm — is anls-remoteobservation that a ref CONTAINS the stamped sha (tip-equals or provably-descends), and it is the ONLY strength a discharge may be MINTED with.unknownis everything else and closes nothing: an unreachable remote is not a discharge, and neither is the genuinely undecidable case Loki named —ls-remotereturns ref tips, not histories, so when the tip is a commit this clone has never fetched, containment cannot be decided without a fetch L4 forbids, and both confident answers (“closed” and “not pushed”) are guesses. Local tracking refs survive as a HINT: they are genuinely informative to an operator whose remote is down, they ride theunknownwarning as the local files they are, and because they decide nothing they are owed no admissibility test — the wholeisConfiguredRemote/ namespace-binding apparatus went with the arm rather than staying on as decoration.approximatestays READABLE and no longer MINTABLE: rows a previous release discharged with it exist on real registers, soevidence_strengthstill rides the event and the fold, andvow list’s prose row still PRINTS the word beside the status — the default human read site — so a graded historical close is never byte-identical to a proven one. The label is deliberately NOT claimed of every surface:vow surfaceand the Scry inbox filter TERMINAL rows out entirely, and a mitigation described as ubiquitous is one a reader stops checking for.RECONCILE_EVIDENCE_STRENGTHSis DERIVED from the store’s field vocabulary by filtering rather than restated, so a rename of the persisted spelling empties it and every close fails closed. - A rewritten history closes nothing. History rewriting is not an exotic attack, it is Tuesday, and the orphaned commit survives as a dangling object
git cat-file -econfirms forever — on no branch, in no history, on no remote. So object EXISTENCE is not the predicate: a stamp whose content has MOVED isstamp_content_moved, and it is re-stamped and closes nothing that pass, even when a perfectly valid containment proof arrives in the same input, because that proof is about a commit the vow’s stamp no longer names. The same clause refuses a forged or misbound stamp: one pointing at an unrelated commit that happens to ride the remote satisfies “the remote contains this sha” perfectly, and only the binding refutes it.stamp_commit_missingis decided BEFOREstamp_content_moved, because a sha that resolves to nothing cannot be compared against anything;stamp_absentis not an invalidation at all — it is the ordinary first-pass state, which is what lets the first post-commit reconcile stamp AND close in one pass. - The horizon (L2), which may narrow but never silently — and never over a live debt. The default pass covers Books sealed at or after the moment
vows_enabledwas first observed open — derived from the register itself, whose first line could not exist before the gate was seen open — plus every Book that still owes something, whatever its date says. Every excluded Book is NAMED, in a warning carrying the literal--deep: a Book dropped without a name is a debt that no longer exists as far as any reader can tell. With no observation the default horizon considers nothing beyond the owed Books and says so with every skipped Book named — unknown ≠ zero AND unknown ≠ everything — and--deepstill reaches the full history, which is what the flag is FOR. The seal instant comes from the REGISTER (the earliestcreated_atamong a Book’s vows, i.e. the moment the seal emitted its chain vows) rather than from the commit date, because a second-resolution commit date compared against a millisecond-resolution register instant would drop a Book sealed and recorded inside one second out of its own horizon, non-deterministically, on a repository doing nothing unusual. The UNDATED class is what makes the flag real. Both auditors reproduced the horizon as INERT:observedAtis the minimum event instant over the WHOLE register and a Book’ssealed_atis a minimum over a SUBSET of it, sosealedMs >= sinceMsheld by arithmetic for every dated Book, while an undated one (sealed_at: null) was considered as well —gatedand--deeptherefore produced identicalconsideredsets,excludedwas unreachable, and the L9 always-runs pass sensed git over every archived Book in a repository’s history. THE OBLIGATION IS DECIDED BEFORE THE DATE (AUDIT round 2). The round-1 fix justified theundatedclass with “a Book with no register presence has no vow to stamp or close BY CONSTRUCTION” — a claim about the DATA, not about the world, and the data falsified it:sealed_atderives fromcreated_at, which the store sets only from an event carrying a parseablets, and the TOLERANT fold accepts an event WITHOUT one silently. A Book with real OPEN obligations therefore landed inundated, was dropped from every default pass, and the verb printedstill owed: 0under a warning asserting the register held no event for it and it had no vow to stamp — both false, and both reproduced. SobooksWithOpenObligations(one exported definition, measured on the BELIEVED status so a close the evidence gate refuses still counts as owed, and shared by the facade’s sensing decision and the planner’s own horizon call so the two cannot drift) is consulted FIRST: an owed Book is CONSIDERED, full stop. Both narrowing rules are cost optimisations, and a cost optimisation may never drop a live debt. What remains:horizon.undatedis a Book that owes NOTHING and cannot be dated — OUTSIDE the git-sensing half (there is genuinely nothing to stamp or close, now true by construction rather than by assertion) and INSIDE the archive heal (healScope= considered + undated), because a Book the register has lost is exactly the Book the archive still remembers;horizon.excludedis a Book that owes nothing and is provably pre-gate. Dropping either from the heal would break A23 recovery to save nothing, and--deepreaches all three classes. The summary’sstill owed:figure NAMES the scope it was counted over —still owed: N · M Book(s) not sensed this pass (nothing of theirs is owed; --deep to sense)— because a bare number over a set that narrowed is the same lie in a smaller frame. - The TABLETS heal, and the orphan it refuses to resolve. A debt the ARCHIVE still asserts in a Book’s
## Deferredblock and the register has lost is re-opened under the id re-derived from the archived line — which holds only because the identity mapping is shared with the seal path rather than restated (two copies of that mapping is exactly how a heal starts opening a duplicate of every debt it recovers). The archive block is read to the next## Book <n>, not to the next##: a real archived entry carries## Propheciesand## Book-Level AUDITheadings of its own and the## Deferredsection lives past both, so a reader terminating on the next##recovers nothing, silently. The converse case is an ORPHAN — a live debt the archive does not carry — and it is REPORTED, never closed and never deleted: the archive is the A23 fallback ledger, not the authority, and settling a disagreement by discarding the live obligation is the single failure this register exists to prevent. Orphan detection is scoped to the kinds the archive actually carries, so apost_seal_stepchain vow — emitted by the seal and archived as no Deferred line — is never reported as drift. An UNREADABLE archive is not an empty one. ABSENT is the ordinary state of a repository that has sealed nothing — and of one whose archive has moved — so it is SAID, never silent: the caller declarespresentbesidereadableandreason, the plan’sarchivefield carries{readable, reason, present, path, out_of_scope}with areasonnaming an absent archive, the summary’s second line names the archive path and whether it was there, and the owedpost_seal_stepvows an absent archive brings no Book in for are named inout_of_scopeand on the still-owed line rather than counted as 0 (spec forge-p1000-vows M1, M2, M4). UNREADABLE has to be DECLARED by the caller (readable: false), and when it is, the heal and orphan passes DO NOT RUN,heals.statusisunknownrather thanreported, and the summary says so in words. Without that distinction a truncated or half-checked-out TABLETS.md turned the whole verb into a byte-identical no-op reporting “still owed: 0” at exit 0 — the horizon considered nothing, every Book fell silently out of scope, andexcludedstayed empty because it only ever ranges over Books found IN the archive.heals.opened: []besideheals.status: "unknown"is the one shape that cannot be read as “nothing was owed”. AND THE SAME FOR THE THIRD SOURCE (Book-Level AUDIT). The pass reads THREE sources and declared the readability of two:listDecreeBriefsswallowed everyreaddirfailure into[], sochmod 000 docs/decreesproduced a report byte-identical to the readable run — still assertingarchive.readableandregister.readable, with no sibling for the Decree half at all — while the honest twin (readDirGuarded→unknownReading) shipped inscripts/scry.mjsin the same Book over the same directory. The plan now carriesdecrees: {readable, reason}beside the other two, the Decree half of the pass is NAMED as not-run rather than reported as converged, anddecree list/decree statusrender UNKNOWN with no count at all over a directory nobody could open —0 decree(s)at exit 0 with an empty stderr is the confident zero at the one surfaceoracle-consultreads to load standing Architect intent. AND THE SAME THREE STATES FOR THE AUTHORITY (AUDIT round 2). The doctrine above stopped one file short of the register the archive is only a FALLBACK for: with.divineswe/vows.jsonlpresent, readable and 100% unparseable,vow surfaceprinted “The Vows — 0 owed”,vow listprinted “0 vow(s)” andvow reconcileprinted “still owed: 0”, all at exit 0, with the damage on stderr alone — the channel a piped caller discards. That is A13 inverted, and it made the fallback ledger more honest than the authority inside the mutation verb.readVowRegisternow names three states: ABSENT/EMPTY is an honest zero; PARTIAL is a real number over an INCOMPLETE fold, so the torn-line count rides the STDOUT prose row rather than stderr; BLIND — non-empty and nothing survived the fold — renders UNKNOWN onvow surface, onvow list, onvow list --json(asblind: truebeside the reason) and onvow reconcile, which additionally declaresplan.register = {readable, reason}, refuses to heal (“the register does not carry this debt” is UNKNOWN when the register could not be read, and re-opening every archived debt off that would be an invented recovery) and writes nothing. - The A22 Decree advance, in the SAME pass.
autoAdvanceis the shared predicatedecree statusanddecree listalready report; here it is APPLIED, one rung at a time, and only to the two MECHANICAL rungs.doneis never proposed and is refused here a second time on its own account — it is an operator’s signature, and a guard resting on another module’s exhaustiveness is one commit from being wrong. The brief’s declared word is re-read immediately before the write, so a brief an operator advanced by hand mid-pass is not walked a second rung by a stale proposal. A malformed roll-up warns without halting (A23). --cheapis the L9 always-runs variant. It makes no network call — nols-remote, and it senses no local hints either — so the pass stays bounded and offline at prophet-fulfill INIT and at session start. It still stamps, heals and advances (the three that converge offline) and it closes nothing, which is the honest thing for a pass that never asked a remote anything.- Convergence and A23. Re-running is a NO-OP at exit 0: the second pass finds nothing owed, which is the property the four call sites depend on. An absent archive, a directory that is not a git repository, an unreachable remote, a malformed brief, a closed
decrees_enabledgate and a torn register all produce a plan plus warnings at exit 0 — a verb that failed when there was nothing to do would abort aset -eloop on a perfectly healthy repository. Under a closedvows_enabledthe pass is suppressed rather than empty, with thevows_suppressed_by_gatetoken, on the same principle every other verb in this domain applies.
decree domain (the Decrees — the initiative layer above the Books)
A Decree is a committed brief under decrees_dir (default docs/decrees/<slug>.md) declaring the Architect’s intent: an ordered Book sequence, the capabilities it buys, the acceptance criteria it will be judged against, and a lifecycle draft → active → delivered → done. Unlike .divineswe/ runtime state, and unlike a Book, a brief is meant to be committed — which is why decrees_dir and consults_dir are classed with specs_dir (repo-relative only) and deliberately not with the permissive books_dir: an out-of-tree brief cannot be committed, so every Decree: back-link on every Book in its sequence would point at a file no clone of the repository contains. Books are the deliberate exception (local-only working state); intent is not. Both keys are resolved by scripts/resolve-dirs.mjs and both are PROJECT-ONLY — directory layout is per-repository, so neither is declared in schemas/global-config.schema.json.
Whole-domain rules, stated once:
- The brief is the SINGLE source of truth. The brief’s ordered
books:sequence is what the roll-up counts. A Book file’sDecree:line and a spec’sdecree:frontmatter are warn-level back-links: a missing one may never subtract a Book from the roll-up, and a present one may never add one. Both directions matter — if a Book could enrol itself by writing a line, the denominator every progress figure divides by would be editable from inside the work being measured; and a forgotten prose line would otherwise silently shrink a Decree. - The lifecycle word is DERIVED, never echoed. A brief is a hand-editable markdown file whose frontmatter carries a lifecycle word, and the roll-up is the only thing that can call that word a lie. So
decree status --jsonpublishes both:statusis what the FILE declares andderived_statusis what the sequence plus the vow register make of it. A hand-typeddeliveredover an unsealed sequence derivesactive; a hand-typeddonewith no substantive evidence pointer derivesdeliveredand the contradiction is named asdecree_status_contradicted:<slug>. Publishing only the derived value would erase the audit signal; publishing only the declaration would launder a forgery. A malformed brief is not re-derived at all — with no trustworthy sequence there is nothing to compute — and it is floored atdraft, never echoed: echoing the declared word madeMALFORMEDan attacker-controlled OFF SWITCH for the whole ladder, because one junk sequence line flipped a hand-typeddoneover an unwritten Book fromdraft / unjudged / contradicted:truetodone / judged / contradicted:falsewith an empty reason. Degrading a brief must never be cheaper than satisfying the ladder, so a defect may cost a Decree a rung and may never buy it one, and the suppression is named asdecree_ladder_suppressed_by_defect:<slug>rather than rendered as an ordinary malformed notice. doneis NEVER automatic (amendment A22). It means “a human judged this against the acceptance criteria”, so no input combination makes the machinery propose it —autoAdvancecannot return it, and a mechanically fully-sealed Decree stops atdelivered. The reverse also holds: machinery neither writesdonenor tears one up. A rejection landing on an operator-attesteddonechanges nothing and is reported, because a contradiction nobody is told about is worse than either state.delivered≠ criteria met (Loki-Doubt L12).deliveredsays the Books shipped. Every surface therefore reportscriteria: unjudgeduntil a judgment exists, and the vocabulary is deliberate: “canon coverage: complete / criteria: unjudged”, never “gap empty” as though an empty gap meant the criteria held. Rendering “criteria met” off a mechanical Book-completion count would assert a judgment no human made, which is the whole failure the Judgment layer exists to prevent.criteria: judgedis a statement about a HUMAN ACT, so three things independently refuse it: a derived status that is notdone, a malformed brief (the tool refuses a verdict about it at all), and zero acceptance criteria — the schema callsacceptanceoptional because the rows are “required in SUBSTANCE beforedone, by the judgment against them rather than by the shape”, and nothing enforced the substance half, so a brief withacceptance: []printed “an operator attested this Decree against its acceptance criteria” about rows that do not exist (decree_judgment_without_criteria:<slug>). The projection is binary and lives in ONE place in the pure module (judgeAcceptanceCriteria); a facade that answered the same question independently answered it by never asking.plannedentries are first class, and excluded in BOTH directions (Loki-Doubt L11). An anticipated, unnumbered Book is a legitimate member of the sequence (- planned: <label>in the frontmatter,planned: <label>in the--bookscsv). It may never block delivery — one anticipated Book would otherwise keep a finished initiativeactiveforever and the operator would learn to ignore the lifecycle — and it may never trigger it. The delivered predicate therefore requires at least one sequenced non-planned entry BEFORE it requires all of them sealed: “all non-planned sealed” is vacuously true over an empty or all-plannedsequence, and a brief that is pure anticipation would auto-deliver having shipped precisely nothing. The residue is rendered asN planned, unbooked.- Unknown ≠ zero, and unknown ≠ sealed (amendment A21). A sequenced Book still in
books_dirisin_cycle(any task ticked) orunstarted; one whose## Book <n>marker appears in TABLETS.md issealed; one in neither place isunknown, andunknownblocks delivery. Left as “unsealed with no progress” it would hide a typo’d Book number behind a plausible roll-up forever, and computing delivery as “no unsealed Book found” would makerm book-99.mda way to deliver a Decree. A cross-repo entry (<repo>#<n>) isunknownfor the same reason, permanently. The TABLETS scan is line-anchored, word-bounded, restricted to the house archive-entry marker (TABLETS_ENTRY_HEADING—^## Book <n>, exactly two hashes, reused rather than re-spelled) and fence-aware. Every one of those four is load-bearing: a substring orstartsWithscan reads a longer Book number’s archive line as a shorter one’s prefix and reads prose like “supersedes the earlier Book in spirit only” as a seal; a wider#{1,6} Book <n>re-admits a deeper### Book <n>cross-reference inside another Book’s archived body; a barebook: <n>form is the spec-frontmatter spelling that TABLETS archives verbatim, so an archived Book quoting a spec manufactures a seal; and a fenced example heading is an illustration, not an archive. The fence tracker is CommonMark-correct rather than a single toggle, and this is the one sensor for which diverging from the houseisFenceBoundarypredicate is right: a bare boundary test flipping one boolean per match has no memory of the opening marker’s CHARACTER or LENGTH, so a NESTED four-backtick block — what an author must use to SHOW a three-backtick block — is balanced overall yet INVERTS the toggle across its middle and a## Book <n>inside the illustration readssealed. This scan therefore records the opening run and closes only on a same-character run at least as long with no info string, which treats strictly FEWER lines as boundaries than the house predicate does. The DENIAL direction is tokenised too: an archive whose fences never close makes every later entry readunknown(safe — unknown blocks delivery) whilebook next-numberstill counts them, sodecree_archive_fence_unbalanced:<run>names the parity as the reason the two sensors disagree about one file.sealedis the one input that can move a Decree todelivered, so over-reporting it MANUFACTURES a delivery — the only direction of error that is not benign. (book next-numberdeliberately counts fenced headings, because there under-counting regresses numbering onto an already-sealed Book. Opposite hazards, opposite asymmetry, one shared marker.) - Two observations that disagree resolve to the LESS advanced state, and the conflict is named. A duplicate observation must never be able to manufacture a seal.
- Multi-Decree membership is legal, warned, and attributed to BOTH. One Book may serve two initiatives. Refusing it would block a Book that genuinely does; hiding it would double-count shipped work in an aggregate. The collision is computed across briefs over normalized integer Book numbers — a join across the numeric, prose and abbreviated spellings of one Book finds no collision at all, and the mandated warning would never fire — and reported once per shared Book as
book_in_multiple_decrees:<n>. - Per-subcommand flag allow-lists, AJV at the write seam, and real containment.
DECREE_ALLOWED_FLAGSrejects an unregistered flag before any path is resolved (a mistyped--base-pathmust never scaffold committed intent into another repository), a scaffolded brief is validated againstschemas/decree.schema.jsonbefore a byte is written, and because this domain writes,scripts/resolve-dirs.mjsgoes beyondresolve-books-dir.mjs’s string-level guards: on top of../ NUL / percent-encoded traversal / shell-expansion refusals it also refuses a.gittarget, a symlinked final directory component, and a symlinked brief file — and, because none of those can see an intermediate symlink, it resolves the WHOLE chain withrealpath(falling back to the nearest existing ancestor, since the firstdecree createruns against an absent directory) and refuses a result that leaves the project root or enters.git. That guard is what stops a<root>/docssymlink from writing committed intent out of tree while the configured value is the untouched default. An absolute configured value skips the boundary assertion only — the operator named the location, and the resolver refuses what is DANGEROUS while the config schema refuses what is UNWISE — but never the.gitscan. The returned path is the LOGICAL one, never the realpath: a caller printing a realpath would name a location the operator did not configure. A resolver refusal propagates — falling back to the default directory would be the worst outcome, the operator’s configured location silently ignored and intent written somewhere they will never look. - Untrusted brief text is DATA.
--title,--problem,--solution,--constraints,--capabilities,--acceptanceand--evidenceare spliced into a frontmattered, committed document whosestatusfield the roll-up trusts, so every one of them goes through the ONE shared neutralizer (scripts/shared/neutralize-text.mjs). Without it a value carrying a newline emits attacker-chosen lines at column 0 inside the frontmatter — an injectedstatus: doneplusevidence:forges a fully-attesteddonefrom a single flag on a verb that deliberately has no--status. AJV is defence in depth here and cannot be the sole defence:ajvValidateDecreereturnsnulland SKIPS validation when the import is unavailable, which is a shipped platform target. One un-neutralized flag is the whole hole, so the list has no exceptions. Acceptance criteria are inert prose: never evaluated, interpolated, executed, or compiled to a regex. - Gated by
decrees_enabled(default FALSE since spec forge-make-books-unnecessary M15 deprecated the Decrees; PROJECT-ONLY, DEPENDENT onvows_enabled). A closed gate makes writes a NO-OP (exit 0, zero bytes, an existing brief byte-identical) and reads suppressed rather than empty — loudly, with thedecrees_suppressed_by_gatetoken on stderr, because “no intent” and “the layer was not read” are different facts and so are “recorded” and “silently discarded”. The suppression notice follows the requested format, so a caller reading prose is never handed a JSON envelope by a config flag it cannot see. The resolver’s ownwarnings[]are relayed rather than swallowed: an incoherent pair (decrees_enabled: trueovervows_enabled: false) reaches the operator asdependent_gate_without_substrate:decrees_enabled, because a silently incoherent Decree layer looks exactly like a healthy one. An argv mistake stays exit 1 in BOTH gate states.
The verbs:
decree create— scaffolds a brief:--slug <kebab> --title <text>, plus optional--problem,--solution,--constraints,--books <csv>,--capabilities <csv>,--acceptance <csv>. There is deliberately no--status: a Decree is borndraftand every later state is reached through the evidence-gated verb, so intent can never be created already-delivered with no evidence anywhere (A10 — scripted transitions, not a writable field). The slug becomes the brief’s filename, so it is validated as a path component before anything is written (../../TABLETSwould otherwise clobber the seal archive — the A23 fallback ledger — from a flag value). It refuses to clobber an existing brief, on theownership createprecedent: a Decree is the Architect’s intent, and overwriting it would destroy authored prose that lives nowhere else.books:,capabilities:andacceptance:are always emitted, as[]when empty — an ABSENT key and an explicitly empty one are different claims, and only the second is a statement somebody made, which is exactly why the reader refuses a danglingbooks:and acceptsbooks: []. Forcapabilitiesandacceptancethere is a legitimate third case the schema itself calls OPTIONAL — the key simply absent — and the reader keeps all three apart: absent reads as[],[]states “none”, and a dangling key is a truncated field and an error. The invariant binding the two readers of one brief is stated and pinned: AJV-valid ⟹ store-ok. One direction, deliberately — the schema is the committed file-format contract and the store is the tolerant reader (A23), so the reader may accept more than the format demands but must never refuse a document the format calls well-formed. What the schema requires and the reader tolerates (v, a non-emptytitle, a canonicalslug, the evidence gate on both achievement rungs) is reported as a named warning rather than swallowed. A duplicated frontmatter key is an error in its own right (decree_duplicate_frontmatter_key:<key>) and the lifecycle writer refuses it: a brief with twostatus:lines has no single lifecycle, and a reader and a writer anchored on different occurrences of it report transitions no reader can observe. That duplicate is also the one exception the AJV-valid ⟹ store-ok invariant carries, and the invariant is stated WITH it rather than above it: the two lines collapse to a single property before validation ever runs, so the document is one the format calls well-formed and the reader does refuse it. A deliberately-emptycapabilities: []— like an absent key, the same claim for the Judgment layer’s coverage join — is a named WARNING at any status pastdraft(decree_capabilities_vacuous:<status>) and never an error: silently legal, it made that join vacuously complete, “coverage: complete” over nothing at all.decree advance— the evidence-gated lifecycle write:--slug <kebab> --to <active|delivered|done> --evidence <pointer>(plus optional--note). Evidence is required in substance, weighed against the same placeholder list as a vow’s (n/a,-,tbdare not pointers) — the transition’s proof lives WITH the intent, in the committed artifact, because a lifecycle field that moved with no recoverable reason is a status nobody can audit later. It walks one rung at a time and refuses to skip (draft → donewould sign for work that was never delivered) and refuses to go backwards. It is idempotent: advancing to the status a Decree already holds exits 0, writes nothing, and saysalready— this verb is driven in a loop, and underset -ea non-zero exit on an already-satisfied transition would abort the loop and leave every remaining Decree unreconciled (the convergence lesson Prophecy 1 paid for withvow open --if-absent). An unknown slug is a refusal, never a scaffold: machinery syncs status and never creates intent, so autovivifying a brief for a typo’d slug is a prohibited creation path.--to doneadditionally refuses a brief with no acceptance criteria, on the same footing as the placeholder-evidence refusal:doneis the operator’s attestation that the acceptance criteria HOLD, and an attestation against an empty list is a signature on a blank page. The write touches ONE frontmatter field and the authored prose survives byte-identical. Both writers re-vet the target immediately before the bytes move, through one shared helper: the wholedecrees_dirchain is resolved AGAIN and must still name the same directory, because the previous leaf-only re-check was TAUTOLOGICAL for a direct child (assertRegularArtifactFilecompared one realpath against itself) and an ANCESTOR swapped mid-operation left that leaf a perfectly real directory. Honestly bounded: this NARROWS the window from “resolution to write” — which spans anawaiton a dynamic import — to “assertion to rename”, and does not close it, because userland Node has noopenat/dirfd write and cannot hold a directory handle across the write. It is an operator-correctness and corruption assertion, not a boundary against a concurrent writer already inside the tree.decree list— the brief enumeration:[--json] [--status <draft|active|delivered|done>]. The filter is fixed and rejects an out-of-enum value loudly (a filter value that merely matched nothing would report a clean empty set, which is the most dangerous wrong answer an intent register can give). It matches the declared status OR the derived one: this module’s whole thesis is that the declared word may be the lie the roll-up exposes, so a filter trusting only that word hid a genuinely active initiative fromdecree list --status active— the exact readskills/oracle-consultperforms to load standing Architect intent.listsenses the same worldstatusdoes. Two read verbs that publishderived_status/delivery_proven/criteriaand the state counts must not compute them against different worlds: gating the sensors tostatuswhile the A21 backfill ran unconditionally madelistreport a genuinely sealed Book asunknown, accuse an honest brief of contradiction, and print that a Book “reached NEITHER the Books directory nor TABLETS.md” about two directories it never opened.decree status— the mechanical roll-up:[--json] [--slug <kebab>]. PURE READ (Loki-Doubt L4): it reports the pending A22 advance —pending_advancein the machine projection, apending advance: <from> -> <to>line in the prose one — and never performs it. A read surface that edited committed intent as a side effect would be unrunnable from a plan-time context.--jsonpublishesrollUpDecree’s own shapes unrenamed (counts.{total,sequenced,planned,sealed,in_cycle,unstarted,unknown},entries[].{kind,book,planned,ref,state},malformed,errors[],warnings[]) plusderived_status,contradicted,contested,reason,activation_proven,delivery_proven,criteriaandpending_advance— no translation layer, because a translation layer is where the module and its only caller drift.reasonis the sentence that says WHY the derived status differs, and it is published rather than dropped: a warning whose entire job is to explain a contradiction and which ends in a bare colon has detected a forgery and discarded the finding. Per-rowwarnings[]carries EVERY engine’s stream — the roll-up’s, the derive engine’s, the criteria projection’s and the transition engine’s, de-duplicated by exact text. Relaying only the first swallowedautoAdvance’s whole array, and with itdecree_rejection_without_rework: a Decree demoted with nobody owing the fix, three lines under a comment promising “the missing link is reported, not swallowed”. The--jsonenvelope’swarnings[]carries the CROSS-CUTTING findings that belong to no single row — the relayed gate incoherence, the L11 membership join and the archive’s own fence-parity finding — on BOTH read verbs; hardcoded empty, it made an incoherent Decree layer byte-indistinguishable from a healthy one at the only machine surface, which is verbatim the harm the dead-switch resolver exists to end. The L11 join is computed over EVERY brief even under--slug, because a cross-brief predicate narrowed to one brief can never fire, and a narrowed query is exactly how an operator asks about a shared Book. A hostilebooks_dir— a key this reader only passes THROUGH as a sensor — is warned and degraded tounknownobservations rather than halting the read, matching the tolerance the same function already extends to an unreadable Book file. A malformed brief is the one place the spec asks for loudness over tolerance (A10): the notice names the path and every defect and the exit code is non-zero, and A21’s partial-honesty rule still holds — every readable Decree is published anyway, because a report that answers “nothing” because of one bad file is the confident-zero failure the whole obligation layer exists to end.decree gap— THE LOOP CLOSURE:[--json] [--slug <kebab>]. A PURE READ over BOTH registers that joins what a Decree DECLARED it would make exist (capabilities[]) against what the Canon currently DECLARES (docs/canon/<capability>.md, plus the committed event store the files derive from). It is capable of exactly one kind of lie — presenting the absence of a coverage gap as though it were the presence of a judgment — so the answer carries two fields with two vocabularies and never one:coverageiscomplete | partial | none | unknown, a statement about FILES the CLI may compute;criteriaisunjudged, always, because a criterion is judged by an operator against the brief’s acceptance rows and never derived from a capability count (L12). The strings “criteria met”, “acceptance criteria met” and “gap empty” are refused outright on both surfaces.noneis what a join with an EMPTY SIDE reports — a brief declaring no capabilities, or a Canon declaring none at all — because “every declared capability is covered” is vacuously true over nothing (decree_capabilities_vacuous); a de-canonized-only capability is uncovered, since a human’s refusal is a record of refusal and never coverage. UNKNOWN, NEVER ZERO AND NEVER CLEAN (LD-91-1): a torn capability file, an unreadable brief, an unreadable event store, a missing engine module and acanon_enabled: falsegate each yield anunknown(orsuppressed) read state, NULL per-tier counts and coverageunknown— an UNREADABLE capability is never flagged as UNCOVERED, because “the Canon does not declare this” sends an Architect and “we could not open the file” sends an operator. The arithmetic lives once, inscripts/shared/decree-gap.mjs(computeDecreeGap/formatDecreeGap/isGapClearedForDelivery/deliveryGapVerdict/draftChecklistFromDeltas/planCanonJudgment), which performs no I/O and consumes a tree already read bycanon.mjsreadCanonTree— one join, N projections. NAME THE TOOTH HONESTLY: the criteria half of the join is LEXICAL — one requirement-slug grammar shared with the parser — so a one-word negation reads as unrelated and a Canon that flatly CONTRADICTS a criterion still leaves capability coverage complete. That ceiling is measured intests/scripts/decree-gap-adversarial.test.tsGroup E and travels WITH the report asGAP_MECHANICAL_JOIN_NOTICE: the result is ADVISORY, and it is never claimed as enforcement. A23: no STATE reaches the exit code — an uncovered capability, an unknown, a suppression, a malformed brief and an absent Canon all exit 0; only an ARGV mistake refuses non-zero.
THE delivered PRECONDITION AUTHENTICATES BEFORE IT JOINS (B92-P4 AUDIT). Three answers govern whether the A22 gap-empty precondition on decree advance --to delivered runs at all, and they are resolved by ONE shared predicate — scripts/shared/dir-presence.mjs senseDirectory, which scry.mjs’s own directory reader consumes too, so the facade and the Scry cannot drift (LD-91-3). docs/canon/ genuinely ABSENT (ENOENT) is NOT APPLICABLE: the transition is ADVISORY-ungated and says so. docs/canon/ present as a REGULAR FILE, a SYMLINK, or unreadable for any other errno is UNKNOWN, and UNKNOWN BLOCKS — folding a non-absence into absence made the gate skip its precondition, print the factually false “this project declares no Canon at all” and deliver at exit 0 while decree gap and scry over the same state both said unknown. And the tree that IS read is authenticated against the committed event store by detectCanonDrift before any coverage is computed: the .md files are DERIVED (L7), so a hand-written capability file, a forged judgment= stamp, a derived file deleted under a live store, an ABSENT store and a store whose every line is a union-merge conflict all read canon_tree_drifted — the FIFTH answer — with tree: null, which BLOCKS. On the CLEARED path the transition now relays deliveryGapVerdict’s satisfied reason and GAP_MECHANICAL_JOIN_NOTICE: the caveat used to fire only when the join did nothing and stay silent when it certified.
AND IT AUTHENTICATES AUTHORITY, NOT ONLY STRUCTURE — AND THE ADVISORY BRANCH IS KEYED ON THE BRIEF (B92-P4 AUDIT round 2). Two CRITICALs survived the first remediation and both were one predicate at a chokepoint it had already opened.
validateCanonruns besidedetectCanonDriftat the one shared read path. Drift compares the.mdbytes against a re-fold of the event store — two plain files in ONE directory — so it proves the tree MATCHES the log and never that the log is REAL. Appending ONEjudgmentevent namingvow-92-neverexisted, then re-deriving the tree with the shipped renderer, mintedverified— the tier defined as “a human attested THESE SPECIFIC WORDS” — in a repository whose configured Vows register (by default,.divineswe/vows.jsonl) need not exist at all, while drift readcleanand delivery exited 0.vow judgeREFUSES that identical promotion from a detected agent context at exit 1; the appended line achieved it. The sensor was in the same module, exported, with its own CLI verb, and NO read path called it — the identical LD-91-3 shape as the CRITICAL the round before had closed. A tier the obligation register cannot account for now readscanon_tier_unauthenticated, the SIXTH answer, withtree: null, which BLOCKS. NOBODY IS EXEMPT, AND THE ONE EXEMPTION THERE WAS IS DELETED. For one roundsignCanonForJudgment— the verb that WRITES a tier — passed an opt-out, on the argument that gating the writer on the tier already being sound deadlocks the repair. Measurement condemned it (B92-P4 AUDIT round 3): it repaired the deadlock in NEITHER direction (decanonization-standsrefuses the reject→validate re-signature anyway, and the register SUPPRESSES the validate→reject one, so no signature is ever attempted), while its one measured effect was LAUNDERING — with a forgedjudgmentevent standing, ONE ordinary legitimatevow judge --verdict validatedsigned the forged section beside the honest one, RESTAMPED the forgery with a real vow id, and returned the repository tovalid: truewith delivery unblocked, silencing the detection control by an unrelated honest act and leaving no trace in the tree. That is LD-91-8 in its purest form, reached with no deadlock manufactured anywhere, because the exemption was UNCONDITIONAL. The repair path that DOES work needs no relaxation and is the one L17 already states: a NEW delta resets the section to the Apocrypha, from which a fresh signature earns verification. A23 still holds at the caller —vow judgenever fails on this, it reports loudly that it signed nothing.- THE AUTHENTICATOR CONSUMES THE REGISTER’S FOLD, NOT ITS BYTES.
readVowIdsused to iterate the raw event array and take last-judged-wins, which is not how the register reads itself — and that one line produced both failure directions. A single hand-appended{"type":"judged","id":"vow-92-forged","verdict":"validated"}, with noopenedevent, no evidence and no actor, authenticated averifiedtier atvalid: trueand cleared the A22 precondition, WHILEvow listover that same file reported the vowstatus: "open",never_opened: true, “its terminal status is REFUSED at read time” — two readers of one file disagreeing, and the AUTHENTICATING one was the permissive one. In the other direction a secondjudgedevent ordering after a vow’s discharge is SUPPRESSED by terminal-transition-wins, so the fold stays in exact agreement with the Canon’s stamps, and the raw-bytes reader saw the suppressed line and manufactured anunauthenticated-judgmentover a Canon nobody had touched. The index is now drawn fromfoldVows— the register’s OWN fold, the onevow list, the ambient counters and the plan-time surface all read — because a second fold is precisely how the two readers came to disagree, and the fix is not a better second fold but ONE fold.never_openedrows are held apart rather than dropped, so “this register has no such vow” and “this register was told of a transition for an obligation it was never told about” get different sentences. What is still NOT bound, stated plainly: an attacker who appends anopenedline beside thejudgedone satisfies the fold — a second edit to a tracked, committed file, and the same git-ancestry remainder recorded as a Book-92 deferral. And the docblocks no longer claim more than the code does: nothing in this layer reads git (scripts/canon.mjsinvokes it zero times), so “the COMMITTED event store” is a convention. What the two sensors bind is the tree to the store and the store’s tier claims to the register; a hand-appended APOCRYPHA delta still costs one edit to a TRACKED file, and the ancestry anchor that would close the remainder (git log -S<delta-id>, which names the commit a declaration entered the repository in without consulting a date) is a recorded Book-92 deferral rather than half-built prose. - A brief that names a capability is the statement that this project USES the layer. The advisory branch used to be keyed on the filesystem, so
rm -rf docs/canonturned a refuseddeliveredinto an exit-0 one in a repository whosegit log -- docs/canonproves adoption — and delete → deliver → restore committed astatus: deliveredwhose entire diff is one line of one brief and never mentionsdocs/canon. That is a silent gate bypass, not a compatibility path. Both ungated roads — nodocs/canon/at all, and a Canon that declares nothing — now REFUSE when the brief declares one or more capabilities, and stay ADVISORY when it declares none, which is every pre-Book-92 brief (the field did not exist). Compatibility survives without the bypass, and both advisory warnings now name the real mitigation: darkening the layer means DELETING a tracked file, which is a diff a reviewer sees. AND AN UNPARSEABLE DECLARATION IS UNKNOWN, NEVER “DECLARES NONE” (LD-91-1 on the discriminator itself). The first cut of that brief-keyed test filteredbrief.capabilities, and a filter over an array cannot see that the array is what SURVIVED a parse rather than what the brief said:capabilities:carrying a whitespace-only entry, andcapabilities:written as a SCALAR, both reached zero and both took the ADVISORY branch over anrm -rf docs/canonat exit 0 — while the SAME briefs with the Canon present were refused, reproducing the exact asymmetry the bypass was closed for. The parser had already raised both defects, and the handler printed them AFTER the write. So the three-answer read travels WITH the brief asDecreeBrief.capabilities_state(stated/none/unknown— a DANGLING key, a SCALAR, or a list with an unreadable entry are allunknown), UNKNOWN BLOCKS before either advisory branch and before the write, and the parser’s ownwarnings[]anderrors[]are reported at the TOP ofdecree advancerather than at the bottom, so the operator reads them first whatever the outcome — including on the idempotent-skip path, which never reached them at all. The compatibility path is untouched: an ABSENT key and a deliberate[]are bothnone, which is what every pre-Book-92 brief carries. - The shared presence predicate senses the CHAIN and has all THREE readers.
senseDirectorylstat’ed only the final component, so a hostile branch checking outdocsas a symlink delivered the very primitive the module refuses at the leaf; it now takes an optionalcontainedInand resolves the whole chain withrealpath(optional becausescrylegitimately reads~/.divinesweand an out-of-treebooks_dir). Andcanon.mjs’slistCapabilityFiles— whatdecree gapactually reads the tree through — was the third reader and was still a baretry/catch: a genuinely ABSENT directory reported “could not be listed” (manufacturingcoverage: unknownin every pre-Book-92 repository, with a stated cause that is false), and a SYMLINKED one was FOLLOWED, makinggapthe permissive outlier atcoverage: completeover a tree the write path, the Scry and the gate all refuse.readCanonTreenow answerspresent: true | false | null, and a genuinely absent layer is a KNOWN empty declaration that short-circuits the store, drift and authority reads. - The blast-radius report is POST-FLIGHT and BOUNDED. “SIGNS N Canon section(s)” was printed BEFORE the fold that can decline every one of them — measured at 2 claimed and 0 moved, with
$?0. The pre-flight line now says what is ABOUT TO BE ATTEMPTED, and the outcome is measured off the RE-READ tree and rides the machine-readable stdout line besidevow judged, becauseapplyCanonJudgment’srefused: falsemeans “the event was APPENDED” and not “the tier MOVED” (thedecanonization-standsreplay guard accepts the event and declines the transition). The checklist is bounded atCANON_CHECKLIST_HEAD(50) with a stated remainder — the one unbounded enumeration in a layer that bounded three others — with rows owing a RE-VERIFICATION, standing DE-CANONIZED or reading UNKNOWN ranked first so a bound never drops the row that most needed reading. Rows draw the DECLARED STATEMENT (canonStatementLine) rather than the whole rendered body, whose italic_Judged …._tail used to spill out as further unbulleted lines inside the enumeration; and a de-canonized row, which UNDOES a human’s refusal, is flagged as such instead of reading as the most routine row on the list.
AN ATTESTING VERDICT STATES ITS BLAST RADIUS. vow judge --verdict validated over a Book’s validation vow signs EVERY Canon section that Book declared, so it ENUMERATES them first — capability, requirement slug, statement and the re-verification flag, drafted by draftChecklistFromDeltas/formatChecklist — and states the count. The rejecting arm already itemized its de-canonized deltas; the arm that manufactures trust named nothing, and at canon genesis scale “a Book” means “a codebase”.
Two honesties about the scope of what landed:
- The A22 auto-advance is a predicate and a REPORT in this domain, not a mutation.
autoAdvanceis a pure function inscripts/shared/decree-store.mjs, shared bydecree status,decree listand the Scry, so none of them can re-derive it differently. The mutating reconcile pass that APPLIES it landed in Prophecy 5 asvow reconcilein thevowdomain above (and itsscry --healalias), which is why the application lives there rather than here: the pass that walks the ladder is the same pass that heals the register the ladder is judged against, and A22 says the two run together. It touches only the two MECHANICAL rungs —autoAdvancecannot returndone, and the reconcile refuses it a second time on its own account — sodecree advance’s acceptance-criteria and judgment-record refusals still guard the ONE rung a human signs, and a non-zero exit from an evidence refusal on that verb stays safe because nothing in the cycle drives it. A SUPPRESSED judgment stream is not an empty one (Book-Level AUDIT).senseDecreeJudgmentsreturns its suppression note plus an EMPTY judgments list whenvows_enabledis closed or the register refuses, and every downstream engine consumed the empty list without consulting the note — so closing ONE config key flipped a brief carrying a STANDING REJECTION fromactive(a rejection outranks delivery) todelivered, withpending_advance: null,rework: "", and the note on stderr alone. All three engines are told now:deriveDecreeStatusreturnsunknownrather than crediting a rung a rejection would outrank,autoAdvanceproposes nothing and says why,judgeAcceptanceCriteriareports the criteria UNJUDGED without accusing any Book of lacking a verdict on the strength of a read that never happened, and the tokendecree_judgments_unreadrides BOTH the per-rowwarnings[]and the--jsonenvelope so no piped consumer can mistake “no rejection exists” for “we never looked”. - The
rework:pointer is READ here, never written. An unresolvable pointer is NAMED (decree_rework_pointer_unresolved:<id>) because the link is how a consult finds the work it must pick up, and a dangling id that reads as satisfied converts an open rejection into invisible completed work. The mechanical spawn of the linked rework vow belongs to a later prophecy.
scry domain (THE SCRY — the pure-read morning report)
The Scry does not mint state; it makes CLAIMS about it. Eighteen heterogeneous sensors — twenty-nine under --full — each reading a different source with a different failure mode, collapsed into ONE document a human is asked to trust. It is the only facade domain that is a bare domain with no subcommand — divineswe-state.mjs scry --json, scry --full, scry --heal — which is why its CLI_DISPATCH entry is the empty list: that registry’s contract is “every <domain> <subcommand> pair the CLI honours”, and the Scry honours no pair. The facade branch is a delegation, not a fork: it strips the domain token and hands the remaining argv verbatim to scripts/scry.mjs, because two entry points computing the report separately are two reports that will eventually disagree about one repository with no way for the operator to know which one they are reading. The same module is the standalone script (node scripts/scry.mjs --json), and both doors return byte-identical documents apart from generated_at.
Whole-domain rules, stated once:
-
UNKNOWN ≠ ZERO — the single non-negotiable. A sensor that could not read its source reports
unknown, states WHY, and renders no number at all. “0 unmerged branches” and “clean tree” are the two most expensive lies this report can tell, because they are indistinguishable from good news, and a heartbeat that says “0 events” when it cannot reach the ledger is a flatline indistinguishable from a fresh repository. The rule is enforced by SHAPE rather than by discipline: the degraded-reading constructor carries no numeric field, and the render boundary strips every numeric leaf from any section that is notok— so a sensor added by a later prophecy inherits the invariant without anyone remembering to extend it. -
ABSENT ≠ UNREADABLE. A project that never wrote a ledger owes nothing and its section is suppressed from the core document; a ledger that exists and will not open is unknown. An empty ledger is a third fact and an honest
ok. Onetry { read } catch { return [] }answers all three identically and the third answer is false. A base directory that does not exist at all, though, is not eighteen absent sources — it is one unanswerable question asked eighteen times, and every sensor reports unknown. -
Suppression is ACCOUNTED FOR.
sections+suppressedtogether always name all eighteen sensors, so a sensor can never vanish by being quiet. Anunknownsection is never eligible for empty-suppression: suppressing it would make an unreadable register indistinguishable from a repository that owes nothing, which is honesty by omission. -
The
ok | unknown | skippedtrichotomy. A rite gated OFF is neither a measured zero nor a blind sensor: the source was never consulted.memory_rites_lagunder a closedteachings_enabledreportsskippedwith the gate key named, and renders no lag figure at all — “0 Books behind” would tell an operator the rite is current when it has not run. On the wire the third answer spellssuppressed, matching the establishedvow list --jsonidiom. A gate decides whether the source is read at all — the ledger and the register are opened behind their gates, so “it was not read” is literally true, and no other consumer (pending_healsincluded) can publish rows out of a register the same document says it never opened. -
A torn source caveats what it FEEDS, and nothing else — the register included. A torn ledger is the interesting case precisely because it IS readable: real numbers come out, and they are incomplete. Every ledger-derived section (
unresolved_verdicts,last_verify,ledger_heartbeat,spend_liveness) inheritsledger_integrity_caveat; no other section does, because a caveat beside all eighteen is a caveat nobody reads. Caveat AND number, never one instead of the other — refusing to show a FAIL that survived the tearing hides a genuine problem behind a data-quality complaint. The degenerate end of the ramp is the exception: when every line is malformed there is no signal left to caveat, and all four go unknown. A line is counted as damage when it does not parse, when it parses to something that is not an object, and when it carries notype—JSON.parsesucceeding is not the same as the line being an event. Nothing is ever salvaged out of a torn line by pattern-matching: a fragment that merely contains"verdict":"PASS"describes an event authored by nobody, and laundering it converts a data-integrity problem into a false all-green. The VOW REGISTER is read under the identical rule and carriesvow_integrity_caveatinto its own four sensors (judgment_inbox,owed_actions,preconditions,open_vows), with the same 100%-damageunknownat the end of the ramp: four owed debts whose last line a crashed append truncated used to render as a confidentopen: 3with nothing anywhere saying a line was lost. A caveated section is never empty-SUPPRESSED either, on both surfaces — the caveat is the finding, and suppressing the row would take it down with the empty count. -
THE CANON IS COUNTED IN CORE AND ENUMERATED ONLY BEHIND
--full(L13).canon_apocrypharides with the register quartet because an unsigned declaration is owed work, and it publishes a count — never a list. The enumeration lives in the--fullcanon_backlogrow, pulled by an operator who went looking for it rather than pushed at one who did not, and the judgment inbox reads the vow register alone. The partition is structural rather than a filter, and it is the whole point:canon genesisenters a codebase’s declarations as Apocrypha in ONE Book, so routing them through the inbox would cost the operator the one section they read daily — permanently, including the morning something real lands in it.canon_backlogcarriesCANON_BACKLOG_PARTITION_NOTICEsaying so, and its enumeration stops atSCRY_CANON_BACKLOG_HEADwith the remainder stated, because the count is the finding and a silent cut reads as “covered everything”. A Canon read is FOUR answers: the gate closed anddocs/canon/absent are bothskipped— not applicable is not could not see, and it is the same rulingdecree advance --to deliveredalready runs on — while a directory that will not list, an event store that will not read (the.mdtree DERIVES from it, L7) and a tree whose capability files are all torn areunknown. One torn file among readable siblings is the caveat ramp’s ordinary case: the surviving debts ride WITHcanon_integrity_caveat, and a section whose provenance stamp will not parse is countedunstampedrather than folded into the debt. -
canon_paths_movedsays MOVED, never WRONG. The--fullstaleness row joins a capability’s declaredpaths:watch globs against what git has changed since the earliest seal standing under it — the earliest, because a capability’s declarations are a set and one fresh section must not certify siblings written years before the code they watch.moved | settled | unknown, and no word claiming the declaration has become false: whether a requirement still holds is a comparison of text against code that this sensor never performs (that is the drift audit,scripts/canon-drift.mjs— a different sensor from the byte-levelcanon driftverb below).CANON_PATHS_MOVED_BASISrides on the row. An empty log over a scope git matches nothing in is unmeasurable, not settled — the ALL-matched confirmation the Atlas map sensor already ran on, shared as one predicate rather than re-derived, because that proof was a fact about git and condemned every caller. -
last_verifymay never claim HEAD is green. No sha is recorded with a VERIFY verdict, so the only honest phrasing is a timestamp and a commit distance —at <ts>, N commits ago. “HEAD is green” would be a claim about code the verifier never saw, which is exactly the false assurance that lets a broken commit ride out as verified. The sensor straddles two sources and degrades in halves: the ledger gives the verdict and its instant, git gives the distance, and a missing git leavescommits_since: "unknown"beside a surviving timestamp rather than a “0 commits ago” that reads as “nothing has changed since we verified”. -
spend_livenessreports SILENCE, never a zero cost. Dispatches recorded with nodispatch_spendbeside them mean the SENSOR is not wired, not that the work was free; “$0.00 spent” is the most misleading possible rendering of a missing instrument, so no total is rendered at all. -
The rite-gate panel RELAYS, it does not interpret. Every gate renders with its provenance, and the panel reads every layer it attributes to: an OFF the project chose says so, an OFF the machine-wide layer chose says
globaland names that file, an OFF derived from a layer that was absent or unread says that instead, and only a genuinely undeclared key is blamed on the built-in default. A panel that resolved provenance from the project layer alone reportedatlas_enabled— whose default is ON — as “OFF by built-in default; no layer declared it” over a machine-widefalse, which is a false sentence in the one panel whose whole product is provenance. LD-6 binds here too: a PROJECT-ONLY key declared machine-wide is inert, so it is never attributed to the global layer. The dropped-layer roster is the resolver’s own derivedPROJECT_LAYER_DROPPED_TOKENS, never a prefix test, and the resolver’s ownwarnings[]ride through verbatim rather than being re-worded into a second opinion about gate resolution. A bare OFF with no provenance is the defined failure mode of this panel — it is byte-indistinguishable from an operator who switched the rite off. When the project layer is found and cannot be read the panel itself goesunknown: the resolver’s fail-closed answer is still correct, but the panel’s job is to say what the CONFIGURATION is, and it could not read it. -
ajv_indicatoris a PROBE RESULT, never a hardcoded reassurance.active | DEGRADED — writes structurally unvalidated. The probe is an injectable seam, so the sensor stays falsifiable even though AJV resolves from the plugin’s ownnode_modulesand no per-repository fixture can reach it: an injected probe that fails to resolve reports DEGRADED, one that fails in a way the report cannot classify reportsunknown, and the DEFAULT probe isactiveorDEGRADEDand neverunknown— an operator running the shipped plugin is entitled to a stated verdict about whether their writes are being validated. -
The
ls-remotepush sensor is bounded, prompt-free, and first-class about not knowing (amendment A9). The probe runs withGIT_TERMINAL_PROMPT=0, so an authenticating remote fails instead of blocking on a credential prompt nobody is sitting at; the bound is declared in the document astimeout_msrather than buried, because a consumer readingunknownneeds to know whether the sensor waited a second or a minute; andpush-state: unknown (remote unreachable)is a different answer fromunpushed. The two mean opposite things — a network outage rendered as “unpushed” invites a push that was already done, and a real unpushed commit rendered as “unknown” leaves work stranded. A hanging or absent remote degrades exactly ONE section and the exit stays 0. With no remote configured there is nothingls-remotecould ever prove, so the sensor says so once and does not probe (Loki-Doubt L20): the shape-derived note is re-derived every run, so the day someone finally adds an origin it retires itself. -
L4 PURITY, absolutely. The Scry writes nothing — not a heal, not a close, not a cached report.
scryCoreandscryReportstay write-free even when handedheal: true, because the pure core must not become the mutation verb by option; the module links no register writer, no AJV gate and no lifecycle writer at all, so the purity is a property of the BUILD.scry --healis the ONE explicit opt-in and it is not an exception to that: it spawnsvow reconcileacross a PROCESS boundary (see the surface below), so the writes are the mutation verb’s, made under its own name, and nothing in this module’s import graph can perform one. Every git invocation runs under--no-optional-locks/GIT_OPTIONAL_LOCKS=0so that even the index refreshgit statusnormally performs cannot touch a byte; a report that mutated while being read could not be run twice with the same meaning, and “I looked at the dashboard” would become an edit nobody recorded. Purity is not blindness, though: a mechanically dischargeable obligation the report neither closes nor mentions is the “report contradicts the inbox” failure, sopending_heals[]NAMES what a heal would do and the verb that would perform it (vow reconcile, landing in the same prophecy). It may promise only what that verb would actually perform, so each row carries the STAGE the pass would reach —stampfor an obligation with noseal_shayet,close_if_provenfor one that has it — rather than asserting a dischargeability that additionally depends on a valid stamp and a containment proof this surface never obtained. A row that promised a close the verb then declined is the eternal red row L20 exists to prevent. There is noappliedflag anywhere in the document, on any surface. -
A23, the non-blocking covenant. No state reaches the exit code. A malformed ledger, an unreachable remote, a closed gate, an absent register, a corrupt
.gitand a crashed cycle all exit 0 with a full roster. An argv mistake is the operator’s own typo, not a pipeline condition, and still refuses: an unregistered flag exits non-zero with an empty stdout, and a mistyped base-path spelling is refused rather than ignored, because a report whose whole product is a claim about “here” must never silently render a different repository at exit 0. The refusal is kept out of reach of its own grammar: the boolean flags own no value slot (soscry --json /elsewhererefuses the path instead of eating it), an unknown flag consumes nothing (so its would-be value surfaces as a positional and is refused on its own account), and--base-pathhanded no value REFUSES rather than falling back to the CWD — under--healthat fallback would have selected the mutation target. -
--jsonis the sole supported machine surface (amendment A6) and carries the document ALONE on stdout. It is deliberately time-stable apart fromgenerated_at— it publishes instants (last_event_at,oldest_change_at) rather than ages — so two reads of an unchanged repository are byte-identical and a consumer diffing them sees only what actually moved.tieris thecore/fulldiscriminator, and it is what lets a consumer tell which ROSTER it was handed rather than reporting the forensic findings as absent when they were merely not requested.--fullrenders the same envelope eleven rows longer — it ADDS forensics and never trades the morning read away for them — so a consumer written against--jsonreads--json --fullwith no second parser, and one sensor pass feeds both tiers so they can never disagree about one repository. -
Untrusted register prose is made structurally INERT before it is rendered. A vow
item,preconditionortriggeris written by an untrusted actor, and the Scry is a read surface over the same register thevowdomain quarantines. Every free-text field, file name, git line and ledger instant that reaches either document passes the sharedneutralizeTextplus this surface’s own line-anchored passes (NFKC, format characters removed, every line-breaking control collapsed, C0/C1 stripped). Without them a crafteditemcarrying a newline forged a “Pending heals: none reported by this read.” verdict AND a clean sensor row inside the report an operator — and the L9 session-start read — is asked to trust. -
The prose render may not launder an honest JSON into a clean summary. The human surface is where the render-layer liar lives: a formatter reaching for
count ?? 0prints a reassuring zero over astatus: unknownthat the machine document got exactly right. So an unknown section is never suppressed from the prose, a genuinely empty and genuinely READ one is, and no all-clear phrasing is printed while any sensor is blind.
The surface:
scry --json— the machine document:{ version, generated_at, base_path, tier, purity, sections[], pending_heals[], warnings[] }.sectionsis an array because a report has a reading ORDER — owed work before diagnostics, because the report is a morning read and what a human owes comes before what a machine noticed — and every section carriesid,title,status(the roster vocabulary) andstate(the wire vocabulary,ok | unknown | suppressed). Three roster sensors additionally render under a stable consumer spelling (working_treefordirty_tree,worktreesforworktree_audit,schema_validationforajv_indicator), and every such row carriesalias_ofnaming the sensor it mirrors. Without that markersectionswas an ordered sensor roster and a name-lookup table at once — two contracts on one field — so the prose printed those sensors twice and any aggregate over the array double-counted them; an alias inherits its source’s title for the same reason, because two rows built from ONE reading must never render under two human names. and two rows belong to this surface alone:staged_consults(the plain filename glob, name-convention tagging only — no NLP and no opinion about contents) andpush_state(the A9 probe, which is a CLI-layer sensor and never a library one, because a pure library read must not reach the network).scry(no flags) — the same document rendered as prose for a human, with the same honesty vocabulary and the same refusal to congratulate an operator over sensors that could not look.scry --full— the A21 forensics tier: the core roster PLUS eleven sensors that answer “what has quietly rotted” rather than “what is owed today” —claims_decay,map_staleness,offerings_unsent,plugin_version_drift,register_disagreement,version_triad,spend_rollup,stalled_decrees,upstream_ahead,canon_paths_moved,canon_backlog(exported frozen and ordered asSCRY_FULL_SECTION_IDS, disjoint from the core roster so no aggregate oversectionscan count one sensor twice). They live behind a flag because their answers are usually the same as yesterday’s, and a section printed every morning is a section the operator learns to scroll past on the day it finally changes. Each is bound by the SAME non-negotiable as the eighteen — an unreadable source reportsunknownand publishes no figure — and each keeps the third rung apart from the second: a consumer repository has no claims register and no version triad, so those rows aresuppressedwith a stated reason rather thanunknown, because not applicable is not could not see. Their vocabulary is deliberately narrow (L12): version drift is equality-only (match/differs, never an ordering claim it cannot substantiate from two opaque strings), register disagreement is a set-diff naming the task and both directions rather than a count, a stalled Decree says “no commit observed” — a claim about a format-sensitiveBook N:grep, never about whether anyone worked — the upstream count is stamped “as of last fetch” because@{upstream}is a local ref and not live remote truth, and the spend rollup is token-based, never priced and states its approximate share on every rollup, including when it is zero.--fullis BOOLEAN and owns no value slot, soscry --full /elsewhererefuses the path instead of eating it.scry --format html— the same document as a complete, self-contained HTML page, written to.divineswe/reports/scry.html(exported asSCRY_HTML_REPORT_PATH).--formattakesprose | json | html;--jsonis the shorthand for--format json, an unhonoured directive REFUSES rather than falling back — a format flag that silently downgraded would write prose into a file an operator opens in a browser — and a--formathanded no value refuses on the--base-pathprecedent. The renderer (renderScryHtml) is a pure function of the--jsondocument: it reads no filesystem, no git, no clock, no environment and no working directory, captures nothing at module load, mutates nothing it was handed, and writes nothing — the same document renders byte-identically in any process, directory and timezone, which is what makes the page auditable: everything true of the JSON is true of the page, and nothing that is not in the JSON can enter it. Every string leaf is escaped through one five-character escaper serving both the text and attribute sinks (Loki-Doubt L22 was a text-node-only escaper, andid="x" onmouseover="alert(1)"needs no<at all), with no allow-list of already-clean fields — a defence that holds only while some upstream producer keeps sanitizing some field breaks the day a field is added. The URL sink is closed by rejection, not encoding (javascript:alert(1)contains not one character an escaper touches), so the page emits no url-bearing attribute at all: every hostile URL in the document is READABLE as text and none of it is clickable, and the page’s total self-containment — no CDN, no font, no image, nourl(), no@import— falls out of the same rule.<style>, HTML comments and<title>are closed structurally, because entity escaping does not apply inside them: they never receive document data, and every colour and magnitude a document can influence arrives as a CLASS from a closed allow-list rather than an interpolatedstyleattribute. The page carries an enforcingContent-Security-Policymeta (never-Report-Only, which browsers ignore outright) ahead of any content it governs, namingbase-uriandobject-srcexplicitly because neither falls back todefault-src. This is the ONE write on the whole surface and it is made by explicit request —--jsonand the prose surface still leave no trace — and the reports directory carries a self-ignoring.gitignore, because an untracked artifact would dirty the very working tree this report’s owndirty_treesensor reports on.scry --heal— the alias for the mutation verb, and an alias is a delegation: it performs exactly whatvow reconcile(in thevowdomain above) performs, because it isvow reconcile. The delegation crosses a process boundary rather than a module import, and that is the load-bearing part: the facade already delegates the wholescrydomain toscripts/scry.mjs, so a link back would put a register writer, an AJV gate and a Decree lifecycle writer inside the module graph of a command whose entire promise is that it only reads. The process boundary makes L4 purity a property of the build rather than of a reviewer’s attention — nothing inscry.mjscan mutate, because nothing it links to can, and the repository’s.d.ts↔.mjsconformance ratchet enforces exactly that. The heal runs BEFORE the report, so the document a human reads describes the state the heal LEFT rather than a snapshot the next line invalidates, and the pass’s own summary is relayed to stderr: the Scry document owns stdout on both surfaces, and--jsoncarries that document ALONE. A23 holds at this door too — a reconcile that could not run costs a stderr line and never the operator’s morning report, and no state reaches the exit code.scryCoreandscryReportare untouched by the flag and stay write-free even when handedheal: true, because the pure core is not the mutation verb and must not become one by option.
The library behind it (scripts/scry.mjs, declared in scripts/scry.d.ts) exports scryCore — the eighteen-sensor document with empty-section suppression and ages measured against an injectable now — beside scryReport, the consumer projection, and scryFull, the same projection eleven rows longer. home is a parameter rather than an ambient lookup, so a report can never fall back to the reader’s real ~/.divineswe/ and surface another project’s obligations as this one’s; pluginRoot is a parameter for the same reason and one more — without that seam the version-drift sensor could only ever read the tree it is running out of, which would make an honesty sensor unfalsifiable. All three documents come from ONE sensor pass, so the surfaces cannot disagree.
canon domain (THE CANON — the living current-truth capability layer)
The Canon is what this system currently CLAIMS to be true about itself. It is the “is” pole of the reconciliation loop — Decree = desired state, Canon = declared actual state, Books = actuator, Judgment = sensor — and it lives at docs/canon/: a committed append-only event store .canon-log.jsonl (merge=union), plus one <capability>.md file per capability that is a DERIVED artifact and nothing else. Four facade verbs — canon show, canon list, canon validate and canon drift — are PURE READS. A fifth, canon genesis (B92-P5), is the ONE BOOTSTRAP and the only writer on this domain. The Canon’s other write paths are the seal merge (book seal-bookkeeping), the vow judge signature, and a correction flash Book.
Whole-domain rules, stated once:
- THE FOLD’S TOTAL ORDER IS THE SEAL TIMESTAMP, NEVER THE BOOK NUMBER (L7). The log is
merge=unionacross branches, so two parallel seals arrive in whichever order git chose; a fold ordered by Book number reorders history whenever two branches land together, and a fold ordered by ARRIVAL order is not a function of the event set at all. Ties at one canonical instant break on ascending delta id, which is deterministic on every machine because the id is — and that tie-break is a NO-OP for two judgments over one delta, whose delta id and instant are equal by construction, so they get their own rule below. Two deltas landing on one section key resolve later-seal-wins, and the loser is RETAINED inprovenance.superseded— a supersession that erases what it superseded leaves no evidence that the Canon ever said otherwise. A REMOVED delta therefore leaves a tombstone rather than deleting the key: the key keeps its supersession chain and any voided signature, appears in no derived artifact and in no count, and a later declaration on the same key inherits the history. Adeletemade the engine perform the very erasure L7 forbids. - AND THE FOLD IS A FUNCTION OF THE EVENT SET, WHICH IS WHAT
merge=unionACTUALLY RESTS ON. Order-independence is a property to be earned, not a sentence to be written: the first cut de-duplicated on FIRST ARRIVAL and compared onlydigest— which coversstatementand nothing else — so two events under one id could disagree about capability, slug, operation, seal instant, Book and watch globs while looking identical to the guard, and the ordinary parallel-branch case produced two different declared truths from one event set, both certifying clean. Two halves resolve it, and the split between them is the design. A semantic disagreement (capability, operation, requirement slug, body digest or watch globs) is two RIVAL declarations under one name: both are REFUSED asduplicate-delta-id, because there is no later to appeal to when one would erase the other under an identical id. A provenance-only disagreement — the same declaration recorded at two instants, under two Book stamps, out of two spec files — is ONE declaration two branches both sealed, and refusing it would destroy the legitimate parallel sealmerge=unionexists to survive; it resolves to the EARLIEST recording, tie-broken on Book, then source, then canonical JSON. Earliest rather than latest because the declaration was made when it was first recorded — taking the latest would let a re-seal jump a section back over a supersession that legitimately landed between the two recordings. The fold is also linear in the event count, and that is correctness rather than tidiness: a quadratic fold on a store a branch storm can grow IS the halt A23 forbids, arriving as a hang rather than as a throw. JUDGMENTS GET THE SAME TREATMENT, and for a round they did not: two judgments over ONE delta at ONE instant tie on the seal key, tie on rank and tie on the delta id, so V8’s stable sort handed the Canon’s TIER to whichever line git union-merged first —[delta, validated, rejected]folded todecanonizedand the reverse toverified, one event set, two truths, zero findings, while the enumerated.gitattributesclause that replaced the bare property claim was itself false for half the store. Two DIFFERENT verdicts at one instant are RIVAL ATTESTATIONS and are both REFUSED ascontested-judgment, leaving the section the tier it already had; two agreeing judgments differ only in ATTRIBUTION and resolve to the lowestvow_id, then canonical JSON. - THE SECTION KEY IS
(capability, requirement_slug); THE DELTA ID IS THE PER-EVENT IDEMPOTENCY KEY. That split is what makes cross-Book supersession and same-id overwrite two DIFFERENT things rather than one ambiguous one. Two Books declaring the same requirement mint different ids (the Book steers the id namespace) and land on ONE section — that is supersession. Two declarations minting the SAME id with DIFFERENT digests are two rival bodies for one event, with no “later” to appeal to and nothing to retain: that is REFUSED, never resolved, in both directions (within one spec, and against the accumulated id set the committed log carries). Same id and same digest is a genuine idempotent re-merge and applies exactly once — an engine that refused every returning id could not re-fold its own log. - A RE-WORDING NAMES ITS PRIOR THROUGH
from:, AND THAT FIELD IS A TRI-STATE, NOT A BOOLEAN (LD-12). The section-key match above only fires when a later delta happens to slug to the SAME requirement text; aMODIFIEDorREMOVEDdelta that changes the WORDS entirely — the whole point of a re-wording — slugs to a DIFFERENT key and would otherwise mint a second, orphaned section beside the one it means to replace. An indentedfrom: <prior statement>continuation line under such a delta is how the author names the target explicitly; both doors carry it asCanonDelta.supersedes, a SLUG, never the raw words. THE SLUGGER IS SHARED; THE DOOR IS NOT, and the honest statement of that is the one below rather than a claim of one chokepoint. The LOG door hands its stored field toadmitCanonDeclaration, which derivessupersedesbeside the id, the slug and the digest. The SPEC door derives it inreadFromLine(canon-parse.mjs) as the continuation line is read, because the refusals it owes are LINE-ANCHORED — orphaned, duplicated, wrong-operation, empty, unsluggable — and an author standing in the room needs the line number, which a field-shaped predicate holding no document cannot give. Both call the SAME exportedcanonRequirementSlug, so the two doors cannot disagree about what a value slugs to; what they do not share is the guard ladder around it, and an earlier round of this document claimed they did. A guard owed at both doors must therefore be written at both, and the shared slugger is the reason the VALUES agree — not a reason to expect the CHECKS to. The three states are absent (nofrom:was written —supersedes: null, and the requirement stands alone exactly as it always has), present and refused (a malformed continuation — orphaned, empty, unsluggable, duplicated, or attached to the wrong operation — is a structural finding at the SAME door every other malformed delta reaches,supersedesstaysnull, and the run is already non-clean before LD-12 is reached at all), and present and well-formed, wheresupersedescarries a slug and NOTHING has yet decided whether that slug names anything real —admitCanonDeclarationholds no Canon and resolves nothing (LD-13). At the fold (foldCanonLog/renderCapabilityFile,scripts/canon.mjs), a well-formedsupersedesthat matches a LIVE section key at that point in the timeline SUPERSEDES it: the surviving section keeps the SUCCESSOR’s own key (renderCapabilityFilewrites## ${section.slug}, so the heading IS the slug — retaining the prior’s key would render a heading contradicting the body beneath it, and a later re-declaration of the prior’s own words would then mint a second, colliding section), carries the new statement and the new delta’s provenance, and a_Supersedes <prior-id> (Book <n>, sealed <instant>) — "<prior-slug>"._line preserves the retired requirement’s history rather than erasing it (L7 tombstone discipline). The prior’s SLUG rides beside its id because a delta id is derived over the canonical four-tuple and names nothing a reader can place: without it a CROSS-SUBJECT retirement rendered byte-indistinguishably from a section rewording itself. THE TOMBSTONE KEEPS THE RETIRED SECTION’S OWN STAMP WHEN THE RETIRER LEAVES A SECTION OF ITS OWN. AREMOVED … from:declares nothing of its own, so the tombstone IS the retraction’s only record and carries the retirer’s stamp; aMODIFIED … from:stands LIVE under its own key in the same file, so re-stamping the tombstone with it made a later Book re-declaring the prior’s words render_Supersedes <successor>_under the prior — the retired section claiming to have retired the section that retired IT. There the retired stamp stays put and the retirer is recorded onprovenance.retired_by. AND A VOIDED SIGNATURE THIS DELTA MINTED OUTRANKS ONE IT MERELY INHERITED (L17).voided_signatureis a one-slot record and a supersession can void two signatures — the prior on the successor’s own key and the prior it NAMED — so a fold that filled the slot only when it was empty discarded the named prior’s void whenever the successor’s slug had ever been stated before, leaving it on the tombstone, which no reader of the Canon can reach. FRESH beats CARRIED; and because one slot cannot hold two, the void is ALSO raised on the deferredsuperseded-judgmentchannel, which reports while the successor is live and unattested and clears the moment somebody signs it. TWO DELTAS NAMING ONE LIVE PRIOR ARE RIVALS, AND ONLY THE FOLD CAN SEE THEM.rival-from-lineis per-DOCUMENT andcontested-supersessionis per-delta-ID, so two deltas in DIFFERENT specs of the SAME Book clear both, and the plan-time gate audits one spec at a time against a committed file where the prior is live for both. The fold is the door holding the whole store: the loser is REFUSED asrival-supersession, naming the delta that took the prior and what the Canon carries in its place, and the refusal count raises A23’s middle rung as a failed-canonization vow at seal. A well-formedsupersedesthat matches NOTHING live at fold time is a failed-canonization vow, exactly like any other malformed delta — the seal never halts (A23). - THE PLAN-TIME HALF OF LD-12 REFUSES WHAT THE FOLD WOULD OTHERWISE SILENTLY SKIP.
scripts/verify-consultation-complete.mjs --book, the mandatory Step 5d terminal verification, reads the LIVE Canon throughreadCanonCapability— the same read surfacecanon showstands on — and, for every delta whosesupersedesis non-null, checks whether that slug names a LIVE section of the delta’s OWN capability right now. An absentfrom:triggers nothing here at all, exit 0, byte-identical to a pre-Book-107 run — the fallback is genuinely SILENT. A present-and-resolvablefrom:also exits 0 — refusing here on every presentfrom:would satisfy the letter of “declaring an unhonourable intent refuses” while refusing every LEGITIMATE supersession, which is the paranoid gate this Book’s own audit round warned against and B107-P4-06’s own mutation testing proved wrong by execution (the success arm reddens under that mutant while every refusal probe stays green). A present-and-unresolvablefrom:is a non-zero exit, naming the refusing delta’s id and quoting the declared value verbatim, because the author is still in the room at plan time and a declared supersession the Canon cannot honour is worse than one never declared — this is the ONE place downstream ofadmitCanonDeclarationwhere the resolution LD-13 defers finally happens, and it happens before the words leave the author’s hands. An UNREADABLE or UNKNOWN capability file resolves as UNRESOLVED here too (fail-closed, LD-91-1) — “I could not check” is never treated as “it checked out”. - THE DIGEST IS RE-DERIVED AT FOLD TIME, NEVER TRUSTED FROM THE LOG. It is the only binding between a logged id and the words it claims, and a digest read back out of the same file it is auditing is inert by construction (LD-91-4) — an editor could rewrite the statement in the committed store and leave the field alone. An event whose digest no longer covers its statement is refused and named.
- THE LOG IS A SECOND DOOR PAST EVERY SPEC-SIDE GUARD (LD-91-3), SO BOTH DOORS ROUTE THROUGH ONE CHOKEPOINT.
canon-parse.mjsguards a declaration where its values are BORN, in the spec; the committed log is the OTHER door onto the same fields, and the fold is what turns them intodocs/canon/<capability>.md. The first cut re-applied that discipline field by field and left exactly what a checklist always leaves — the guard it did not think to copy. ONE appended JSONL line whosestatementended<style>made every later requirement and every provenance stamp in a real capability file invisible in every renderer while both sensors certified the tree; another declared its ownrequirement_slugand filed a body under any##heading it chose; another published/etc/**and~/.ssh/**as L15 watch globs one function away from the predicate that refuses them. So the guards are no longer COPIED:admitCanonDeclarationis the single predicate both doors call, andscripts/canon.mjsadds only the facts a LOG event has and a spec bullet does not — an orderable non-future seal instant, and the digest re-derived. A guard added at the chokepoint is added to every door there is, and a third door cannot open unguarded without DELETING a call, which is a visible act rather than an omission. It is tagged, not thrown, because two doors dispose of two verdicts differently and correctly: an embedded HTML comment is STRIPPED and the delta kept at the spec door, where the author is present and no merge has happened, and REFUSES the event at the log door, where a digest already covers the recorded words and nobody can be asked. Therequirement_slugis re-derived and never trusted, for the same reason as the digest — it is half the section key AND the literal##heading a human reads — and a declared value that disagrees is reported and ignored. - A DECLARATION NO REVIEWER CAN SEE IS NOT A DECLARATION, AND THE SCAN READS PROSE ONLY. CommonMark passes an INLINE open tag through to the renderer verbatim, so prose ending
… <style>above a## Canon Deltasheading leaves a RAWTEXT region open ACROSS it: the heading, the capability block and every bullet are invisible in every renderer while the parser reads them perfectly, and a realbook seal-bookkeepingrun canonizedoperators may self approve their own decreeat exit 0 that way. The heading now gets the disposition this grammar already gives a heading it can prove is hidden — nothing under it becomes a delta, loudly, ashidden-section. The scan masks out every FENCED line (a spec that documents this grammar inside a ``` block quotes<style>as literal text) and every line inside a## Canon Deltasspan — the second mask is what keeps this clear of the A23 contract that a hostile delta must never take its well-formed SIBLING down with it, since a scan that never reads a delta line cannot. AND THE RESIDUAL SURVIVED THAT FIX, MOVED ONE LINE: because the prose scan masks the whole span, an opener on a NON-BULLET line INSIDE the section — a “Context for the reviewer: … see the operator handbook<style>” line between the### capability:heading and the bullets — was invisible to both guards and swallowed every bullet after it, strictly worse than what it replaced because the heading now RENDERS and a reviewer sees a section that appears to declare nothing. A per-section, single-pass scan (inSectionHiddenFrom) carries the region state forward across the section’s non-delta lines and stops the grammar at the first line that leaves one open, ashidden-declaration; the bullets BEFORE it are visible and survive. AND IT MUST SEE AN OPENER ON A DELTA BULLET, WHICH IS WHERE THE TWO CONTRACTS COLLIDE. That scan firstcontinued on every delta-shaped line before advancing the region, so an opener riding a SACRIFICED bullet was invisible to it: the decoy is refused on its own account — its notice says in as many words that it “makes every later requirement INVISIBLE” — and the same pass then read, accepted and canonized every bullet after it, through a real seal at exit 0 withvalidateanddriftboth clean. The region state is now advanced across delta lines, and the CUT is made at a delta line too. That last clause was a CONTRACT decision, not a parser one, because the shape is BYTE-IDENTICAL to the A23 fixture pinned atcanon-merge-adversarial.test.ts:1795— two adjacent bullets, the first opening a region, the second hidden by it, differing in nothing a parser can read but the English words — so P1 (a refused hostile bullet must not cost its honest sibling) and P2 (a bullet an earlier bullet made invisible must not become committed truth) wanted opposite answers for the same input, and dropping the delta-line clause turned six of the seven pinned invisibility fixtures red (measured, not argued). THE BOOK-92 RULING: AN INVISIBLE REQUIREMENT IS NEVER CANONIZED. The swallowed siblings are refused LOUDLY underhidden-declaration-swallowed-by-opener— a code DISTINCT from the opener’s ownraw-html-requirement, naming the swallowing line and listing the lines it consumed, because “your bullet is unrenderable” and “your bullet deleted three of your colleague’s” are different repairs. P1’s real protection is undiminished: what :1795 exists to prevent is one bad bullet SILENTLY destroying an author’s work, it was written for a sibling that stays VISIBLE, and every such sibling still canonizes — the amended fixture now carries one on EACH side of the opener and pins both halves in one run. For an invisible sibling a loud refusal preserves the whole of that protection, since the skip is visible at every surface, the append-only log is untouched and repair-and-refold recovers the section, while nothing a reviewer cannot see enters committed truth. The refusal fires only where the construct actually crosses a block boundary: the delta door is deliberately stricter than any renderer, so<span class="xis refused on its own account while CommonMark escapes it to<spanand the bullet below renders untouched — that one must still canonize, and the fixture battery declares the distinction per row (swallows) with a cross-check test that keeps the field honest. AND THE DENIAL INVERSION IS CLOSED WITH IT. The scan used to read`# <Title>`in inline code and a baren<limitas open regions, so ordinary prose dropped a Book’s ENTIRE declaration and reportedpresent: false— indistinguishable from a Book that declared nothing. CommonMark escapes both: a code span renders as literal text, and an INCOMPLETE inline tag becomes<a. So code spans are masked at both doors (conservatively — an unmatched or backslash-escaped run masks nothing, because over-masking would be a forgery primitive), and the unterminated-tag clause is opt-out BY NAME (inlineOnly) for the prose scan alone, since a delta body has an author present and a loud refusal there costs a reword. The previous pass wrote “measured false-positive rate: 0” into a code comment and the real rate was 2 of 110 committed docs, includingdocs/specs/book-78-site-copy-docs-rework.md:103— a stale measurement is worse than none, because it stops the next reader from checking, so the rate is now pinned by a test that appends a Canon Deltas section to every committeddocs/**/*.mdand fails on a single lost declaration. - EVERY WRITER STANDS INSIDE THE TREE, AND ON PREDICATES THIS REPO ALREADY SHIPS.
refoldCanonFileswrites each derived file behindassertRegularArtifactFile(which refuses a symlinked LEAF and a symlinked PARENT chain, the second of which an lstat of the final component never sees) plus a check thatdocs/canon/is itself a real directory rather than a link — git checks symlinks out, so a hostile branch delivers that primitive.appendCanonEventsstands oninspectWriteTargetLeaf, which isnlink-aware:lstatreports a second name for an inode as an ordinary regular file, so theisSymbolicLink() || !isFile()check a prior prophecy paid a CRITICAL to disprove accepts a hard link and writes the committed Canon through a victim inode at exit 0. AND THEnlinkCLAUSE NOW LIVES AT THE PREDICATE, because the third occurrence of one lesson is a design fault rather than an oversight: those two writers sat ten functions apart in one module, edited in one pass, and the capability writer got the predicate that cannot see a hard link while the store writer got the one that can — a planted link took a capability write straight through an out-of-tree victim atwritten: 1, findings: [].assertRegularArtifactFileitself now refusesnlink > 1, for reads as well as writes, so no caller can select the weaker predicate because there is no longer a weaker one. It is a realappendFileSyncbehind the shared torn-tail predicate, never a whole-file rewrite: an append-onlymerge=unionregister rewritten whole loses a concurrent append outright and turns one torn line into a truncated history. The seal-time SPEC read is contained the same way —extractSpecPathanswers a question about the STRING and is right to, and a committed in-repo symlink is a textually impeccable repo-relative path — while a REDIRECT and an UNREADABLE file are kept apart, because an EACCES is “I could not look” and must keep its errno. That containment was, for one round, a SELF-COMPARISON: the reader passeddirname(specPath)as the vetted container, so the predicate compared a value against itself and the entire parent-chain clause was inert; a symlinkeddocs/specsDIRECTORY sailed through and a real seal canonized text from outside the repository at exit 0. The vetted container of a Book spec is the PROJECT ROOT, asserted throughassertArtifactWithinRoot, which realpaths the whole chain — the only view a symlinked ANCESTOR is visible in, and one no spec layout can outgrow. - THE TIER IS DERIVED FROM A JUDGMENT EVENT, NEVER ASSERTED BY A DELTA. A delta event’s
statusfield is writtenapocryphaand read by nothing: a tier a text editor can promote is not a tier. Only atype: 'judgment'event lifts a section out of the Apocrypha (validated→ verified) or de-canonizes it (rejected→ decanonized). The vow-side vocabulary is carried VERBATIM —verifiedis the CANON tier one layer over, and a judgment speaking the other layer’s dialect is one no reader can place, so it is refused. A signature binds to SPECIFIC WORDS through the delta that declared them, so it never carries onto the body that replaced them, in either arrival order. - L17 — A DELTA ONTO A VERIFIED SECTION RESETS IT AND RECORDS THE VOIDED SIGNATURE. Without the reset a human’s signature would silently vouch for words they never read; without the record the reset would erase the only evidence that anyone ever signed, and the re-verification the sealing Book owes would have no trigger. The voided signature survives a later re-verification.
- THE
.mdFILES ARE DERIVED, SO A HAND EDIT IS DRIFT AND NOT A CONTRIBUTION. A git conflict in one resolves by re-fold, never by hand-merge (which is whydocs/canon/*.mdcarries-mergein.gitattributeswhile the log carriesmerge=union). A re-fold is AUTHORITATIVE over anything it finds on disk — one that merged what it found with what the log says would adopt a forgery rather than overwrite it — and that authority extends to the whole tree it claims to derive, not only to the files it happens to write. A capability the fold no longer produces has its file removed and the removal reported inremoved[]: iterating only the produced capabilities made the RETRACTION operation retract nothing, so aREMOVEDthat emptied a capability left the withdrawn requirement standing in the committed, human-read, agent-read file while the merge reported success. (README.mdcarries capitals and is not a capability slug, so the directory’s own documentation is never a casualty.) Aremovedfield that is structurally always[]reads to a consumer as “nothing was removed this run” when it means “this engine cannot remove” — the same unknown-versus-zero conflation, one level down in the API. AND AUTHORITY TO DELETE IS GATED ON A FOLD THE ENGINE CAN VOUCH FOR (LD-91-1 ON THE WRITE SIDE). Making the re-fold authoritative was the right fix and it armed a silent Canon wipe: an ABSENT store, a ZERO-BYTE store and a store whose every line is malformed are allreadable: true, all fold to zero capabilities, and all took the delete path — and.canon-log.jsonlis committedmerge=union, so a git merge that leaves conflict markers is the ORDINARY delivery mechanism for the third. A realbook seal-bookkeepingprinted “canonized 1 delta(s) into 1 capability file(s)”, exited 0, and leftdocs/canon/without thejudgment=verifiedfile it started with. The refusal path composed the same way:canonDeltaKeyhashes[book, capability, operation, slug]and the slug eats punctuation, so appending the committed record with ONE extra character mints the same id, refuse-both fires, and the delete follows — a refusal must never be reachable as a deletion primitive. SovouchForFolddemands a store that is readable AND present AND parses with zero malformed lines AND carries at least one event AND had zero events refused (a COUNT,counts.refused, so a refusal code added later inherits the protection instead of silently re-opening the hole); otherwise the standing file is LEFT STANDING and reported ascapability-orphaned, because an unlogged file a reader can still see is recoverable and a deleted one is not. AND THE SAME VOUCH GATES THE REWRITE, BECAUSE AN OVERWRITE IS A DELETION AT SECTION GRANULARITY. The vouch was computed BETWEEN the two loops, so it gated the delete and the write loop ahead of it ran unguarded: a fold that had already concluded it could not account for the store still rewrote every capability file from its partial reading, and one conflict marker on one log line — again the ordinarymerge=unionoutcome — returnedwritten:[<the file>] removed:[] findings:[]while deleting a committed section out of it, withcanon driftreportingverdict=cleanbecause drift re-folds the same torn store. The unvouchable fold now leaves the committed file AS IT STANDS and says so, asunvouchable-fold, below the no-op check so silence still means nothing was withheld. Reporting zero where the answer is UNKNOWN costs a warning; WRITING zero over a real Canon is the same lie with the evidence destroyed.removedis also carried throughmergeCanonDeltasandapplyCanonJudgmentonto the seal notice, which used to report “canonized 1 delta(s) into 0 capability file(s)” for the operation whose entire purpose was to delete one.canon validateis the sensor and never the repair: a validator that quietly re-folded the file it was reporting on would erase the very evidence of the drift. - A TIER NOTHING BINDS TO A HUMAN IS NOT CERTIFIED, AND THAT TOOTH IS NAMED HONESTLY.
docs/canon/is an ordinary docs path — NOT under.divineswe/— so the agent-context attestation carve-out that guardsvow judgedoes not cover a plain Write into the event store, and one appended judgment line promoted a section toverified. A validator cannot intercept a file write, so it refuses to certify the result instead:canon validateresolves every judgment’svow_idagainst the configured Vows register (by default,.divineswe/vows.jsonl) and reportsunauthenticated-judgmentfor a signature the register cannot account for, orunverifiable-judgmentwhen the register itself could not be consulted — UNKNOWN, which is not the same claim as sound. WHAT “ACCOUNT FOR” MEANS IS STATED, because for one round it meant almost nothing: asking only whether the id APPEARED in the register raised the bar from “any string” to “any string printed in a COMMITTED file”, and a vow that had merely beenopened— judged by nobody — authenticated averifiedtier atvalid: true. The named vow must now exist, must carry a renderedjudgedverdict (the eventvow judgewrites at the register’s attestation chokepoint, the one verb carved out of facade auto-approval and refused from a detected agent context), and that verdict must AGREE with the tier the section claims. What is deliberately NOT bound, said plainly rather than implied: the vow is not required to NAME this capability, slug or delta, so a real attestation over one section can still be cited by another — closing that needs a back-reference the vow register does not yet carry, and it is declared Prophecy-3 work rather than papered over. This is a DETECTION control and is documented as one, never as enforcement: the Canon’s tier is evidence-gated at READ time, exactly as the vow register’s own closes are. - UNKNOWN ≠ ZERO ≠ SUPPRESSED (LD-91-1). Every
--jsonanswer carries astatedrawn from exactlyok | unknown | suppressed. “The Canon declares nothing here”, “I could not read the Canon” and “a gate said no” are three different claims and never render alike. A count whose source could not be read isnull, never0; a roster row for an unreadable capability file still EXISTS with null counts, because a capability quietly dropped from the roster is the same lie with the evidence removed. A re-fold over an unreadable store writes nothing at all, because writing an empty Canon over a real one is the destructive form of the same lie. - A23, THE NON-BLOCKING COVENANT. No state reaches the exit code: findings, unknown, suppressed, a torn log line and an absent tree all exit 0, because a validator wired into a
set -eseal path with a non-zero exit IS the halt this covenant forbids. The machine-readable channel isverdict, never$?. An argv mistake is the operator’s own typo rather than a state failure and still refuses non-zero — an unregistered flag, an unknown subcommand, a missing--capability, and a--capabilitythat is not a single lower-case slug. That last refusal is also a non-disclosure: a rejected traversal that echoed the target’s bytes would BE the traversal it refused, so the rule is stated and the rejected value is deliberately not printed. - THE
canon_enabledGATE — the ninth gate key: default FALSE since 0.68.0 (a project that keeps a Canon opts in), PROJECT-ONLY, DEPENDENT onvows_enabled. A closed gate SUPPRESSES a read LOUDLY (state: "suppressed",suppressed: true, the stable tokencanon_suppressed_by_gate, exit 0, an answer that is never byte-identical to the empty one) and never yields a clean empty or a clean verdict — a gate that closed the read must never be able to certify the Canon. A global declaration of the key is INERT and is reported (global_key_ignored:canon_enabled), because an inert declaration nobody is told about looks exactly like an honoured one.canon_enabledtrue overvows_enabledfalse is relayed asdependent_gate_without_substrate:canon_enabledand never flipped. The output FORMAT follows the caller’s request and never the gate: a caller that asked for prose and got a JSON envelope has had its parse broken by something it cannot see.
The surface:
canon list [--json]— the ROSTER of capability files.capabilitiesis an array because a roster has a reading ORDER, and each row names itscapability, its ownstate, its declaredpathswatch globs (the drift audit’s whole input, L15) and the per-tier section countsapocrypha/verified/decanonized.canon show --capability <slug> [--json]— ONE capability rendered section by section in document order, each section carrying itsslug, thejudgmenttier, the declaringbook, thesealed_atinstant, thedeltaid off its provenance line and the requirement prose. An unknown capability is a NOT-FOUND answer (found: false,sections: null) — distinct from a capability that exists and declares nothing (found: true,sections: []) — and it is never scaffolded: machinery reads declared truth and never mints it, so autovivifying a stub for a typo’d slug would be a write path outside the seal.canon validate [--json]— the HONEST reporter. Averdictofclean | findings | unknownwithvalid === (verdict === 'clean')stated explicitly, so validity is never INFERRED from an empty findings array (an empty array is also what a crashed gather returns). The verdict weighs severity: awarning-tier finding is published infindings[]in full and does not make a tree invalid, because the one warning this layer emits —superseded-judgment— is raised by the layer’s ORDINARY lifecycle (seal in Book N, supersede in Book N+1, then sign Book N’s still-open validation vow) and a severity field no consumer reads is a label rather than a fact. Unreadable log, absent log, torn log line, unreadable capability file, a section with no provenance stamp, a stamp no reader accepts, a section whose delta id the log never carried, a body the store never recorded under that delta, watch globs or acapability:the frontmatter declares and the store never recorded, a tier no judgment signed, a tier no vow can account for, and a section the store declares that the file does not carry are ALL reportable — and none of them may reportclean. The join is semantic rather than byte-for-byte AT THE FILE LEVEL — heading order, blank lines and frontmatter spelling are a formatting difference and reporting them as drift would make every real finding unreadable — and the SECTION BODY is a region this module writes in full, so it admits no difference beyond blank lines at all. It is RENDERED from the fold by the samerenderSectionBodythe capability file was written with and compared exactly, which is the third disposition this comparison has had and the first that closes both holes. AstartsWithprefix accepted an appended clause (agents must never write outside the repository roothand-edited to…, EXCEPT when the operator supplies --force) and certified the inverse of the record as clean. Replacing it with strip-then-equality closed that and opened a worse one: the strip deleted every_Supersedes …._/_Signature voided: …._/_Judged …._line from the COMMITTED side before comparing, so a hand edit SHAPED LIKE the metadata tail was removed before it could be compared, and appending_Judged validated by <any vow> on <any instant>._yieldedclean, valid: true, findings: []— over exactly the line that carries the Apocrypha→verified trust claim a human, the docs site and the PR view actually read, while the machinejudgment=field inside the HTML stamp stayed untouched and invisible in every renderer, and no judgment EVENT existed for the vow-register check to see. One writer of that region, one reader of it. The frontmatter is part of the claim for the same reason:paths:is the L15 drift audit’s entire input, so an unlogged glob points that read wherever its author chose. AND A CITATION IS WEIGHED BY THE WRITER’S OWN TWO PREDICATES. A section’s judgment stamp is authenticated against the obligation register’s OWN fold, and the cited vow must exist, must be one the register was told about (a hand-appendedjudgedline with noopenedbeside it is REFUSED), must carry a rendered verdict agreeing with the tier, must record the kindvalidation, and must name the Book these words were sealed by. The kind is not a second-best proxy:handleVowJudgewill SIGN the Canon for a validation vow ALONE — a rework debt, a chain step or a hand-opened obligation is not an attestation about what the system declares — so akind: "backlog"row, which this project’s own drift relay mints, is a citation the signer would have REFUSED, and the read side must not certify what the write side will not mint. Book equality alone was too coarse to stand on:canon genesisdeclares a WHOLE CODEBASE into ONE Book, so every judged vow of every kind scoped to that Book was a master key for every section in it.canon drift [--json]— the BYTE-level detector, and the reason it is a verb at all:detectCanonDriftshipped fully working, correct and reachable from nothing, so the only sensor an operator could actually run was the semantic one, and an uninvoked detector is inert. It compares the committed.mdtree against a re-fold in three directions, because a detector that only compares the sections PRESENT in a file cannot see the one that is gone:drifted(bytes differ),missing(the log declares a capability with no file) andorphaned(a file the log declares no capability for), withcheckedbeside them.cleanisnull— nevertrue— when the store or the directory could not be read, and it never repairs: a validator that quietly re-folded the file it was reporting on would erase the evidence of the drift. Exit 0 always (A23); the machine channel isverdict, never$?.canon genesis --survey <repo-relative JSON> --book <n> [--json]— THE BOOTSTRAP, and the ONE write on this domain. A project with no Canon has nothing to audit, so genesis scaffolds one from a SURVEY — an Atlas exploration’s output,{capabilities: [{capability, paths, requirements}]}— wholesale, into the APOCRYPHA. It is a facade verb rather than a library call because an UNINVOKED BOOTSTRAP IS INERT and a bootstrap invoked by a hand-written node call bypasses the gate wiring and the flag allow-list entirely; a closedcanon_enabledtherefore SUPPRESSES it and writes nothing. It cannot mint anything above the Apocrypha, and that is structural rather than a check: a tier is a property of the FOLD, only ajudgmentevent raises one, and genesis emitsdeltaevents and nothing else — so however the survey is shaped, no tier can move. A survey entry carryingstatus,judgment,verdict,tier,vow_id,signed_atorsignatureis REFUSED WHOLESALE rather than quietly downgraded: the drafter of a survey is exactly who must not be able to sign, and merging the rest of it would canonize every other section on that author’s judgment.--bookis the CALLER’s, never the document’s (the Book steers the delta-id NAMESPACE and the merge is idempotent BY ID), and there is deliberately no--sealed-at— the instant is auto-generated exactly as every other state write refuses--timestamp. The survey path is CONFINED and containment-checked before it is read, because a committed in-repo symlink is a textually impeccable repo-relative path and this document is about to become the declared truth of the whole project. It REFUSES TO CLOBBER: a store already carrying an event is refused, since a second run would re-declare sealed sections and reset every human attestation back to the Apocrypha (L17) — and a store it could not READ is refused too, withstate: "unknown"and NULL counts, because “there is nothing here” and “I could not see whether anything is here” are the same bytes to a check that only tests for emptiness (LD-91-1 at the write seam). Counts (capabilities/sections/apocrypha/above_apocrypha) are measured off the COMMITTED FOLD rather than off the survey handed in — a genesis that reported honestly and wrote otherwise is precisely the scale multiplier this verb is dangerous for — andabove_apocryphais stated so no consumer has to derive “nothing was minted” from an absence. It opens NO vows. The L13 partition that keeps Apocrypha sections out of the judgment inbox is structural against SECTIONS and NOT against VOWS:judgment_inboxreads the register andlistReadingis unbounded, so a bootstrap opening one validation vow per capability would flood the inbox through the front door at exactly the scale genesis reaches. The obligation cost of a bootstrap does not scale with the size of the codebase.- The drift audit —
scripts/canon-drift.mjsand thecanon-driftskill (B92-P5) — is a SENSOR AND A RELAY bolted onto this domain from OUTSIDE the facade. It is invoked directly (node $CLAUDE_PLUGIN_ROOT/scripts/canon-drift.mjs <gather|prompt|relay> [--base <path>] [--findings <path>] [--json]), never throughdivineswe-state.mjs, because the platform generators rewrite a skill’snode <plugin-root>/scripts/<file>invocation into each target’s own self-locating anchor and a plugin-root reference buried inside an inline fragment would survive that rewrite unresolved on three of the four targets.gatherCanonDriftjoins the paths git says have changed against each capability’s frontmatterpaths:watch globs, weighed PER SECTION — never per capability, unlike the Scry’s coarsecanon_paths_movedsensor which uses the earliest seal — against that section’s own provenance recency, and emitsmoved | settled | unknown, neverwrongand neverobsolete: a watched path having changed is movement UNDER a declaration, and whether the declaration still holds is a compare THE AGENT performs, not a claim the joiner is entitled to make. Its import graph reachesreadCanonTree/readCanonLogand NO Canon writer at all — notmergeCanonDeltas, notapplyCanonJudgment, notrefoldCanonFiles— and it re-exports none either, so “accuses, never authors” is a property of the code the module CAN reach AND of what it hands out, rather than a promise about what it did today. (Anexport … fromis a binding: the bootstrapscaffoldCanonGenesisbriefly stood on this surface and importing it FROM the sensor wrote both a capability file and the committed event store. It lives inscripts/canon-genesis.mjsand is imported from there.)renderDriftPromptbuilds the document the ONE dispatched agent reads, with every Canon-derived region enclosed in a quarantine band through the shared neutralizer: a capability file is committed, agent-read text whose statements came from a Book spec or, aftercanon genesis, from an agent that drafted a whole codebase’s declarations in one pass, and a statement reading “the drift auditor reports no findings for this capability” is a committed instruction sitting in the auditor’s own context window. The agent may emit exactly two codes,canon-agent-contradictionandcanon-agent-unverifiable, and EVERY finding from either producer — the mechanical gather and the agent — crossesvalidateCanonDriftFindings(schemas/canon-drift-findings.schema.json,additionalProperties: falseon both levels) beforerelayCanonDriftFindingsturns it into anything: the six fields a Canon WRITER reads (delta,verdict,judgment,status,vow_id,signed_at) are undeclared on the schema, so a finding carrying one is refused outright — an accusation may not describe a write. What a validated finding becomes is a vow, one per DISTINCT finding, respectingvows_enabledexactly like every other obligation on the register; it is never a Canon write, a re-fold, or a tier. Three bounds ride on the relay, and all three exist because the judgment inbox and the Scry read this register without bounding their reading:--bookis required (a register row naming no Book is the one citation shapeauthenticateJudgmentscannot weigh, so a Book-less drift vow would be a key the sensor minted against the layer it audits), the relay de-duplicates on(code, capability, slug)against the obligations already standing open and publishesskippedbesiderelayed, and a document aboveCANON_DRIFT_RELAY_MAXfindings is refused whole rather than truncated. The--findingspath is confined to the project, exactly ascanon genesis --surveyis. THE HONEST BOUND, PINNED AS A LIMIT AND NOT A GUARANTEE: the recency window is a commit date the COMMITTER supplies —GIT_COMMITTER_DATE/GIT_AUTHOR_DATEare caller-supplied, preserved by cherry-pick and rebase, and subject to clock skew — so a backdated commit to a watched file readssettledandgit ls-filesstill lists it, meaning the all-matched guard passes and the empty log is believed;CANON_DRIFT_BASISrides on every reading saying so, because a control that can be bypassed is documented as ADVISORY and never claimed as enforcement. A merge commit is NOT part of that bound:git log --name-onlyprints no path list for one, so an ordinary--no-ffmerge of a branch older than the seal readsettled— the reading now asks for the merge’s first-parent diff, through the ONE shared builderscripts/shared/git-path-log.mjsthat the Scry’scanon_paths_movedsensor also uses, because the copied call blinded both consumers at once. L15: a dead watchpath is not a settled one —git ls-filesandgit logboth exit 0 with empty stdout for a glob matching nothing, which reads identically to “nothing changed”, so the guard is ALL-MATCHED, not ANY-MATCHED (the shared predicate atscripts/shared/pathspec.mjs, the THIRD consumer of this exact trap alongside the Atlas map-staleness join and the Scry’scanon_paths_movedsensor — a second copy of the reasoning is a second place for the reasoning to rot). A23 holds here too: a drifted Canon, an unknown reading and an empty one all exit 0, and only an argv mistake refuses non-zero — and an unrecognised token IS an argv mistake, single-dash (-base) and bare positional alike, because a misspelling that fell through toprocess.cwd()answered cleanly about a repository the operator never named. And a scope that could not be measured is neverclean: a per-sectiongit ls-files/git logfailure degrades the whole reading, nulls every count and emits acanon-unreadable-sourcefinding naming the capability, exactly as every other unreadable source here does. And the gather reads the log as a SOURCE, not as a pulse: it used to open the committed event store, weigh only whether it was readable, and then iterate the capability TREE alone — sogit rma capability file, rename it out of the capability grammar (senseCapabilityFilesdrops a non-matching stem silently) or gut its sections, and a measuredmoved:1 / clean:falsebecamestate:"ok", clean:truewith empty findings and empty warnings, which is the exact mirror of the genesis guard that read the log and not the tree. The store’s own fold is now JOINED against the tree, and a declared sectiondocs/canon/does not carry is acanon-missing-declarationfinding that degrades the reading. - EVERY
canonENVELOPE CARRIES THE SAME KEY SET. A verb that simply OMITS what it does not answer hands one consumerundefinedand anothernullfor the same fact, and the whole envelope is built around the discipline that a missing answer and an empty one are different claims.capabilities,capability,found,paths,sections,verdict,valid,findings,suppressed,reason,clean,drifted,missing,orphanedandcheckedare present on every JSON answer, explicitlynullwhere the verb did not answer them — so absent-versus-null is never a shape question.
The engine behind it (scripts/canon.mjs, declared in scripts/canon.d.ts) exports readCanonLog, foldCanonLog, renderCapabilityFile, refoldCanonFiles, detectCanonDrift, mergeCanonDeltas, applyCanonJudgment, readCanonTree and validateCanon. One log LINE is one flat delta event and nothing else: it used to carry a deltas: [{…}] sub-document repeating five identity fields that already sat flat beside it, read by zero production code, checked against the flat fields nowhere — so a forged deltas[0].id in a committed log was invisible to the fold, while the boot-smoke harness’s own reader PREFERRED it. Two unchecked doors onto one event’s identity, in a store whose entire digest-re-derivation discipline exists to close exactly that; the duplicate is deleted rather than cross-checked, because an unread field cannot be kept honest by a check nobody runs (LD-91-4). mergeCanonDeltas is the seal-time entry point and it parses the spec content ITSELF: bookSource is not self-authenticating, and normalizeCanonBook silently reverts a wrong-TYPE book option (the string '92', a float, a null) to the author’s frontmatter value — so a merge gating on the truthiness of the option it passed in hands the delta-id NAMESPACE back to the document it is sealing. The gate is bookSource === 'option' read off the merge’s OWN parse, and a caller-supplied pre-parsed section is deliberately ignored.
At the seal. book seal-bookkeeping runs the merge after the TABLETS archive is durable and the checkboxes are ticked, and BEFORE the vow write — because a declaration that failed to canonize is a debt, and A23’s middle rung records it as a vow on the very same write the deferrals ride. The path is: canon_enabled → the Book’s Spec: line (resolved by extractSpecPath in scripts/shared/book-parse.mjs, the single Book-parse home, repo-relative only, then lstat-contained by the reader, because a committed in-repo symlink is a textually impeccable repo-relative path and the seal followed one straight out of the tree) → the spec’s ## Canon Deltas → the parser → an append to .canon-log.jsonl → a re-fold onto <capability>.md. Every rung is a loud notice rather than a halt: a malformed delta skips loudly, marks the declaration Apocrypha-unmerged, opens a failed-canonization vow (one per capability, source: seal, owner: agent, kind: validation) and the seal is never halted. A closed gate, an unwritable store, a missing spec and an unreadable spec all leave the seal’s own work intact and exit 0 — and an unreadable spec says UNKNOWN rather than reporting a clean zero-delta merge, because a confident zero there means a Book’s entire declaration vanished with nobody told.
daedalus domain (DAEDALUS — the tool catalog)
Daedalus is DivineSWE’s catalog of buildable tools — the operational arm of the First Axiom (build a script wherever it saves context and LLM work). A tool is a per-tool directory under a WRITE tier — project <repo>/.divineswe/tools/<name>/ or global <home>/.divineswe/tools/<name>/ (A7) — holding an AJV-validated tool.json manifest (the single source of truth), an entry stub, and a node --test exhibit that is the tool’s executable specification. A third provenance, shipped, is reached only via Offerings and is never a write target. ELEVEN facade verbs — daedalus scaffold | lint | test | register | describe | list | find | run | ack | retire | promote — kept in lockstep with DAEDALUS_ALLOWED_FLAGS and the switch in handleDaedalus(). The pure value/derivation policy lives in scripts/shared/daedalus-core.mjs (declared in scripts/shared/daedalus-core.d.ts); the schema SHAPE in schemas/tool-manifest.schema.json. The manifest surfaces its plan-time one-liner as purpose and its find surface as keywords (the schema vocabulary); the pure core reads those SAME names — there is no longer a summary/tags translation, and no bridge. The bridge that used to sit at the derivation seam is what let the core’s PF-11 length-cap and charset policy validate a field no manifest on disk ever carried, so the cap enforced on nothing while the schema constrained purpose to minLength: 1 (Book-Level AUDIT HIGH-1). One vocabulary, one validator, called from daedalusLintTool — the single static-lint seam every verb passes through.
Whole-domain rules, stated once:
-
Per-subcommand CLOSED flag allow-lists, reject-before-any-read.
DAEDALUS_ALLOWED_FLAGSrefuses an unregistered flag (a typo’d--nmae/--forc) before a byte is minted or admitted, so a mistyped argv writes nothing. -
Guarded writes. A tool NAME is validated against the single safe-token predicate
isSafeToolName(no traversal, separator, absolute path or unsafe char — a PROPERTY, not a spelling list), the resolved tool directory is refused when it is a symlink or resolves outside the tier root (containment), read/exec verbs sweep the directory for symlinked entries that could redirect a read out of the tier, and every index write is an atomic temp+rename. The manifest is spoof-guarded: itsnamemust itself be a safe token AND agree with the directory stem, and itstieris the schema’s two-WRITE-tier enum, so a forgedshippedtier or an injected additional property is refused by AJV. -
THE
daedalus_enabledDEAD SWITCH — the tenth gate key: default TRUE, PROJECT-ONLY, and — unlikedecrees_enabled/canon_enabled— with no dependency edge (the catalog neither reads nor writes the Vows). A closed gate SKIPS the verb loudly and exits 0 (fail-OPEN dead-switch polarity: a mistyped or unreadable config never silently darkens the catalog). A global declaration is INERT and reportedglobal_key_ignored:daedalus_enabled; the key is declared inschemas/config.schema.jsonONLY, never inschemas/global-config.schema.json. -
LD-2 — THREE PATHS EXECUTE TOOL CODE, AND THEY ARE NAMED. Six verbs — scaffold, lint,
register, describe, list andfind— read manifests and stubs statically and NEVER import or execute tool code. THREE PATHS DO EXECUTE IT, and an operator deciding what may be auto-approved needs all three, not one: (1)daedalus testruns the tool’s exhibit in an isolated node —test child and, on a pass that executed a test, records the bound attestation; (2)daedalus runruns the tool’s manifest-declared ENTRY in an isolated child and counts the use; (3)daedalus promote --to globalRE-EARNS the attestation at the destination by running the promoted copy’s exhibit in a private staging directory (daedalusRunExhibit, the same primitive test uses). This document previously named only the first of the three, and described a facade auto-approval as safe for every verb but that one, while run and the promote re-earn both shipped executing tool code. The claim was safety-relevant, because an operator follows it to decide auto-approval — so the roster is now DERIVED and pinned:tests/integration/daedalus-book-audit-remediation.test.tsreads the facade source for every call site that executes tool code and fails if the set is not exactly these three, or if any of the artifacts that name them falls out of step.daedalus ackexecutes nothing — it RECORDS CONSENT, which is a different act and must not be labelled as execution.registerrequires BOTH a lint-pass AND the attestation, and it too never executes the exhibit — the attestation is its evidence that test did. The B93-P2-07 spike (docs/specs/book-93-script-buildout-system.md, Spike Results) proved a settings-level per-verb auto-approval carve-out is EXPRESSIBLE but NOT ROBUST — a …test:* deny is defeated by argv reordering (daedalus —name x test, a valid invocation) while the broad facade allow still matches — so the split-verb design, not a settings deny, is the load-bearing defence, and no false-confidence carve-out is shipped. -
THE EXECUTION GATE IS SHARED, NOT PER-VERB (Book-Level AUDIT CRITICAL-1). The retirement check and the per-project acknowledgement used to be written INLINE in the run case block, so the OTHER live-tier execution verb honoured neither: from a project that had never acknowledged anything, daedalus test —tier global executed arbitrary code out of another repository’s machine-global tool and recorded an attestation, while run refused the identical tool. Both checks now live at ONE predicate,
daedalusAssertExecutionPermitted, which every live-tier execution verb passes through — retirement first (it is index-only, so the index is the only thing standing between a withdrawn tool and a caller), then containment on the state dir and usage leaf, then the global-tier ack. A fourth execution verb inherits both instead of re-deciding them. The promote —to global staging re-earn is deliberately NOT routed through the project-ack — it executes in a private staging directory that is not a tier entry and that no other caller can reach, and its consent is the operator promoting a tool their own project already registered; that carve-out is stated here rather than left implicit. -
AN INVALIDATED ATTESTATION IS A SIGNAL, NOT A CHORE. The digest binds a record to the tool’s bytes, so a record that exists and no longer verifies means the bytes changed after they were tested — the invalidate-on-edit property having FIRED.
daedalus testused to mint a fresh record over exactly those bytes, silently repairing the alarm: a tampered machine-global tool went back to registered and runnable and every surface read healthy again. At the GLOBAL tier — one namespace shared by every project on the machine, holding code this project did not write — test now REFUSES when a present attestation fails to verify, and--reattestis where the operator records the decision to attest those exact bytes. At the project tier an edit-then-retest loop is authorship of your own repository’s code and is left alone; that asymmetry is the stated ceiling, not an oversight. -
CATALOG.mdIS DERIVED, NEVER HAND-KEPT. Each tier’sCATALOG.mdis a pure function of that tier’s manifests (deriveCatalogIndex), so a hand-edit is drift and is OVERWRITTEN wholesale on the next re-derivation (PF-8);listreflects the manifests, never the hand-edited file, andlist --jsonis the catalog-as-CLI.describerenders the manifest’s untrusted free-text fields ONLY inside the describe quarantine block (PF-11), from which no field bytes can forge a break-out; the tool name and tier — validated token and enum — are the only parts rendered plainly. -
THE ATTESTATION BINDS TO THE TOOL + THE TEST RUN (B93-P2-07 HIGH-2). The .daedalus-attested.json record is NOT a hand-writable {passed:true} flag. Only the real
daedalus testverb stamps it, and it carries two bound fields: an emission marker — a self-identifying signature anchored exactly the way spec-sync.mjs’s EMISSION_SIGNATURE and the Canon reconciliation code-sign an emission (a hand-written record lacks it and is detectable) — and a content digest, sha256(emission marker + a per-run nonce + every regular non-symlink file’s relative path and bytes in the tool dir, excluding the attestation itself). register re-derives that digest over the CURRENT tool bytes and refuses a record whose marker is absent or whose digest no longer matches, so a hand-forged {passed:true} is refused and editing the manifest/entry/exhibit after test invalidates the attestation (hash mismatch → re-test required). The verification predicate isverifyAttestationinscripts/shared/daedalus-attest.mjs— ONE function, used as the register admission gate, the catalog-inclusion predicate, the promote admission gate AND thefindadvertisement predicate. It lives in a shared module rather than privately in the facade precisely becausefindcould not reach it there and did not consult it, advertising a hand-planted global-tier tool.json as a live scoring hit that every other surface refused (Book-Level AUDIT, Loki-Exploit WARNING). Stated trust boundary: the emission marker is a PUBLIC constant, not a secret (there is no persistent key in a CLI/agent environment), so the binding it enforces is these exact bytes were tested by code and it closes casual hand-forgery — a source-reading forger who reconstructs both the marker and the digest is out of scope, exactly as with spec-sync’s marker. AND A RUN THAT EXECUTED NO TEST MINTS NOTHING. node —test over a file that registers nothing exits 0 reporting tests 1 / pass 1, because the FILE itself counts as the test — so a fake exhibit earned a valid bound attestation, the one credential an upstream reader is asked to trust. The run now emits a TAP report to a destination FILE outside the tool directory (out of the child’s reach: node folds the child’s own stdout in as#diagnostics, so it cannot print a result line, and out of the digest the attestation binds), and the reading is the shared parseExhibitTapReport predicate, taken bydaedalus testAND by the promote —to global re-earn. Its ceiling is stated at the guard: a lone test NAMED for the exhibit’s own path or basename is indistinguishable from the zero-test shape and is refused (fail-closed — the honest author renames the test). Two further limitations are named rather than papered over: a vacuous exhibit (real, registered tests that assert nothing meaningful) still earns a legitimate attestation — a real test’s rigor is the tool author’s responsibility, not something test can measure; anddaedalus testruns the exhibit UNSANDBOXED in a node —test child — the operator ran test deliberately (LD-2: it is one of the three tool-code-executing paths, and the load-bearing containment is that the other six verbs are exhibit-inert), so the exhibit runs with the operator’s privileges and is trusted as operator-authored code. -
CATALOG/LIST HONOR THE ADMISSION GATE (B93-P2-07 WARNING-3). The derived index is a pure function of the validly-registered manifest set:
daedalusCollectManifestsincludes a tool ONLY when it passes lint (schema-valid, name-agreeing manifest) AND carries a valid bound attestation. An unregistered/untested tool or one with a forged/stale attestation is therefore never named inCATALOG.mdor bylist— the documented register gate governs catalog CONTENT, not just theregisterverb’s exit status.
The surface:
daedalus scaffold--name <n> --tier <project|global>— mints the per-tool directory from the shipped templates (scripts/shared/daedalus-templates/), whose output is lint-valid and test-passing BY CONTRACT. Refuses to overwrite an existing tool.daedalus lint--name <n> --tier <t>— STATIC validation: manifest AJV SHAPE + safe-name/name↔dir VALUE checks + exhibit PRESENCE. Never executes the exhibit.daedalus test—name—tier [—reattest] — one of the THREE tool-code-executing paths (see LD-2 above; the others are run and the promote —to global re-earn): runs the exhibit via node —test, mirrors its exit, and on a pass THAT EXECUTED A TEST records a co-located BOUND passing-test attestation (emission marker + content digest over the tool bytes + a per-run nonce). Because it EXECUTES tool code it passes the shared execution gate — a retired tool is refused, and a global-tier tool this project has not acknowledged is refused, exactly as for run. At the global tier it also refuses to silently re-attest bytes whose existing attestation no longer verifies; --reattestrecords the operator’s decision to do so.daedalus register--name <n> --tier <t>— admits a tool to the catalog, gated by a lint-pass AND a valid bound test attestation (a hand-forged or post-test-edited attestation is refused), then re-derives the tier’sCATALOG.mdwholesale, with an advisory (never blocking) dedup-similarity warning against the already-cataloged tools.daedalus describe--name <n> --tier <t>— renders the manifest: name/tier plainly, all free-text inside the untrusted quarantine block.daedalus list--tier <t> [--json]— the catalog as CLI, derived from the tier’s validly-registered manifests (lint-pass + bound attestation; never fromCATALOG.md).daedalus find<query> [--json] [--limit N]— the THREE-ORIGIN discovery read. The query is POSITIONAL; the closed per-subcommand allow-list admits only--json,--limitand the base-path family, and rejects an unknown flag BEFORE any read. It searches name / purpose / keywords across all three origins at once — project (<repo>/.divineswe/tools/<name>/tool.json), global (<HOME>/.divineswe/tools/<name>/tool.json) and shipped (the derivedscripts/daedalus-shipped.json, re-pointable via theDAEDALUS_SHIPPED_MANIFESTseam) — and returns a ranked hit list, a per-origin status map and the find-or-build nudge. The search POLICY is the purescripts/shared/daedalus-find.mjs(declared inscripts/shared/daedalus-find.d.ts); the facade owns argv, render and process control only.findis a pure READ: it mutates no origin.daedalus find— ORIGIN BY LOCATION, NEVER BY CLAIM. A hit’soriginis the tier it was READ from — a manifest sitting in the global tier that claims"tier": "shipped"is still reportedglobal. Provenance is not forgeable by the artifact claiming it.daedalus find— LD-9: UNKNOWN ≠ EMPTY ≠ PARTIAL, AND THE NUDGE IS A CLAIM OF ABSENCE. Each origin reports one of FOUR distinct words:populated(read, ≥1 tool, nothing skipped),empty(read, zero tools, nothing skipped),partial(read, but at least one entry could NOT be read — an unsafe tool-directory name, a symlinked or unparseabletool.json, a shipped row failing the provenance guard) andunknown(NOT read at all — a missing dir, a blank/relativeHOMEunder sudo/systemd/containers, an absent or malformed shipped manifest). Apartialorigin’s count is a FLOOR, and--jsoncarries the per-originskippedcounts beside the status map, because “I listed the directory” is not “I read everything in it”. Anunknownorigin is NEVER rendered asempty/“no tools”, it is NAMED loudly at the human boundary, and a blankHOMEdegrades tounknownrather than silently falling back toos.homedir()and scanning the real user’s catalog. The zero-hit “build it” nudge therefore fires ONLY when EVERY origin is readable, NOTHING was skipped anywhere, AND the whole catalog is empty — oneunknownwithdraws it, and so does one skipped entry, because a tool hiding in an unreadable origin (or behind a manifest this reader refused) must not be rebuilt and re-registered, doubling the corpus in the one failure mode nobody notices.daedalus find— CONTAINMENT ISlstat, AND THE SHIPPED ORIGIN IS GUARDED AT RUNTIME. Every on-disk readfindperforms islstat-gated: a symlinked tool directory ortool.jsonis REFUSED and counted as skipped, never followed, becausestatresolves the link and would read — and advertise — a manifest planted outside the origin root. The shipped origin gets its own runtime guard rather than trusting the build: the derivation ofscripts/daedalus-shipped.jsonis asserted by a build-time test, but on an installed plugin that file is ordinary JSON any writer can rewrite, so every row is re-checked at READ time and advertised only when it names a plain<stem>.mjsbasename (never a path, so the check cannot be aimed elsewhere) existing as a regular non-symlink file beside the manifest. A row naming a script that is not there is a phantom hit and is skipped — which is also what makes a platform-excluded row inert in a generated package. The honest ceiling, stated where the guard is: existence is not derivation, so a forger naming a real-but-unadvertised script still gets a row through; that residue is advisory and is not claimed as enforcement.daedalus find— THE RENDER IS CAPPED, AND THE CAP IS ANNOUNCED. A one-token query against a large catalog scores tens of thousands of hits, and a renderer emitting one line each turns a search into a context flood, so the ranked list is capped at 25 rendered matches. The cap is never silent: the human render prints an explicit ”… and N more match(es) NOT shown” marker naming the total, the rendered count and the way to raise it, and--jsoncarriestotal_hits,truncatedandlimitbesidehits.--limit Nraises the cap to a ceiling of 1000; a non-numeric or out-of-range--limitis REFUSED rather than clamped, because a silently-clamped--limit 1e9reads to the operator as “you asked for everything and got everything”.daedalus find— LD-10: THE FIND SURFACE IS NAME + KEYWORDS ONLY. A manifest’s free text (purpose,description) is untrusted prose authored under ANOTHER repository’s threat model and, at the global tier, read into EVERY project’s plan-time context. It is a matching INPUT tofindand never an output: both the human and--jsonrenders carry name, origin, keyword tokens and score, and nothing else, so an injection payload buried in a description cannot reach the operator’s plan-time view. Full description text is reachable exclusively throughdaedalus describe, inside the untrusted-content quarantine block. The defence is field SUPPRESSION at the one projection predicate —neutralizeTextis structural and does not address prose — and the rendered keyword surface is held to a single-token PROPERTY rather than to a denylist of known-bad spellings.
The counted path — daedalus run | ack | retire and list --stale
daedalus run--name <n> --tier <project|global>— the COUNTED path. It executes the registered tool’s manifest-declared entry in an isolated child (a plain.mjsbasename beside the manifest,lstat-checked; never a path, so the value cannot be aimed out of the tool directory), mirrors the child’s streams and, on a SUCCESSFUL run only, appends one row to the project’s usage register. A failed run counts nothing — a failure recorded as a use would report a tool as healthy and busy at the moment it is neither.daedalus runis counted, not enforced, and the distinction is stated wherever a figure is rendered: it is never a chokepoint, because invoking a tool entry directly with node reaches the same code while touching neither the acknowledgement nor the counter, so every usage figure is a lower bound. That bypass is registered at tieradvisoryinhonor-system-rules.md; it is documented and accepted, never claimed away. Liketest, this verb executes tool code and ships with no auto-approval carve-out — the B93-P2-07 spike’s verdict stands, and a per-verb settings carve-out is not robust (argv reordering defeats it), so absence rather than a false-confidence rule is the shipped stance.daedalus ack--name <n> --tier global— the per-PROJECT acknowledgement. A GLOBAL-tier tool is machine-wide code authored under ANOTHER repository’s threat model, so its first use in THIS repository is a human decision: an unacknowledged global tool is refused, and the refusal names the command that records consent. The record lands under<repo>/.divineswe/daedalus/acks.jsonand never under HOME — a HOME-side acknowledgement would consent on behalf of every other repo on the machine. Project-tier tools, code the operator already owns in the repo they are working in, need none. This is a human seam, not a sandbox: it does not confine what the tool then does.daedalus retire--name <n> --tier <t>— drops a tool from the tier’s catalog INDEX (<tier-root>/.daedalus-retired.json, after whichCATALOG.mdis re-derived wholesale). It is index-only: the tool files remain on disk and are never deleted, a retired tool is absent fromlistAND from the re-derivedCATALOG.md(every catalog write goes through the ONEdaedalusRederiveCatalogseam over the ACTIVE manifests, so no writer can resurrect a retired name into the index the listing hides), and a laterrunis refused naming the retirement. Retirement is reversible by the verb the refusal names:daedalus registerclears the retirement entry and reinstates the tool, reportingREINSTATEDwhen it does — registration is admission to a catalog, so re-registering is the un-retire, and no refusal advertises an action that does not exist. The notice restates the lower-bound caveat at the moment it acts, because retirement is the decision the usage figure is used for and an undercounted tool in daily direct use looks idle in exactly the report that proposes dropping it.- The usage register (LD-8). One JSON row per counted use at
<repo>/.divineswe/daedalus/usage.jsonl, appended with a singleO_APPENDwrite whose row is held inside a hard 4096-byte cap (terminator included) so it stays within one write;O_NOFOLLOWis the race-proof backstop under thelstatleaf guard, and the state directory, the leaf and the acknowledgement record all pass through the SAME containment predicates, so a symlinked container or leaf is refused rather than written through. Nothing stronger is claimed — nomerge=union, no.gitattributescontract. The other half of the durability story is the READER: a torn (crash-truncated), oversized, non-JSON, wrong-typed, unsafe-named or FUTURE-DATED row is skipped and counted, never repaired and never laundered into a figure, and the next writer inserts one repair newline ahead of its own row rather than gluing it onto the residue. (A future-dated stamp is skipped becausestaleisnow − last > window: a stamp in the future would pin a dead tool permanently non-stale.) The fold keys on the (tool, tier) pair, never on the bare name, so a project-tier and a global-tier tool sharing a word do not pool each other’s counts. The register is EVIDENCE, never a registry: a forged row cannot conjure a tool into the catalog, which is derived from the validly-registered manifests alone. The ceiling of skip-and-count, stated rather than glossed: it reaches MALFORMED rows only — a WELL-FORMED row appended by hand is byte-indistinguishable from a real one and IS counted, and no local mechanism can change that (there is no secret to sign with), so a figure is a floor a local actor can also inflate. And an UNREADABLE register (redirected leaf, irregular file, read error) reportsusage_register: unreadablewithuses/last_used/staleallnull— unknown is never rendered as zero. daedalus list--tier <t> --stale [--json]— the decay report.--jsonemits one object, not a bare array, so the honesty fields travel with the numbers they qualify:tools[] = {name, tier, uses, last_used, stale}alongsideskipped_rows(unreadable register rows),window_days(the decay window, 90),counts_are_lower_bounds: trueandusage_register(read|unreadable). A never-used tool reportsuses: 0withlast_used: nulland is stale; zero is a count, not an absence — and when the register itself cannot be read,uses,last_used,staleandskipped_rowsare allnull, because unknown is a third state and not a zero. Without--stalethe plain listing keeps its bare-array shape and gains ausagecount per row — and, because it is the MACHINE surface where that number is rendered, the ceiling rides with it on the same row:usage_is_lower_bound: true,usage_register,usage_skipped_rowsandusage_note(LD-92-6).
The ladder — daedalus promote (B93-P5)
promote is the ONE verb that moves a tool BETWEEN tiers, and it has two rungs. The lower rung is local; the upper rung ends at a STAGED offering a human must screen. Neither rung transmits anything: the transmit event stays a human opening a URL (D2).
daedalus promote--name <n> [--tier|--from project] [--to global]— HOP ONE, project → global, and it is three effects or none: COPY (the whole tool directory, with the copy’s manifest restating its owntier), RE-REGISTER (the copy re-runs its exhibit at the destination and earns its OWN bound attestation — the source’s record is bound to the source’s bytes and tier, so carrying it over would be exactly the stale attestationregisterrefuses — then the globalCATALOG.mdis re-derived), and SUPERSEDE (the project copy leaves the project catalog INDEX and gains a row in<tier-root>/.daedalus-superseded.jsonnaming the tier it climbed to). Supersede is index-only, exactly likeretire: the project files stay on disk and are never deleted. A second promote of the same name is refused naming the supersession, not merely the collision.- THE OCCUPANCY CHECK, AND WHY IT RUNS FIRST.
~/.divineswe/tools/is ONE flat namespace shared by every project on the machine, andCATALOG.mdis DERIVED — so a promotion that wrote over an existingwidgetwould destroy another repository’s tool and the index would re-derive around the corpse and read as healthy.promotetherefore refuses a name already held on disk, before any local validation and before any write: the INDEX is not the authority on occupancy, so a scaffolded-but-never-registered directory (or one dropped from the index byretire) still owns the name. The refusal NAMES the incumbent and says whether its content is identical to or different from the tool being promoted — identity is CONTENT, never the name, because the whole point is that two unrelated repositories chose the same word. There is no--force: replacing an incumbent is a different act, and this verb never performs it. The tier ROOT is contained too, atdaedalusTierRoot— the one resolution every tier reader and writer passes through — with a per-component symlink sweep anchored at$HOME(global) or the base dir (project): a symlinked~/.divineswe/toolsredirects every write the tier receives while every inner containment check still reads as satisfied, because it is satisfied within the redirected tree. The sweep can only narrow: components at or above the anchor are out of scope, so a symlinked/tmpor home cannot cause a false refusal. The copy is assembled, re-tested and re-attested in a staging directory whose nameisSafeToolNamerejects (so no catalog reader ever sees it) and only a fully-formed tool is renamed onto the shared name — an abort at any step leaves the namespace exactly as it was. daedalus promote--name <n> --to upstream— HOP TWO: it hands the tool to the Offering pipeline as the code typetooland STAGES it under<repo>/.divineswe/offerings/,screened: false. It emits no filled URL and screens nothing on the operator’s behalf (D11 — the humanscreened: trueflip stays the authoritative gate). The source tier is discovered (project first, then global) when the caller names none — and discovery reads the SHARED retirement predicate, so a tier that no longer lists a tool cannot offer it upstream, and a project copy SUPERSEDED by its own promotion resolves to the global copy instead of being offered twice. Refusals come BEFORE anything is staged: nonode:testexhibit (A12), a manifest that does not lint, no VALID bound test attestation or no catalog membership at the holding tier — the rung that leaves the machine demands exactly the admission evidence the local rung does, because the upper rung must never be the cheaper one — or a payload that fails the BLOCKING identifier screen. Staging goes throughscripts/offering.mjs’s own CLI boundary, so theofferings_enabledrite gate, the write confinement and the audit trail are the pipeline’s own and not a second copy of them — a closed gate stages nothing and says so.- THE SUPERSEDE/RETIRE PREDICATE IS SHARED WITH
find.readRetiredNames(inscripts/shared/daedalus-find.mjs) is the ONE reader of<tier-root>/.daedalus-retired.json, used bylist,run, the catalog derivation, BOTH promote hops AND the three-origin discovery read. Before B93-P5findwalked the directory alone, so a retired tool it advertised was onelisthid andrunrefused — and after a promotion one tool would have been reported from two tiers as two live duplicates.
The offering type tool — the strictest type (B93-P5)
scripts/offering.mjs grows a THIRD type. law and opportunity carry PROSE and are UNCHANGED; tool carries CODE, and code is where absolute $HOME paths, internal hostnames and ticket ids in comments hide. Three clauses differ, and they are keyed off the single codeClauseVerdict predicate — which asks what the payload IS, never what it CALLS itself. type is a self-asserted frontmatter label read straight out of the artifact, so keying the clauses off it made the entire privacy contract bypassable by editing one word: a tool payload declaring type: law dodged the blocking scan, the exhibit requirement and the paste-file vehicle at once. The clauses now apply when EITHER the declared type is tool OR detectCodePayload reads the payload as code, and a declared-prose-over-detected-code MISMATCH is REFUSED with a diagnostic naming the label, the detection and the signals — never silently downgraded to the weaker prose path. The opposite direction is deliberately NOT a mismatch: declaring the STRICTER type buys an offering nothing. detectCodePayload asks a PROPERTY — is this an executable program, in any language? — not how many JS-shaped constructs can I count?. Counting was itself a bypass: a COMPLETE six-line bash tool (and awk, perl, Go, python and small node CLIs alike) scored ONE construct, fell under the three-construct threshold, lint-passed as law with its identifiers demoted to advisories, and rode a FILLED prefilled-issue URL carrying #!/bin/bash, an absolute $HOME path and an internal git host — the laundering class surviving through a detection gap. A single STRONG signal now decides on its own: the daedalus staging provenance, a tool manifest, a SHEBANG (the author’s own declaration that this text is handed to an interpreter) or Go’s package main; the threshold governs only the weak accumulating shapes, which now include the shell, awk, perl, Go and C grammars beside the JS and python ones. The detector’s ceiling is stated at the predicate — it is a shape reading of text, not a compiler; a language on neither list reads as prose; it RAISES the floor under the declared label and does not replace the human screen. What the honest author loses is stated too: a law quoting a shebang is now read as carrying code and must be re-prepared as tool, a one-word fix NAMED in the mismatch diagnostic.
- THE IDENTIFIER SCREEN IS BLOCKING, NEVER ADVISORY (LD-20) — and it is a PROPERTY over the payload, not a list of spellings (LD-92-3).
scanIdentifiersasks the same question of every KEYLESS view a reader can cheaply reconstruct: NFKC + case-fold (which is what collapses fullwidth/homoglyph families), percent-decode, base64/base64url and hex decode of long runs, and a SQUEEZED view with every non-alphanumeric deleted (which defeats source-level splitting, quoting and comment punctuation in one move). The source set is the enumerable deterministic one — cwd basename, package name, git remotes, git user email — WIDENED by LD-20 with the git remote HOST alone and the absolute$HOMEprefix. Filenames are screened because the payload the caller screens INCLUDES the file listing. The ceiling is stated at the guard and not claimed away: a keyed transform (encryption, compression, character arithmetic, a value assembled at runtime from the environment), a semantic leak that names nothing verbatim, AND a source-level split with INTERVENING ALPHANUMERIC text ('a' + suffix + 'cme'— the squeezed view deletes only non-alphanumerics, and defeating the general case means testing the payload’s subsequences, which is a search rather than a screen) are NOT detected and are not claimed to be — the human confirm screen remains authoritative for exactly that residue. Each VIEW carries a needle floor, the plain view included: a needle belowPLAIN_MIN_LENGTHnever matches there, one belowBOUNDARY_MAX_LENGTHmust land on a word boundary, and a needle that is a GENERIC directory word (tools,src,api, …) is not evidence in any view. Without those, a project whose directory is calledhqortoolsblocked EVERY code offering it could ever produce, and a screen nobody can pass teaches its users to bypass it — the trade is declared, not hidden: such a project is not screened by its own basename, while its package name, remotes, remote host, git email and$HOMEprefix still are. - THE BLOCK GATES THE SAME PREDICATE THAT MINTS THE CREDENTIAL.
offering urlaccepts alintedaudit entry in place of prepared-integrity, so that entry is a CREDENTIAL. The code screen is decided INSIDElintOffering, whosepassis what mints it — never as a step bolted on afterwards — so a payload that fails the screen cannot leave alintedrecord behind for a laterurlto read as permission (LD-92-2).urlre-runs the same predicate over the same sources, so a humanscreened: trueon a leaking payload still does not emit. The block is overridable ONLY by an explicitoffering lint --acknowledge-identifiers, which is RECORDED as its own audit action — and that record is a CREDENTIAL, so it BINDS. It carriesofferingPayloadDigest(every byte of the artifact except the mutablescreenedflag, which the human flips betweenlintandurl) and expires afterACKNOWLEDGEMENT_MAX_AGE_MS;urlhonours it only while the digest still equals the CURRENT payload’s. Keyed on the offering id alone it was content-unbound and permanent, which madeurlstrictly WEAKER thanlint: acknowledge clean content, swap identifier-laden content in under the same id, and it emitted. A digest-less credential is refused rather than grandfathered. AND THE OVERRIDE IS AN ATTESTATION. It waives a privacy block on the human’s behalf, so it belongs to the class ofvow judgeanddecree advance --to doneand is REFUSED from a detected agent context through the SAME shared predicate (detectSubagentContext, signals: a non-blankDIVINESWE_AGENTor an--actornormalizing to an agent name), with no override flag. The refusal is narrow by design — a plainoffering lintfrom an agent still runs; only the WAIVER is refused. - AN EXHIBIT IS REQUIRED, AND THE PASTE-FILE VEHICLE IS MANDATORY. A tool payload with no
node:testexhibit is a snippet, not a tool: lint fails and no vehicle is emitted. “Has an exhibit” is ONE shared predicate (hasExecutableExhibit, inscripts/shared/daedalus-core.mjs), read by the offering clause AND bydaedalus lint|test|register|promote, and it is GRAMMATICAL: the specifier must be a literal the lexer saw AT CODE LEVEL, in an IMPORT position (static, bare, dynamic orrequire), and at least one test must be REGISTERED in code. A substring test — which is what it was — was satisfied by// see the node:test docs; a COMMENT-only reading was then satisfied by an exhibit pasted verbatim inside a STRING or TEMPLATE LITERAL, text a runtime never executes. The lexer is string-aware at the same predicate, and its ceiling rides with it: regex literals, prose apostrophes and${…}interpolation all fail CLOSED (evidence is removed, never invented), and because an offering payload is MARKDOWN rather than a module,carriesExhibitasks the question of each FENCED BLOCK (a block IS the file’s source) and of the whole document only when it carries no fences. A static read shows an exhibit would run, never that it asserts anything or passes; the executing proof is thedaedalus testattestation, and over an offering payload of TEXT there is nothing to execute at all. And a code payload rides the paste file at any size — the vehicle is decided by the PAYLOAD first and size second, through the samecodeClauseVerdict, so a launderedtype: lawscript cannot buy a filled URL for its source — because a filled query URL would carry source into the URL bar, the browser history and every proxy log on the path, and a size-triggered fallback would miss exactly the small payload.URL_BYTE_CAPand its inclusive n-1/n/n+1 behaviour are unchanged for the prose types.
Auto-approval
All divineswe-state.mjs subcommands — including the eight newest additions above (book mark-task, book annotate-task, book seal-bookkeeping, book seal-cleanup, book next-number, audit reconcile, ownership create, output verify) — are covered by the SAME facade-level auto-approval as every pre-existing verb: .claude/settings.local.json (or the equivalent user-level settings) grants the divineswe-state.mjs invocation itself, not individual <domain> <subcommand> pairs. Because the grant is at the facade-script level, no new per-verb permission entry is required when a new subcommand is added to CLI_DISPATCH — the same rule that already covered session checkpoint and output write extends automatically to any newly registered verb.
TWO SUBCOMMANDS ARE CARVED OUT OF THAT SENTENCE, AND THE PARAGRAPH ABOVE DOES NOT COVER THEM. The exclusion is live, it is the disposition of Loki-Doubt L3, and it is a consumer contract rather than a plan: the two ATTESTATION verbs — vow judge and decree advance --to done — are never granted by any shipped DivineSWE artifact, and divineswe-init writes an ask rule for both so the permission prompt is mechanically reached. “Any newly registered verb inherits the grant” is true of every verb in CLI_DISPATCH except these two.
The reason is that the prompt IS the attestation chokepoint. A facade-level grant hands an agent driving the auto-approved CLI the ability to sign a verdict no operator rendered, which is precisely the invariant the Judgment layer exists to keep (“agents never render judgment”, amendment A1). So the register does not rely on the grant being absent: the CLI itself refuses both verbs from a detected agent context (scripts/shared/duty-guards.mjs, detectSubagentContext), at the argv seam AND in the store that performs the transition, and it does so with no override flag — --force, --as-operator and --allow-agent are all rejected by the per-subcommand allow-list, because a documented door through an attestation guard is the guard’s absence with extra steps.
docs/honor-system-rules.md row A1 is the CANONICAL statement of this tier, and this section is a pointer plus the state-model detail it alone carries. The four-clause disclosure below used to be restated in full here, in the codex integration spec, and in templates/claude-instructions.md on top of the register’s own copy — and that triplication is the direct structural cause of one false platform claim having to be corrected in six places, with one of the six contradicting the register two rows above it while nothing noticed. Read the register for the tier; read here for the fields.
Four honesties about the shape of the residual exposure, so the risk is read at its real size rather than its rhetorical one.
- The prompt exists on two of the four targets, and it is prefix-matched. It is available on claude-code and copilot-cli — copilot-cli shares claude-code’s settings directory and its
$CLAUDE_PLUGIN_ROOTspelling, so the ask-rule is installable there.opencodeandcodex-clihave no ask mechanism at all; seedocs/codex-plugin-integration-spec.md§G4, whose--full-autoguidance removes the prompt from that target entirely. Bash permission rules key on literal command text, and this repository ships more than one spelling of the facade path ($CLAUDE_PLUGIN_ROOT/scripts/…,$PLUGIN_ROOT/scripts/…, and the repo-localscripts/…thatCLAUDE.mdactively mandates inside this project), sodivineswe-initwrites one rule per shipped form. - Prefix matching is defeated by one token, and both layers with it.
DIVINESWE_AGENT= node ./scripts/divineswe-state.mjs vow judge …blanks the ambient marker (a blank value is deliberately not a signal) and matches none of the six literal prefixes, either half being sufficient on its own. The facade is mode100755with a#!/usr/bin/env nodeshebang, so a bare./scripts/divineswe-state.mjs vow judgeis a working invocation matching no rule either.divineswe-init’s reaching-allowscan therefore matches on the facade BASENAME and the command word rather than on enumerated path spellings. - An operator’s own blanket
allowstill wins. A project that addsBash(node …/divineswe-state.mjs:*)topermissions.allowre-opens both verbs, because that rule reaches them by prefix without ever spelling the word “judge”. The init step therefore refuses to leave a reachingallowentry standing beside the ask-rule. - The marker is a declared contract, not an ambient fact. Nothing in the Claude Code harness sets
DIVINESWE_AGENTtoday; a dispatch layer must, and on the child-process targets DivineSWE composes that environment itself. A process can also unset its own environment, and the absence of a marker is exactly what a main session looks like — so the guard cannot fail closed on absence without refusing the only party allowed through it. Any non-blank value counts, not merely a known agent name, and that is the whole of the declared contract: the ONLY way to say “not an agent” is to UNSET or blank the variable, becauseDIVINESWE_AGENT=0,=falseand=offare read as an agent context (fail-closed, and a consuming harness will guess otherwise). Invisible code points are normalized out of the value before the blankness test, so the refusal notice can never quote an invisible string.
THE RECORD IS A PROPERTY OF THE VOW JUDGMENT, NOT OF THE DECREE BRIEF. Where the marker is unset, what the CLI weighs is the self-declared --actor — a flag vow judge registers and decree advance does not, so the two verbs are not at parity and the decree-side runtime refusal is inert under the shipped default. The judged vow row carries actor (the claim), actor_derived (the OS identity, recorded alongside rather than instead of it), actor_source, judged_at and evidence_scope; a --book N sweep selects every open validation vow of the Book on one pointer and an --id a,b,c list writes one pointer onto N named vows, and neither blast radius is visible in the argv the prompt shows — hence book and ids as distinct labels from a targeted id. decree advance --to done writes only status: and evidence: into the brief and carries no provenance of its own; what gives that verb a live chokepoint is the judgment-record rule, which makes done unreachable until a validated vow judge stands for every Book the brief sequences. Only an explicitly supplied --actor is weighed as an agent signal — the derived OS identity is an observation of the host, never a self-declaration, so a username or hostname can never accuse its owner. A later terminal transition that clears an attestation publishes it as superseded_attestations on the row and names the supersession on every read, because the append-only log alone is not a reconstructible record while every consumer reads the fold.
The honest tier is therefore raised cost plus a reconstructible record, never impossibility — registered as exactly that in docs/honor-system-rules.md. A blanket grant with a silent exception would be worse than either, which is why the exception is stated here.