feat: a node that was never authored is never advised to freeze, and prose that claims output must prove it - #207
Merged
Merged
Conversation
Direction beat for `authoring-beat-named`. Nothing is frozen — the ONE approval is the human's and was not stamped. The node was authored before #206 merged, so it was repaired before being trusted. Nine referents were stale: every `add.py:` line anchor had drifted with that PR's +322 lines (freeze's refusal 1260->1394, placeholders_in 2270->2595, status 1777->2015, todo, new, BODIES, milestone_done, the quick lane), and one check still pinned the verb count at 22 when `add check` had made it 23. Both headline claims re-measured against the post-merge engine rather than taken from the node's prose. Both hold: `add new Milestone` then `add freeze` returns `freeze recorded at authority process` with `goal: <one line>` untouched, and `placeholders_in` returns [] for any Milestone body, because it reads RULES · ASSUMPTIONS · CHECKS and a Milestone carries none of them. ADD's one human approval is stampable today against a node stating no goal, no scope and no exit criterion. Two defects found in the node itself while driving it: `sensitivity: high` is not a value the engine recognises. SENSITIVITY_FLOOR maps mechanical|data|architecture|security and `.get(sens, "process")` turns anything else into the LOWEST floor, so both affordance-truth tasks computed `process` where they meant `plan` — every other node in this bundle says `architecture`. Corrected here; the underlying refusal is filed as a lesson against specs/method and is queued for the enforcement node. The milestone-advice check asserted on `status`, whose `next:` line targets the task frontier and never names a milestone at all — it passed with no fix applied, which is exactly the vacuous shape R:VACUOUS_GUARD forbids. Retargeted to `new`, the surface that actually emits the advice. 21 checks. 12 RED for the right reason; the 9 green are deliberate regression guards — a frozen node's affordances unchanged, the quick one-call lane still closing, the verb count, the `status:` vocabulary, frontier ordering, and the empty-frontier affordance. test_new_scaffold.py's pinned assertion is RE-AIMED rather than deleted: the scaffold's `next:` stays a pinned interface, it was the value that was wrong. One contract detail the PLAN implies without stating: `_beat_of` returning `scaffold` needs a matching entry in `todo`'s sort map, or scaffolds sort after verify instead of first. `active()` reads the `status:` field rather than the beat, so no node drops off the frontier. author: Tin Dang
`direction.md` states the design — "There is no author verb — you fill those
sections by editing that file directly" — and no affordance in the engine knew
it. `BEAT_NEXT["direction"]` mapped the whole beat to `add freeze {slug}`, so
from the moment `add new Task` wrote a file of placeholders, five surfaces
recommended a verb `freeze` is structurally guaranteed to refuse. The cost was
never the wasted call: a scaffold and a finished contract read identically from
`status`, so a node could sit unauthored for days inside a milestone that read
as in progress.
The derived beat gains a `scaffold` rung between created and frozen, computed
from the SAME predicates the refusals call — `gives_unauthored` at T0,
`placeholders_in` at T2 — never a copy of them. `status`, `todo`, `new` and the
CARD scaffold all resolve through one map, and `status`'s own stamp test (a
third reading of the beat) is gone.
On Milestones the bad advice was not refused: it SUCCEEDED. `placeholders_in`
reads RULES · ASSUMPTIONS · CHECKS and a Milestone body carries none of those
three, so it returned [] for EVERY milestone and ADD's one human approval was
stampable against a node stating no goal, no scope and no exit criterion.
`_milestone_stubs` closes that, deliberately narrow: goal · why · EXIT, the
three the milestone lifecycle already reads, since `milestone_done` refuses on
`why:` and on the EXIT tally. A guard reaching SCOPE and GROUND too would refuse
real milestones whose ground is thin, and a guard everyone learns to widen past
is worse than a narrow one that holds. This bundle's own `v3-final-collateral`
is refused by it — the intended one-time authoring cost.
`card_drift` now compares the DERIVED beat, not the raw `status:` field. The
2026-08-17 replan recorded A5 as falsified and this is its repair: `freeze` does
not move `status:`, so a freshly frozen node advertised `next: add freeze <slug>`
— the approval it had just passed — while `todo` and `status` derived `build`,
and `doctor` reported it CLEAN. `render_card` goes through `_next_verb` for the
same reason: reading BEAT_NEXT raw put a third answer on the CARD, missing that
a sealed-but-unbriefed task owes `add brief` first.
R:T2SCAN kept honest. `build-orient` froze "building a report must not read a
single body", and that Reject is not this task's to weaken — so `_is_scaffold`
is one function with two tiers: T0 always, T2 only when the caller already holds
the body. `status` gets the T0 answer, `todo` and `freeze` the complete one.
Residual, recorded rather than hidden: a node with an authored `gives:` but
still-template RULES reads authored to `status` alone.
Two incumbent value pins re-aimed, neither weakened: `test_todo_beat_is_stamp_derived`
now expects `scaffold` where it expected `direction` (the token moved, the
stamp-derivation it covers did not), and `test_sync_repairs_card_drift`'s fixture
drifts from `beat: scaffold` — it was replacing a string the body no longer
carried, a silent no-op its own assertion caught.
The gate refused the first PASS with six unbound rules and was right: A5 and A12
were proven but never cited, A4 and A11 had no check at all, and `test_tree_parity`
is a MODULE name that binds nothing. Three checks added, citations corrected,
contract refrozen.
26 checks. Receipt 3: 795 passed / 7 skipped, exit 0, kind test-ids, freshness
content. Gated PASS at authority `plan`, freshness fresh. Four engine twins
synced, ENGINE_MD5 re-aimed.
author: Tin Dang
…nning it `promised-capability-guard` closed this class for the READMEs and its own `why:` named the gap it left — those guards check nouns the engine EXPOSES, never capabilities the prose PROMISES. `loop.md` is where the gap bit. It told the reader `add status` shows `goal not met (m/n exit criteria)` and pointed at "the plan-vs-state line in `add status`". `status` prints neither, and the first string no longer exists anywhere in `add.py` — it was reworded to `milestone_goal_unmet` inside `milestone_done`'s refusal — so the existence anchor the README guard uses would have resolved it and passed. The claim sat in the loop's Gather step, named as THE cue that starts the loop, so an agent following the skill waited for a signal the engine never sends. The guard registers every skill-tree line that names a backticked `add <verb>` AND claims a rendering, and proves each one by DRIVING that command against a bundle put into the state the claim describes. The anchor is captured stdout. An unregistered claim fails by name, quoting the sentence with its file and line, so a new claim cannot join the corpus silently. Both false claims reworded to what the engine does today — in all three live skill trees. The Gather cue is now `add milestone-done <slug>` refusing with `milestone_goal_unmet (m/n exit criteria)`, and planned-but-unscaffolded tasks are found at the `scaffold` beat in `add todo`, which the sibling task shipped an hour ago. Both replacements are themselves registered and driven. M4 held: `add.py` is untouched by this task, asserted by a check that diffs it. Building the missing `status` surface is a real improvement and a separate task; a guard that ships having made its own claims true has never once refused. The guard caught a live consequence of the sibling task while being written. Its first driver for "`add todo` counts them down" built a node with template RULES, which now sits at the `scaffold` beat and is told to author rather than given the countdown. That is better guidance, not a regression — the countdown is the DIRECTION beat's hint and still renders there — so the driver was corrected to build the claim's real state rather than the engine to match a bad driver. The gate refused the first PASS with six unbound rules and was right twice over: five probes had no check, and `test_loop_claims_are_repaired_in_all_three_trees` was PARAMETRIZED, so it reported as `test_x[skills]` and bound nothing while sitting green. De-parametrized, five checks added, contract refrozen. 21 checks. Receipt 2: 815 passed / 7 skipped, exit 0, kind test-ids, freshness content. Gated PASS at authority `plan`, freshness fresh. author: Tin Dang
Both tasks gated PASS at authority `plan`. Two criteria were AMENDED before being ticked rather than ticked as written, each carrying its amendment note in the milestone body: Criterion 1 said every surface that derives a `next:` names authoring. No surface bound by `build-orient`'s frozen R:T2SCAN can satisfy that — `status` must not read a body — so it is reworded to every surface that CAN read the body, naming the one escaping shape (authored `gives:`, template RULES) which `todo` and `freeze` still catch and which `_is_scaffold`'s docstring records. Criterion 4 said `freeze` refuses a Milestone whose CARD, SCOPE, GROUND or EXIT are template. M7 was deliberately narrowed to goal · why · EXIT — the three the milestone lifecycle actually reads — so the criterion is narrowed to match what shipped rather than overstating it. The close line reads `checked by Tin Dang (unattended)`: the ticks were made by a process, not by a human at a terminal, and #206's attribution records that distinction rather than letting a claimed name stand alone. author: Tin Dang
| import tempfile | ||
| from pathlib import Path | ||
|
|
||
| import pytest |
TinDang97
added a commit
that referenced
this pull request
Sep 1, 2026
…ing seal
Direction beat for `risk-accepted-integrity`: the frozen-contract draft and its
red checks. No engine change yet — the freeze is the human seam and this node
declares `sensitivity: security`, so the floor is `human`.
The finding, measured rather than inferred. All 16 integrity refusals in `gate`
are conditioned on `verdict == "PASS"`. Against the current engine, a Task
created seconds earlier — still carrying `goal: <one line>` and every template
section, never frozen and never briefed — reached `done` in three calls:
add run <slug> -- true
add gate <slug> RISK-ACCEPTED --reason "probing"
add done <slug>
R:UNSEALED, the post-freeze drift check, R:UNBRIEFED, the placeholder check and
R:UNDECLARED_SENSITIVE all read PASS only, so the lane meant to be the
SCRUTINISED one carries no scrutiny at all. This is #206's own lesson one
verdict over: skipping `freeze` did not FAIL the post-freeze guards, it
SWITCHED THEM OFF. That hole was closed for PASS and left open next door.
Two smaller findings fell out of the same read, both re-characterized by
execution rather than taken from notes:
* `_paths_touch` over-matches on string prefix, not path segment. The
recorded symptom (`scope: s` defeating the A17 floor) is false — probed, it
returns False. The real defect is the opposite and lives in the EXEMPTION
direction of R:UNDECLARED_SENSITIVE: `scope: src` exempts a changed
`srcfoo/secret.yaml`, and `secrets_public/x` matches `secrets/**`.
* an unrecognised `sensitivity:` degrades to the LOWEST floor. `high` and
`critical` both read `process`. This bit two real nodes on the
affordance-truth branch, which declared `high` and silently floored to
`process` instead of `plan`.
The contract splits the refusals in two: those protecting the RECORD (was it
frozen, did the contract drift, is the body still a template, did the build
touch an undeclared sensitive path) bind every verdict; those judging the
EVIDENCE (stale receipt, non-zero exit, unbound covers, coverage gap) stay
PASS-only, because signing for imperfect evidence is what the verdict is FOR.
19 checks: 13 red for the right reason, 6 green as counter-guards pinning
behavior that must NOT change — a lone refreeze still satisfies the seal, a
legitimate RISK-ACCEPTED on a frozen node still closes, a first-call gate still
hears the receipt message rather than a security refusal, and a HARD-STOP stays
recordable on an unfrozen node (refusing it would only lose the finding).
Suite: 824 passed / 13 failed / 7 skipped across both roots — the 13 are these.
Filed while dogfooding the `scaffold` beat from #207, which earned its keep: it
refused to advance this node while `gives:` still read as template, catching a
`<slug>` inside backticks that `status` alone would have shown as `direction`.
author: Tin Dang
TinDang97
added a commit
that referenced
this pull request
Sep 1, 2026
… the ONE approval asks its questions out loud (#208) * test(engine): RISK-ACCEPTED signs for weak evidence, never for a missing seal Direction beat for `risk-accepted-integrity`: the frozen-contract draft and its red checks. No engine change yet — the freeze is the human seam and this node declares `sensitivity: security`, so the floor is `human`. The finding, measured rather than inferred. All 16 integrity refusals in `gate` are conditioned on `verdict == "PASS"`. Against the current engine, a Task created seconds earlier — still carrying `goal: <one line>` and every template section, never frozen and never briefed — reached `done` in three calls: add run <slug> -- true add gate <slug> RISK-ACCEPTED --reason "probing" add done <slug> R:UNSEALED, the post-freeze drift check, R:UNBRIEFED, the placeholder check and R:UNDECLARED_SENSITIVE all read PASS only, so the lane meant to be the SCRUTINISED one carries no scrutiny at all. This is #206's own lesson one verdict over: skipping `freeze` did not FAIL the post-freeze guards, it SWITCHED THEM OFF. That hole was closed for PASS and left open next door. Two smaller findings fell out of the same read, both re-characterized by execution rather than taken from notes: * `_paths_touch` over-matches on string prefix, not path segment. The recorded symptom (`scope: s` defeating the A17 floor) is false — probed, it returns False. The real defect is the opposite and lives in the EXEMPTION direction of R:UNDECLARED_SENSITIVE: `scope: src` exempts a changed `srcfoo/secret.yaml`, and `secrets_public/x` matches `secrets/**`. * an unrecognised `sensitivity:` degrades to the LOWEST floor. `high` and `critical` both read `process`. This bit two real nodes on the affordance-truth branch, which declared `high` and silently floored to `process` instead of `plan`. The contract splits the refusals in two: those protecting the RECORD (was it frozen, did the contract drift, is the body still a template, did the build touch an undeclared sensitive path) bind every verdict; those judging the EVIDENCE (stale receipt, non-zero exit, unbound covers, coverage gap) stay PASS-only, because signing for imperfect evidence is what the verdict is FOR. 19 checks: 13 red for the right reason, 6 green as counter-guards pinning behavior that must NOT change — a lone refreeze still satisfies the seal, a legitimate RISK-ACCEPTED on a frozen node still closes, a first-call gate still hears the receipt message rather than a security refusal, and a HARD-STOP stays recordable on an unfrozen node (refusing it would only lose the finding). Suite: 824 passed / 13 failed / 7 skipped across both roots — the 13 are these. Filed while dogfooding the `scaffold` beat from #207, which earned its keep: it refused to advance this node while `gives:` still read as template, catching a `<slug>` inside backticks that `status` alone would have shown as `direction`. author: Tin Dang * test(engine): the ONE approval asks its questions out loud Direction beat for `freeze-interview`: the frozen-contract draft and its red checks. No engine change yet — the freeze is the human seam. `freeze` already refuses an incomplete contract eight ways: template placeholders, an unauthored `gives:`, collapsed surfaces, unswept (dimension, surface) pairs, an unbudgeted explore. Every one of those checks the DOCUMENT. None checks the CONVERSATION. `## ASSUMPTIONS` is by construction a list of silences the AI filled in on the human's behalf, each line carrying the reading taken and the cost if it is wrong. The sweep made the AI WRITE DOWN what it decided. Nothing made it ASK. So ADD's ONE human approval is a single stamp that says nothing about whether the human was ever shown a single decision they are approving. At a human floor `freeze` now refuses until every open decision has been put to a human and answered (R:UNINTERVIEWED). Two calls, both ratified by the human this session: * ENFORCED, not advisory. This repo's own record is the argument: three probe campaigns showed recommended prose gets routed around, which is why `brief` became a checkpoint (R:UNBRIEFED) rather than advice. * ASSUMPTIONS + REJECTS. An assumption is a silence the AI filled and a Reject is a boundary the AI drew — both are decisions the human never made but will be held to. The Musts came FROM the human, so re-asking them is noise, and an `n/a` retirement already states its own reason. Design consequence found while reading `freeze`: it computes `authority = authority or authority_for(graph, cid)`, so an explicit `--authority process` DOWNGRADES even a `security` node. Keying the interview on the passed flag would therefore have shipped the guard with its own one-flag off switch, so M5 keys it on the COMPUTED floor and E1/test pins that. Answers persist as evidence, not a boolean: one `act: interview` stamp carrying a digest of exactly what was interviewed, plus a `.d/interviews/<n>.md` sidecar it references — the same sidecar shape `.d/runs/<n>.md` already uses, so no body section is renumbered (R:RENUMBER; the engine keys sections by name and number). Staleness follows `direction:` and `brief:`: edit an interviewed assumption and the digest moves, so the interview re-opens. Edit a Must and it does not. `correct` is a real verdict and does NOT complete an interview — it is cleared by editing the item, which moves the digest and requires a fresh pass. `defer` does complete one: a human may accept a risk knowingly. 21 checks: 18 red for the right reason (`add.interview` absent, `freeze` does not refuse, the three skill trees do not teach it), 3 green as counter-guards pinning behavior that must NOT change — a `process`-floor node is never interviewed, the interview runs LAST so nobody is asked about template slots, and the section census is unchanged. author: Tin Dang * fix(engine): RISK-ACCEPTED signs for weak evidence, never for a missing seal Build beat for `risk-accepted-integrity`. All 19 checks green. `gate`'s sixteen integrity refusals were each written `verdict == "PASS"`, so RISK-ACCEPTED walked past every one of them and `done` accepted the resulting stamp. Measured before the fix: a Task created seconds earlier — still every template slot, never frozen, never briefed — reached `done` in three calls. The sixteen inline conditions are now ONE predicate over two named tuples, so the classification is reviewable in one place instead of being re-decided at each site: * INTEGRITY_REFUSALS protect the RECORD — unsealed, drift, unbriefed, placeholders, undeclared_sensitive, phantom_scope, and the three explore equivalents. They bind every verdict that APPROVES or CLOSES. * EVIDENCE_REFUSALS judge the RUN — stale_receipt, failed_run, unbound_covers, hollow_explore, no_security_lens. PASS only: signing for imperfect evidence is what RISK-ACCEPTED is FOR, and three of them already named it as their own remedy. * HARD-STOP is refused by neither. It never closes a task, so refusing it would only stop a finding being written down. `_binds` raises on an unclassified name, so a refusal added later cannot silently inherit either tier. The verdict literal now appears exactly once in `gate`, as `closes`. `done` gained the seal check: any (re)freeze must precede the entitling gate. A refreeze recorded afterwards (the re-cross pattern) is not required to. `_paths_touch` matches whole path SEGMENTS. The dangerous direction was the EXEMPTION clause of R:UNDECLARED_SENSITIVE, where a string-prefix read let `scope: src` sign for a `srcfoo/` the node never declared, and `secrets_public/` answer for `secrets/**`. An empty side now matches nothing. `sensitivity:` floors UP when unreadable. `SENSITIVITY_FLOOR.get(v, "process")` sent every unrecognised value to the LOWEST floor; `high` and `critical` both read `process`, which is how two real nodes on the affordance-truth branch floored at `process` where they meant `plan`. `new` now refuses the value outright — deliberately breaking that function's "slots, never validation" stance, because `sensitivity:` is not a prose slot but the enum that computes the floor, and `init` already refuses an unknown `--profile` for the reason. Four fixtures were corrected, none weakened; every assertion is unchanged: * two of my own — one anchored on `depth: standard`, which `new` does not write unless supplied, so it was a silent no-op and passed against nothing; one used `brief()` (which COMPILES) instead of `brief_stamp()` (which records), and one built its bundle where `gate`'s `root.parent` could not see the git repo. * two incumbent — `test_done_transitions_with_gate` and `_make_done` in test_reopen.py both forged a gate stamp with no freeze, which is exactly the record M6 now refuses. They assert that an entitling gate transitions; that stays true, so they gained the seal the method always implied. Four engine twins synced; ENGINE_MD5 re-aimed 1bf61710 -> 4c2a048d. author: Tin Dang * fix(engine): `unbriefed` is an EVIDENCE refusal, not an integrity one Correcting the tier split shipped one commit ago. I classed R:UNBRIEFED as INTEGRITY; the full suite disagreed, and it was right. `test_brief_gate.py::test_non_pass_verdicts_are_never_blocked` carries `covers: M3` of the brief-gate task's FROZEN contract — "a verdict is how a node LEAVES a bad state (same stance as R:TRAP)". That Reject belongs to another task and is not this one's to weaken, so the refusal moves rather than the test. It also reads correctly on the merits. A missing brief says the BUILD ran without the compiled prompt — a fact about how the run was driven, which is what the evidence tier is for. The record itself still cannot be forged under any verdict: the seal (R:UNSEALED), the post-freeze drift check and the placeholder guard all stay INTEGRITY, so a RISK-ACCEPTED still cannot close an unfrozen, drifted or template node. Same shape as the R:T2SCAN conflict resolved during affordance-truth: when a frozen Reject from a neighbouring task contradicts a design choice here, the frozen one wins and the reasoning is recorded at the site. Twins resynced; ENGINE_MD5 re-aimed. author: Tin Dang * fix(engine): close risk-accepted-integrity — the gate refused its own first PASS The gate refused the first PASS on three unbound referents, and was right on all three: * A13 and A14 were the two assumptions added to close the sweep during authoring. Their checks were WRITTEN (the HARD-STOP counter-guard and the backslash case) but never declared in `## CHECKS`, so the gate bound nothing — docstring `covers:` is not the binding, the node's CHECKS section is. * `test_no_existing_refusal_was_narrowed` was DECLARED covering R:WIDEN and never written at all. Writing it found the defect it was for. Rewriting sixteen inline conditions into two named tuples is precisely the edit where a refusal quietly disappears, and one had: R:UNFROZEN_EXPLORE was listed in INTEGRITY_REFUSALS but never dispatched through `_binds`, because that refusal is UNCONDITIONAL — it refuses HARD-STOP too. Routing it through `_binds` would have narrowed an existing refusal to buy tidiness, so it comes out of the tuple with the reason recorded at the site. The check's own arithmetic was wrong too (14 names; there are 13). Replaced with an explicit member set rather than a smaller count — naming them pins strictly more than counting them, and a dropped refusal now fails by name. Refroze, re-briefed, re-ran, gated PASS at `human` on receipt 2: 20/20 checks bound, freshness fresh. author: Tin Dang * feat(engine): the ONE approval asks its questions out loud Build beat for `freeze-interview`. All 21 checks green; 858 passing across both roots. `add interview <slug>` compiles every non-`n/a` assumption and every Reject into a numbered question carrying the reading taken and the cost if it is wrong. Bare it is a read. With `--answer <id>=confirm|correct|defer --by "<name>"` it writes a `.d/interviews/<n>.md` sidecar and appends one `act: interview` stamp referencing it. At a human floor `freeze` refuses until every decision is answered -> R:UNINTERVIEWED. It is the LAST refusal in freeze's ladder: everything above says the contract is unfinished, and there is no sense putting template text to a human. It is also the only one that checks the CONVERSATION rather than the document. Keyed on `authority_for`, never on the `--authority` argument, since the line below it is `authority or authority_for(...)` — reading the flag would have let `--authority process` switch the interview off on a security node. Staleness follows `direction:` and `brief:`. `interview_gap` reads the stamp whose digest MATCHES the current text, not the latest: revert an edit and the earlier interview is the one that answers for this text. `correct` never completes an interview — it is cleared by editing the item, which moves the digest. `defer` does: a human may accept a risk knowingly. Adding the 24th verb rippled exactly as this repo's record says it does — 15 failures across six registries and five budget pins, every one a real staleness: * the CLI verb-set registry, the book's command reference (in `docs/`, not the root copy I edited first), and both READMEs' "23-verb kernel" line. * `test_no_new_verb_in_the_cli_surface` re-aimed 23 -> 24. Its claim is that the AUTHORING-BEAT task added no verb; a later task legitimately adding one moves the value without weakening that. * SKILL.md went 176 -> 179. One of the five budget guards is literally R:BUDGET_BUMP, so the addition was funded by COMPRESSING the freeze bullet it sits beside, back to 176 exactly. * `_open_decisions` returned a `"kind"` key, which collided with the receipt KIND vocabulary a separate guard enumerates against the skill corpus. Renamed to `"of"` rather than teaching the skill a word it does not mean. Two security fixtures (`test_gate_coverage`, `test_sensitive_path_floor`) called `add.freeze` and discarded the result, so the new refusal made them fail on R:UNSEALED one step later. They now interview first AND assert the freeze landed — a fixture that silently skips the one approval tests no real path. Dogfooded on this repo's own bundle: `add interview freeze-interview` puts all 12 of its own open decisions. author: Tin Dang * feat(engine): close freeze-interview — the gate refused its own first PASS Same pattern as the sibling node, and the gate was right again: A1, A3 and R:SELFANSWER were unbound. * A3's staleness was proven by a check that cited only M7 and E3. * A1 and R:SELFANSWER had no check at all — and writing them found that my own SKILL.md compression had left the R:SELFANSWER instruction only in direction.md, WRAPPED ACROSS A LINE BREAK. This repo has silently unpinned a phrase that way before, so the sentence is reflowed onto one line and the check asserts it line by line rather than against the whole file. `test_selfanswer_is_carried_by_prose_not_by_an_engine_claim` states the limit plainly: `--by` is recorded verbatim, exactly as `freeze --by` is, so nothing here VERIFIES a human. Claiming otherwise would be the worse failure — a guard that asserts what it cannot prove is how a stamp becomes theatre. The discipline is prose, and the check proves the prose ships. Gated PASS at `plan` on receipt 2: 23/23 bound, freshness fresh. Worth recording: this node's own floor is `architecture` -> `plan`, so by M5 it was EXEMPT from the refusal it ships. The feature did not gate itself, and that is the design — a `plan` freeze has no human in the room to interview — but it means the first real exercise of R:UNINTERVIEWED is the next security-floored node, not this one. Three lessons filed to specs/method. author: Tin Dang * fix(engine): an interview accumulates across passes over the same text Found in the pre-merge review of #208, not by the suite. `freeze-interview` was reopened to `build` rather than patched after close. `interview_gap` returned on the FIRST stamp whose digest matched the node. Two consequences, both wrong and in opposite directions: * a human who answered three decisions, was interrupted, and came back to answer the rest was still refused — with no way forward but editing the node to move the digest, which is the one thing that DISCARDS the answers; * had it read the last match instead, a follow-up `correct` would have been outranked by the earlier `confirm`. Every pass over the same text is now folded in order, later winning. An interview is a conversation, and two sittings about the same decisions are one interview. Fixing that exposed the second half. Each stamp recorded the FULL decision list with `unanswered` for ids that pass had not touched, so folding made every pass clobber the one before it. The stamp's answer map is now SPARSE — only what that pass actually answered. The sidecar still lists every decision, because that is the human-readable record; the stamp is the machine-readable delta. Two checks, red first, and the second is the counter-guard for the first: accumulate must not mean a stale `confirm` outranks a later `correct`. 862 passed across both roots. Twins synced, ENGINE_MD5 re-aimed. author: Tin Dang * chore(node): re-gate freeze-interview after the review fix The reopen reset the gate; receipt 3 re-earns it at 25/25 bound. author: Tin Dang
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
affordance-truthmilestone, closed 7/7 and driven through ADD itself.1 — A node that was never authored is never advised to freeze
direction.mdstates the design plainly — "There is no author verb — you fill those sections by editing that file directly" — and no affordance in the engine knew it.BEAT_NEXT["direction"]mapped the whole beat toadd freeze {slug}, so from the momentadd new Taskwrote a file of placeholders, five surfaces recommended a verbfreezeis structurally guaranteed to refuse.The cost was never the wasted call. A scaffold and a finished contract read identically from
status, so a node could sit unauthored for days inside a milestone that read as in progress — observed live in another repo, and true of this bundle's ownv3-final-collateralsince 2026-08-11.The derived beat gains a
scaffoldrung between created and frozen, sorting first intodounder its own header:It is computed from the same predicates the refusals call —
gives_unauthored,placeholders_in— never a copy, because two notions of "authored" is precisely how advice and refusal came to disagree.status,todo,newand the CARD scaffold all resolve through one map, andstatus's own stamp test, a third reading of the beat, is gone.On Milestones the bad advice was not refused — it SUCCEEDED.
placeholders_inreads RULES · ASSUMPTIONS · CHECKS and a Milestone body carries none of those three, so it returned[]for every milestone and ADD's one human approval was stampable against a node stating no goal, no scope and no exit criterion. Measured, not inferred:_milestone_stubscloses it, deliberately narrow: goal · why · EXIT, the three the milestone lifecycle already reads, sincemilestone_donerefuses onwhy:and on the EXIT tally. A guard reaching SCOPE and GROUND too would refuse real milestones whose ground is thin, and a guard everyone learns to widen past is worse than a narrow one that holds. It refuses this repo's ownv3-final-collateral— the intended one-time authoring cost.card_driftnow compares the DERIVED beat, not the rawstatus:field. This repairs a falsified assumption recorded by areplanon 2026-08-17:freezedoes not movestatus:, so a freshly frozen node advertisednext: add freeze <slug>— the approval it had just passed — whiletodoandstatusderivedbuild, anddoctorreported it CLEAN.render_cardgoes through_next_verbfor the same reason; reading the map raw put a third answer on the CARD, missing that a sealed-but-unbriefed task owesadd brieffirst.2 — A claim that the engine prints something is proven by running it
promised-capability-guardclosed this class for the READMEs, and its ownwhy:named the gap it left: those guards check nouns the engine exposes, never capabilities the prose promises.loop.mdis where that bit. It told the readeradd statusshowsgoal not met (m/n exit criteria), and pointed at "the plan-vs-state line inadd status".statusprints neither. The first string did not even live inadd.pyany more — it had been reworded tomilestone_goal_unmetinsidemilestone_done's refusal — so an existence anchor of the kind the README guard uses would have resolved it and passed.Both sentences sat in the loop's Gather step, and the first was named as THE cue that starts the loop, so an agent following the skill waited for a signal the engine never sends.
The guard registers every skill-tree line naming a backticked
add <verb>and a rendering, and proves each by driving that command against a bundle put into the state the claim describes. The anchor is captured stdout. An unregistered claim fails by name, quoting the sentence with its file and line. Both false claims are reworded to truth in all three live skill trees; the new Gather cue isadd milestone-donerefusing withmilestone_goal_unmet (m/n exit criteria), and planned-but-unscaffolded tasks are found at thescaffoldbeat this PR ships. Both replacements are themselves registered and driven.add.pyis untouched by that task, asserted by a check that diffs it — building the missingstatussurface is a real improvement and a separate task. A guard that ships having made its own claims true has never once refused anything.Verification
The gate refused both tasks' first PASS, and was right both times. Two distinct ID traps, worth knowing about:
test_x[param]and the gate binds bare ids, so a green check sat there with its rule unbound;test_tree_parityis a file, not a test).Beyond those, five
probe:clauses had no check at all. Eight checks added across the two tasks, contracts refrozen, then gated.status:vocabulary, frontier ordering.ast.feature_version.Reviewer notes
build-orientfrozeR:T2SCAN— "building a report must not read a single body" — andstatusneeded the body. That Reject is not this task's to weaken, so_is_scaffoldis one function with two tiers: T0 always, T2 only when the caller already holds the body. Disclosed residual: a node with an authoredgives:but still-template RULES reads authored tostatusalone.todoandfreezeboth catch it, and it is recorded in the docstring.checked by Tin Dang (unattended)— the ticks came from a process, not a human at a terminal, and the attribution shipped in feat: ceremony proportional to kind and size, andadd checkfor the tally the gate reads #206 records that rather than letting a claimed name stand alone.test_new_scaffold.py's pinned affordance is re-aimed, not deleted; two incumbent value pins intest_todo.pyandtest_doctor.pylikewise. Thetest_doctorfixture was replacing a string the body no longer carried — a silent no-op its own assertion caught.