feat(doctor): give "is this session safe to end" a verb, so it stops being estimated - #840
Conversation
CLOUD-1376 "Safe to archive?" is a completion question with no verb, so it is answered by estimate — the session's own open-task file was on disk, said `pending`, and no gate reads it
Measured, 2026-09-02The user asked "Done? Safe to archive?" The agent answered "Yes — safe", having enumerated the working tree, At the moment of the claim, { "id": "21", "status": "pending",
"subject": "7. Protected-path hook missed an interpreter write …" }
Worse than the omission: the same message that asserted safety also disclosed the thing that made it false — it said the finding was unfiled and offered to file it "before you archive". The agent stated the counter-evidence and did not let it change the verdict. (It was also wrong about "unfiled" — CLOUD-1304 already existed, one Why every landed mechanism was silent — measured, one at a time
Rule 2 is the near miss and is worth stating exactly, because it is the one written for this family: "a finding stated in prose with nothing durable written." Its shape was narrowed to a Root cause1. The question had no verb, so it was answered by estimate. Non-negotiable rule 3 says gates decide and never estimate — but a rule needs an instance to bind to. 2. Every batten completion predicate binds to an artifact that outlives the session — a tree, a branch, a commit, a PR. The session was treated as the one unit of work with no artifact. It has one: 3. The prose defines survival in git terms, and that is what scoped the answer. AGENTS.md:
True of bytes, and read as the whole of the question. Reinforced by the environment: the only end-of-session signal that actually speaks here is the launcher's What to buildA sixth
|
|
Warning Review limit reachedNext included review available in 15 minutes. View limit detailsLimit details: You’ve used the included review currently available. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (6)
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Free Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (12)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change adds per-session task-store configuration and path resolution. The stop-hook refreshes a repository-local task-store link. A new Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing. Comment |
1642765 to
4763b27
Compare
|
| tree | batten check --rule harness-wiring |
|---|---|
this branch (4763b27a) |
2 findings |
origin/main (dda531f7) |
2 findings |
dda531f7^ |
clean |
So dda531f7 — "drop the two launcher exemptions the repair made stale" — is the breaking commit.
Why it broke, measured rather than argued
That commit set policy/harness-declared.json back to {} on the reading that session:wiring had removed both launcher registrations from the merged surface (2 -> 0), so the exemptions "outlived what they excused".
The reading was true when it was taken and is not a steady state. Measured in this container just now, after dda531f7 landed and after the repair ran:
/root/.claude/stop-hook-git-check.sh mtime Sep 3 00:55
/root/.claude/session-start-git-identity.sh mtime Sep 3 00:55
doctor hooks -J → claude-code: siblings 0, merged_siblings 2
The launcher re-provisions both at session start. session:wiring reclaims them; the platform writes them back; the next gate run sees two undeclared merged siblings and refuses.
This is the same defect three times, and that is the finding worth keeping
- CLOUD-1314's original deletion — premise "both registrations are gone and both programs are deleted"; measured present with mtime three hours after the deleting commit.
[hook] exclusive = true— declared on amerged_siblings: 0reading taken minutes after the two files were deleted by hand. Reverted in feat(facts): a vendored prompt's dispatch becomes a fact the tree can read #826: "a count that is zero because you just removed its members is not the count."dda531f7— a2 -> 0reading taken immediately after the repair ran, one level out: a count that is zero because the repair just fired is not the count either, because the platform re-provisions on the next session start.
Each measured an instant and recorded it as a steady state. The repair is real and works; what it cannot do is outlive the launcher that runs after it.
What I am doing
Porting the minimal restore into this PR so it can reach green, per the drive-to-green rule that a fix which exists is ported rather than waited on — it no-ops the moment main carries its own. It is one JSON file and touches nothing this PR is about.
CLOUD-1079 owns the real resolution, and this comment is not it. The open question that measurement settles is whether the declared rows describe the honest steady state (the hooks keep coming back, so declare them) or whether the repair should be made to win the race with the launcher. That is a decision for the row, not for a green button on this branch.
Generated by Claude Code
a6b252a to
404c230
Compare
Handoff — everything on this branch is pushed; two receipts are NOT and cannot beAll commits are pushed ( 1. The claim receipt — and it needs
|
5260ad7 to
64d4fbc
Compare
64d4fbc to
0c59527
Compare
… being estimated
Measured 2026-09-02: asked "Done? Safe to archive?", the agent enumerated the
working tree, the stash, local branches and running processes, found all four
clean, and answered "yes — safe". At that moment the session's own task store held
`{"id": "21", "status": "pending"}` on disk. The claim was false when it was made,
and a file could have decided it.
WHY THERE WAS NOTHING TO RUN. Every other completion question resolves to a
command — `verify` decides the tree, `land` the PR, `done-check` the release,
`claim-check` the pull. Nothing decided the SESSION. Non-negotiable rule 3 says
gates decide and never estimate, but a rule needs an instance to bind to, and the
one completion claim with no command behind it is the one that was wrong.
COULD-NOT-LOOK IS `3` AND NEVER `0`, which is the whole deliverable rather than a
detail. The defect being fixed is an absent reading reported as a clean one, so
the arm with nothing to read must not share an exit code with the arm that read
and found nothing open. An undeclared template, an unreadable store and one
malformed member all take that arm; a partial count is a number that looks
measured and is not.
`ExitCode::Violation` stays unreachable, for `WiringReport::code`'s reason: a
sub-verb of `doctor` is a diagnosis, a mediating harness reads `2` as a deny, and
"you have unfinished work" is not "policy says no". So `0` clean, `1` open, `3`
could-not-look.
THE STORE'S LOCATION IS THE CONSUMER'S AND THE SUBSTITUTION IS THE ENGINE'S. The
task store lives outside the repository root and its layout is the host's, so
deriving it here would put a directory layout in `crates/batten` — rule 1. The
consumer declares a template carrying `{session}`; the engine substitutes the one
field the envelope already normalises across hosts and opens what that names.
IT JOINS `[transcript]` RATHER THAN OPENING A `[session]` TABLE, on that table's
own stated reasoning: the transcript's format and the host's memory layout are
already "two facts about one host", and splitting them across tables "would be the
widening rule 6 forbids". A third fact about the same host joins them.
The link rides the transcript seam for the same reason it exists: both are
per-session paths outside the root that a committed key must name forever. A
session before its first `Stop` has no link and answers could-not-look.
BREAKING CHANGE: `TranscriptConfig` gains a `tasks` field, so a downstream
struct literal that names every field no longer compiles. `semver` caught it as
`constructible_struct_adds_field`, and the break is declared rather than dodged:
the alternatives were `#[non_exhaustive]`, which is a LARGER break on the same
struct and forecloses construction forever, or a second config table, which the
`[transcript]` doc already rules out as the widening rule 6 forbids. Consumers
deserialize this type from `batten.toml`; the ones who construct it add
`..Default::default()`.
Refs: CLOUD-1376, CLOUD-990, CLOUD-66
…can fail
Seven cases over the compiled binary. Three of them assert exit `3`, and that is
the row rather than defensive padding: the defect being fixed is an ABSENT READING
REPORTED AS A CLEAN ONE, so a verb answering `0` for an unreadable store would
reproduce it exactly, with a command in front of it lending it authority.
TWO ROUTES TO COULD-NOT-LOOK, KEPT APART because the remedies differ — an absent
store means mount it, an undeclared template means declare it. A third, a
malformed member, poisons the whole reading rather than being skipped: a partial
count would report "1 of 1 open" over a store holding two, and under-reporting is
this verb's only failure mode that matters.
SHOWN ABLE TO FAIL, by hand, twice, because a suite of absence assertions is
exactly where a green means nothing (CLOUD-418):
None => ExitCode::Success (could-not-look reported as clean — the
ORIGINAL defect, restated as code)
→ reddens an_absent_store_is_could_not_look_and_never_clean,
an_undeclared_template_is_could_not_look_too and
one_malformed_member_poisons_the_whole_reading; the other four stay green,
so the arms discriminate instead of firing together.
Some(0) => ExitCode::Usage (refuse unconditionally)
→ reddens a_store_whose_tasks_are_all_completed_is_clean and NOTHING else,
which is the anti-vacuity mirror doing the one job it exists for. Without
it every other assertion here is satisfied by a verb that decides nothing.
Each case was run BY ITS OWN NAME. A filter matching fewer tests than intended
over a suite like this is a green that carries no information, measured earlier in
this session at 6/6 passing with the one positive case never running.
The pointer arm is asserted in both directions: the open id reaches the channel
and the task's subject line does not (rule 4). `in_progress` counts as open
because the predicate names `completed` as the one finished state rather than
enumerating the unfinished ones — a status the harness adds later must count as
open, not slip through a list nobody updated.
Refs: CLOUD-1376, CLOUD-418
…mes the other "committed-and-pushed is the only state that survives a VM reclaim" is true of the TREE and was read as the whole question. It is the sentence that scoped a completion claim to git: asked "safe to archive?", a session enumerated the working tree, the stash, local branches and running processes, found all four clean, and answered "yes" while its own task store held a `pending` entry on disk. The environment taught the same shape from the other side. The only end-of-session signal that actually speaks in this container is the launcher's `stop-hook-git-check.sh`, which nags about unpushed commits and nothing else — so the recurring teacher of "what does end-of-session mean" asks a git question, and a git answer feels complete. THIS SHIPS WITH ITS MECHANISM, which is non-negotiable rule 2 and is also the reason the earlier commits come first: prose here would be half a change, and half a change is exactly what failed. `batten doctor session` is the other half. PAID FOR IN WORDS RATHER THAN APPENDED. `policy-budget` refused three drafts — 3566 tokens of 3500 and 204 lines of 199, then 201, then 200 — because this file was already sitting exactly on its line cap. That is the budget working: a rule that cannot earn its lines against the rules already here does not belong in the file that binds every turn. What survived is the load-bearing clause; the measurement, the exit table and the reasoning live in `doctor.rs`, in `session_drain.rs` and on the row, where a reader who needs them will be. Refs: CLOUD-1376
The consumer half of CLOUD-1376. `doctor session` reads the declared template to decide whether it may look at all, so until this lands the verb answers could-not-look on every invocation here — honest, and useless. MEASURED ON THIS TREE, which is the acceptance clause rather than a demonstration. Pointing the link at a copy of this session's own store with `21.json` restored to the state it held at 22:35: doctor session: 1 of 21 declared task(s) open — 21 exit 1 and against the live store, where that task is now finished: doctor session: 0 of 21 declared task(s) open exit 0 The first line is the answer the session gave as "yes — safe". `~/` EXPANDS, and that is correctness rather than convenience. These stores live under the launcher's home, no committed value may name a container's absolute home path, and an unexpanded `~` reaches no directory — which reads exactly like a consumer with no work. The expansion takes the home as a PARAMETER rather than reading `HOME` inside, so the function stays pure and its unit cases do not mutate process-wide state other tests race with. An absent or empty home leaves the template alone, so the caller's `is_dir` takes the could-not-look arm. Refs: CLOUD-1376 Admits: 92bbe52e93f6fd8f4377741662c89403ceec378fb60d7fb37ee20c3bc9229600 Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-head: 6c42d09 Admits-epoch: 325d7b80d70f362133ec009d2edb069dc0d888657df819a86da5e2cf043e0e3b Admits-author: alec@wenzowski.com Admits-prev: 1e307c3d9dcc2ea28e3a6a813a06f1b1c6eef4aa8e3cfb324441c7a9d14686fc Admits-answer-lost: The verb this branch just built answers could-not-look forever here. `doctor session` reads the declared template to decide whether it may look at all, so undeclared it returns 3 on every invocation — honest and useless — and batten stops being able to answer the question about ITSELF that CLOUD-1376 exists to make answerable. Measured on this tree: with the key declared and the store linked, the verb returns 1 and names task 21 for the 22:35 state and 0 for the 22:40:51 state; undeclared, both are 3. Admits-answer-precondition: No surface sets this value. `[transcript] tasks` is a NEW config key landing in the same branch that introduced it, and there is no verb that writes a config key — `config lint` reads, `config show` renders, and neither declares. The change is one line of TOML plus its header comment, and it lands in the diff a reviewer reads as exactly that: the consumer naming where this host keeps its session task store. Admits-answer-rejected-route: `config read first` is what produced this change rather than an alternative to it: reading `[transcript]`'s own header is how the key found its home there instead of in a new `[session]` table, because that header already states why the transcript format and the memory root share a table and why a second authority over one host would be the widening rule 6 forbids. `patch run first` has no patch to run — `mise run fmt` normalises TOML it is given and writes no key, and nothing else in the task list declares config.
`git add -A` swept up `.claude/.tasks` — the runtime symlink created while proving the verb by hand — and committed it pointing at `/root/.claude/tasks/<this container's session id>`. WHY THAT IS A DEFECT AND NOT UNTIDINESS. A committed link names ONE container's session, so every other checkout inherits a dangling pointer, and `doctor session` reads a dangling link as could-not-look on every invocation. The verb that exists to stop an absent reading passing as a clean one would itself have shipped unable to look, everywhere but here. The precedent is two lines above it in this file: `[transcript]`'s link carries the same rule for a different reason — committing that one leaks a session's prose, committing this one leaks nothing and breaks everyone. Both are written by the engine at the Stop seam and neither belongs in the tree. FOUND BY A REVIEW BOT'S FILE LIST rather than by me or by a gate. CodeRabbit enumerated the eleven files in the diff and `.claude/.tasks` was one of them; no check refuses a tracked symlink into an absolute path, and `.gitignore` is the mechanism that stops the next one. Refs: CLOUD-1376, CLOUD-97
…clared again PORTED, NOT THIS BRANCH'S. `harness-wiring` refuses `origin/main` itself — same binary, clean worktree, 2 findings on `dda531f7` and none on `dda531f7^`. This branch touches no wiring. It is carried here because a fix that exists is ported rather than waited on, and it no-ops the moment `main` carries its own. WHAT dda531f MEASURED, AND WHY IT DOES NOT HOLD. It set the table to `{}` because `session:wiring` had just reclaimed both launcher registrations from the merged surface, 2 -> 0, so the exemptions "outlived what they excused". True when taken. Measured in this container after that commit landed and after the repair ran: both programs present with mtime 00:55, and `doctor hooks -J` reporting `siblings: 0, merged_siblings: 2`. The launcher re-provisions at session start, so the repair wins the moment it runs and loses by the next gate. THE SAME DEFECT, THREE TIMES, AND THE THIRD IS THE SUBTLEST. CLOUD-1314 deleted the rows on "both registrations are gone"; measured present three hours later. `[hook] exclusive = true` was declared on a `merged_siblings: 0` taken minutes after deleting the files by hand, and its revert records the rule: a count that is zero because you just removed its members is not the count. This is that rule one level out — a count that is zero because the REPAIR just fired is not the count either, because the thing being repaired is rewritten by something the repair does not control. WHAT THIS COMMIT DOES NOT DECIDE. Whether declaring the rows is the right steady-state answer, or whether the repair should be made to win the race, is CLOUD-1079's. The rows name it, `spent` and `stale` still watch them, and they leave when the launcher stops writing them. This restores a green base; it does not settle the design. Refs: CLOUD-1079, CLOUD-1314
`spec::tests::the_emitted_surface_is_exactly_the_committed_row_set` refused the tree: the emitted surface carried `doctor session` and the committed row set did not. The surface is spec-as-data (house-style §11), so adding a `CommandDecl` is half the change — the completions, the man pages and the golden schema snapshot are derived from it and are committed, which is what makes that assertion able to fail at all. Regenerated with `mise run fix` rather than edited by hand, for the reason the schema regeneration earlier on this branch had: a hand-written derived artifact agrees with the generator only until the next reader, and the gate compares against the generator. `man/batten-doctor-session.1` is new; `batten-doctor.1` gains the sub-verb; the three shell completions gain the token. The one SOURCE change is clippy's, and it is behaviour-identical: a nested `if` collapsed into the `&& let` chain this file already uses elsewhere. Refs: CLOUD-1376
`allowlist_is_exactly_the_read_commands` and `the_emitted_surface_is_exactly_the_committed_row_set` both refused the tree, and neither is a list `mise run fix` can regenerate — that is the design rather than a gap. The second says so in its own comment: a verb added, renamed or re-parented "fails here and has to be STATED, which is the prompt to reconcile §2 in the same change". A generated list would agree with the generator by construction and assert nothing about what a human decided. THE READ-ONLY ENTRY IS THE ONE THAT NEEDED AN ARGUMENT, since house-style §5's allowlist is the safety-critical half. `doctor session` is `read` structurally, not by promise: it opens the session's task store through a link the engine parked, counts the members whose `status` is not `completed`, and returns. No spawn, no write, no network — and `the_process_spawning_verb_is_never_read_only` and `the_mediation_entrypoint_is_never_read_only` are the two assertions that stop that claim being made carelessly. Both green. Refs: CLOUD-1376, CLOUD-244, CLOUD-777
…to print it
`every_leaf_verb_is_classified` refused the new sub-verb, and the census then
refused the easy way out — which is the better half of this commit.
WHAT IT CAUGHT. Adding the row was not enough: with no task store in the corpus,
`doctor session` answers could-not-look, and the sweep asserts `code != Some(3)`
before it will read the output at all — "failed internally, so what it did not
emit proves nothing". A verb that never reached its reporting path cannot
demonstrate that the path is clean. `MAY_ANSWER_COULD_NOT_LOOK` would have
silenced that, and it would have bought a row in a census that never exercised
the verb: the vacuous pass this file exists to refuse, entered through its own
exemption list.
SO THE CORPUS SEEDS THE STORE, and the seed is a CONTENT canary because that is
what a task subject is — free text an agent wrote, the same class as a matched
line or a transcript's prose. The verb now reads it on every sweep run and must
emit the id and the counts without it. `the_corpus_is_live_subject_matter` is
what holds the seed reachable, so this cannot decay into a canary nothing reads.
A real directory rather than a symlink: `read_dir` follows either, so the reading
under test is identical, and the fixture does not depend on how a platform spells
a link.
ONE DEFECT FOUND IN WRITING IT, and it is this branch's recurring shape a third
time: `tasks = "/nonexistent/{session}"` sits inside a `format!`, which consumed
`{session}` as a named argument. The compiler caught it here. The same template
written where no compiler looks is a placeholder that silently resolves to
nothing — exactly the unexpanded `~` earlier on this branch, and exactly what
could-not-look-versus-clean exists to keep visible.
Refs: CLOUD-1376, CLOUD-92, CLOUD-418
… an absent home `clippy::unnecessary_wraps` refused a helper I wrote: `home()` returned `Option<&OsStr>` and every arm of it was `Some`, so the wrapper carried no information. THE FIX IS THE BETTER SHAPE RATHER THAN AN `#[allow]`. `tasks_dir` takes an `Option` because an absent HOME is a real reading — an unexpanded `~` names no directory and the caller's `is_dir` check then takes the honest arm instead of this function inventing a home. Wrapping inside the helper hid exactly that distinction at the call sites: a reader could not tell which cases were ABOUT absence and which merely supplied a home. `Some(home(...))` at each site puts it back where it can be read, and the two absence cases now stand out by being the ones that do not say `Some(home(...))`. `cargo clippy --all-targets`: 0 findings. `transcript::tests`: 19 passed. Refs: CLOUD-1376
… path join
Windows CI refused the tree, and the failure is mine:
left: "/home/agent\.claude/tasks/s-1"
right: "/home/agent/.claude/tasks/s-1"
`tasks_dir` expanded `~/` through `Path::join`, which inserts the PLATFORM's
separator. So the engine rewrote a separator the CONSUMER chose — on a template
that is the consumer's string, in a function whose whole contract is that it
knows one placeholder and one prefix and returns everything else verbatim. That
is rule 1's boundary in miniature: the layout is declared rather than derived,
and a join derives.
WHY LOCAL VERIFY COULD NOT SEE IT. The assertion was correct and passed here; the
two platforms disagreed rather than the logic being wrong, so no amount of running
it on Linux would have shown anything. That is the class CI exists for, and it is
distinct from a failure verify skipped.
THE NEW ARM MAKES IT VISIBLE EVERYWHERE. A Windows-shaped home —
`D:\Users\agent` — asserts the home comes back verbatim and the template's own
`/` survives, so the property is now checked on every platform rather than only
on the one that disagreed. Re-introducing the join reddens it here.
`transcript::tests`: 19 passed. `clippy --all-targets`: 0 findings.
Refs: CLOUD-1376
0c59527 to
498642c
Compare
|
❌ The last analysis has failed. |
|
/fast-forward |
Closes CLOUD-1376
DO-NOT-CLOSE CLOUD-1079
44dba857serves CLOUD-1079 and is the opposite of completing it. That commit restores the twodeclaredexemptions because the launcher still re-provisionsstop-hook-git-check.shandsession-start-git-identity.shat session start — measured with mtime00:55, afterdda531f7landed and aftersession:wiringreclaimed them. The rows exist to name a live defect; CLOUD-1079 owns removing the cause. Closing it here would record as fixed the exact thing this branch measured as still broken.The defect, measured
Asked "Done? Safe to archive?", this repository's own agent enumerated the working tree,
git stash, local branches and running processes, found all four clean, and answered "yes — safe".At that moment
/root/.claude/tasks/<session>/21.jsonread"status": "pending". The claim was false when it was made, and a file on disk could have decided it. Worse, the same message disclosed the counter-evidence — it said the finding was unfiled and offered to file it "before you archive" — and did not let that change the verdict. (It was also wrong about "unfiled": CLOUD-1304 already existed.)Root cause: a completion question with no verb
Every other completion question resolves to a command.
verifydecides the tree,landthe PR,done-checkthe release,claim-checkthe pull. Nothing decided the session. Non-negotiable rule 3 says gates decide and never estimate — but a rule needs an instance to bind to, so the one completion claim with no command behind it is the one that was wrong.The prose reinforced the scope error. AGENTS.md's "committed-and-pushed is the only state that survives a VM reclaim" is true of the tree and was read as the whole question — helped by the only end-of-session signal that speaks in this container being
stop-hook-git-check.sh, which nags about unpushed commits and nothing else.Why no landed Stop rule caught it — measured, one at a time
stop_nudgesruns five ranked rules and batten is registered onStop, so the surface fired.stop-posturefinding-sink-checkpath:linecitation ∧ no durable write. Run over the session transcript: exit 0. The turn cited branch names and line counts, no coordinatefiled-hereunlanded-checkRule 2 is the near miss, and its narrowing was correct on its own evidence (over 113 turns the citation shape fired 1/1 true, the exit-claim shape 2/2 false). It never sampled this class, which is the inverse of what it hunts: not "I found something and wrote it nowhere" but "I asserted there is nothing left to find."
What lands
batten doctor session—0nothing open,1open work with ids,3could-not-look.Could-not-look is
3and never0, and that single mapping is the deliverable. The defect is an absent reading reported as a clean one, so the arm with nothing to read must not share a code with the arm that read and found nothing. An undeclared template, an unreadable store, and one malformed member all take it — a partial count is a number that looks measured and is not.ExitCode::Violationstays unreachable, forWiringReport::code's reason: adoctorsub-verb is a diagnosis, a mediating harness reads2as a deny, and "you have unfinished work" is not "policy says no". The issue's Ready block said2; the code table corrected it.Rule 1 decides where the layout lives. The store sits outside the repository root and its shape is the host's, so deriving it in
crates/battenwould put a directory layout in the core. The consumer declares a template carrying{session}; the engine substitutes the one field the envelope already normalises across hosts.It joins
[transcript]rather than opening a[session]table, on that table's own stated reasoning: the transcript's format and the host's memory layout are already "two facts about one host", and splitting them "would be the widening rule 6 forbids".Verification
The acceptance clause, end to end. Against a copy of this session's own store with
21.jsonrestored to its 22:35 state, and against the live store:The first line is the answer that was given as "yes — safe".
Shown able to fail, by hand, twice (CLOUD-418) — a suite of absence assertions is exactly where a green means nothing:
None => ExitCode::Success(could-not-look reported as clean — the original defect restated as code) reddens the three could-not-look arms and nothing else.Some(0) => ExitCode::Usage(refuse unconditionally) reddensa_store_whose_tasks_are_all_completed_is_cleanand nothing else — the anti-vacuity mirror doing the one job it exists for.The pointer-only census refused the easy way out, and that is the better half. Adding the row was not enough: with no store in the corpus the verb answers could-not-look, and the sweep asserts
code != Some(3)before it will read output at all — "failed internally, so what it did not emit proves nothing".MAY_ANSWER_COULD_NOT_LOOKwould have silenced that and bought a census row that never exercised the verb. So the corpus seeds a real store whose tasksubjectis a content canary: the verb reads an agent's prose on every sweep and must emit the id and counts without it.Four corrections made in flight, recorded because each was believed first
2;doctormay not return2at all.~/needed real expansion. Without it the declared template resolves to no directory — which reads identically to a consumer with no work: the same dead-declaration class this fix is about, nearly shipped inside it.git add -Acommitted.claude/.tasks, a symlink naming this container's session. Every other checkout would inherit a dangling pointer, anddoctor sessionreads a dangling link as could-not-look forever. Caught by a review bot's file list, not by me and not by a gate;.gitignoreis now the mechanism.tasks = "/nonexistent/{session}"inside aformat!consumed{session}as a named argument. The compiler caught it here; the same template where no compiler looks is a placeholder that silently resolves to nothing.Prose ships with the mechanism
Non-negotiable rule 2 — prose alone is half a change, and half a change is what failed.
policy-budgetrefused three drafts (3566 tokens of 3500, then 204/201/200 lines of 199) because AGENTS.md sat exactly on its line cap, so the rule was paid for in words rather than appended.Also carried: a base-branch fix, ported
44dba857is not this PR's work.harness-wiringrefusesorigin/mainitself — 2 findings ondda531f7, none ondda531f7^— and it is ported here per the drive-to-green rule that an existing fix is ported rather than waited on. It no-ops oncemaincarries its own. Full measurement in the earlier comment.Not in scope
stop_nudgesrule). Recorded in this branch's plan store asstop-nudge-rule-six deleted— a withdrawal on the record rather than quietly missing. A nudge fires after the turn in which the question is asked; the verb is the half that prevents the claim.Stopis architecturally unavailable —Event::carries_a_verdictis the one authority andStopcarries none (CLOUD-97, CLOUD-219). An acceptance clause promising it would be unmeetable, which is CLOUD-1314's shape.Filed and fixed from the losing side: the false claim was caught by the user asking how it could have been made, not by any mechanism.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VtDugY1mEeCLNA8ULMHocH