Skip to content

feat: RISK-ACCEPTED signs for weak evidence never a missing seal, and the ONE approval asks its questions out loud - #208

Merged
TinDang97 merged 9 commits into
mainfrom
feat/gate-seal-enforcement
Sep 1, 2026
Merged

feat: RISK-ACCEPTED signs for weak evidence never a missing seal, and the ONE approval asks its questions out loud#208
TinDang97 merged 9 commits into
mainfrom
feat/gate-seal-enforcement

Conversation

@TinDang97

@TinDang97 TinDang97 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #207 (feat/affordance-truth), which is the base here so the diff is just these 7 commits. Retarget to main before #207 merges — merging the parent with --delete-branch first orphans this one.

Two nodes, both driven through ADD, both closed on a receipt-backed gate. They are one finding at two depths: a guard written for one verdict is a guard for one verdict.

1 — RISK-ACCEPTED signs for weak evidence, never for a missing seal

#206 found that skipping freeze did not fail the post-freeze guards, it switched them off, and closed that hole for PASS. Every one of gate's sixteen integrity refusals was written verdict == "PASS", so the identical hole stayed open one verdict over. Measured before the fix — a Task created seconds earlier, still carrying goal: <one line> and every template section, never frozen, never briefed:

$ add run probe -- true          &&  add gate probe RISK-ACCEPTED --reason "probing"
$ add done probe
/tasks/probe.md is done

Three calls, from scaffold to done, past R:UNSEALED · drift · R:UNBRIEFED · the placeholder check · R:UNDECLARED_SENSITIVE. The lane meant to be the scrutinised one carried no scrutiny at all.

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. The split is "would a verdict here be a FABRICATED record, or merely an OPTIMISTIC one?"

  • INTEGRITY_REFUSALS protect the RECORD — unsealed · drift · placeholders · undeclared-sensitive · phantom-scope, plus two explore equivalents. They bind every verdict that approves or closes. Accepting a risk is not a route around the seal.
  • EVIDENCE_REFUSALS judge the RUN — stale receipt · failed run · unbound covers: · open questions · missing security lens. PASS only: signing for imperfect evidence is exactly 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 — a refusal added later cannot silently inherit either tier.

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). Two smaller findings fell out of the same read, both re-characterized by execution rather than taken from notes:

  • _paths_touch over-matched on string prefix. 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 signed for a changed srcfoo/secret.yaml, and secrets_public/ answered for secrets/**. Now whole-segment.
  • An unreadable sensitivity: floored DOWN. high and critical both read process. Two real nodes on the parent branch declared high and floored to process where they meant plan. It now floors to human, and new 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 same reason.

2 — The ONE approval asks its questions out loud

freeze already refused an incomplete contract eight ways. Every one checks the document. None checked 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 those down. Nothing made it ask. So ADD's one human approval was a single stamp that said nothing about whether the human had seen a single decision they were approving.

$ add freeze <slug> --by "Tin Dang" --authority human
cannot freeze `<slug>` — the ONE human approval is being asked for decisions
no human has been shown: A1, A2, A3, A5, R:HATCH -> "R:UNINTERVIEWED"
next: add interview <slug>

add interview (the 24th verb) compiles every non-n/a assumption and every Reject into a numbered question carrying the reading and the cost. Bare it is a read; --answer <id>=confirm|correct|defer --by "<name>" writes a .d/interviews/<n>.md sidecar and one act: interview stamp — the same sidecar shape .d/runs/ uses, so no body section is renumbered (the engine keys sections by number, which has bitten this repo).

Design calls, each pinned:

  • It runs LAST in freeze's ladder — there is no sense putting template text to a human.
  • Keyed on the computed floor, never on --authority. The line below it is authority or authority_for(...), so reading the flag would have shipped the guard with its own off switch: --authority process on a security node.
  • Staleness by digest, like direction: and brief:. interview_gap reads the stamp whose digest matches the current text, not the latest — revert an edit and the earlier interview answers for it. Reword an interviewed assumption and it re-opens; edit a Must and it does not, because the Musts came from the human.
  • 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.
  • The engine claims nothing about humanity. --by is recorded verbatim, exactly as freeze --by is. R:SELFANSWER is shipped prose, and a check proves the prose ships — a guard that asserts what it cannot prove is how a stamp becomes theatre.

Verification

The gate refused the first PASS on both nodes, and was right both times. In each case the culprit was the same class: assumptions added during authoring to close the sweep had checks written but never declared in ## CHECKS — the docstring covers: is not the binding. Two rules had no check at all.

Writing one of those missing checks found a defect on its first run: rewriting sixteen conditions into two tuples is exactly the edit where a refusal quietly vanishes, and one had — R:UNFROZEN_EXPLORE was listed in the integrity tuple but never dispatched, because it refuses unconditionally, HARD-STOP included. Routing it through _binds would have narrowed an existing refusal to buy tidiness. It comes out, with the reason recorded at the site. (The check's own count was wrong too — it asserts an explicit member set now, which pins strictly more than a number.)

  • Red first: 13/19 and 18/21. The green ones are deliberate counter-guards — a process-floor node is never interviewed, a lone refreeze still seals, a legitimate RISK-ACCEPTED still closes, a first call still hears the receipt message, a HARD-STOP stays recordable on an unfrozen node.
  • 861 passed / 0 failed / 7 skipped across both roots. Four engine twins synced, both MD5 pins re-aimed.
  • Dogfooded: add interview freeze-interview puts all 12 of that node's own open decisions.

Reviewer notes

  • An incumbent frozen contract overruled a design choice here. I classed unbriefed as integrity; test_non_pass_verdicts_are_never_blocked carries brief-gate's frozen M3 — "a verdict is how a node LEAVES a bad state." That Reject belongs to another task, so the refusal moved rather than the test. It reads correctly on the merits: a missing brief is a fact about how the run was driven, and the seal, drift and placeholder guards still bind every verdict.
  • The interview feature does not gate itself. Its node is architecture → floor plan, and M5 keys on the computed floor, so it was exempt from the refusal it ships. That is the design — a plan freeze has no human in the room — but the first real exercise of R:UNINTERVIEWED will be the next security-floored node.
  • Six incumbent fixtures were corrected, none weakened; every assertion is unchanged. Four forged a gate stamp with no freeze, or called add.freeze and discarded a refusal. They now include the seal the method always implied and assert it landed — a fixture that silently skips the one approval tests no real path.
  • Adding a verb rippled into six registries and five SKILL.md budget pins, one of which is literally R:BUDGET_BUMP — so the addition was funded by compressing the freeze bullet beside it, back to 176 lines exactly. test_no_new_verb_in_the_cli_surface is re-aimed 23 → 24, not deleted: its claim is that the authoring-beat task added no verb, and a later task legitimately adding one moves the value without weakening that.
  • A separate hole is flagged but not fixed here: freeze takes authority or authority_for(...), so an explicit --authority process downgrades even a security node outright.

Update (pre-merge review): rebased onto main after #207 merged as a squash, so this now targets main directly and carries only its own 9 commits.

The review found one defect, and the node was reopened to build rather than patched after close. interview_gap returned on the FIRST stamp whose digest matched the node — so 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, which is the one action that DISCARDS the answers. Reading the last match instead would have been wrong in the opposite direction: a follow-up correct outranked by the earlier confirm.

Passes over the same text now fold in order, later winning. 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 — the sidecar remains the human-readable record, the stamp is the machine-readable delta. Two checks, red first, the second being the counter-guard for the first.

Also found, NOT fixed here — pre-existing: _oneline is applied to --reason but not to --by, on freeze and gate alike. add freeze --by "ev{il}" writes a stamp that parses back as zero stamps: the whole verified: ledger is lost, and that ledger's ordering IS the trust model. interview follows the same incumbent convention. Worth its own node.

863 passed / 0 failed / 7 skipped after the rebase.

TinDang97 added a commit that referenced this pull request Sep 1, 2026
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
…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
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
…ng 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
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
… 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
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
… 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
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
The reopen reset the gate; receipt 3 re-earns it at 25/25 bound.

author: Tin Dang
@TinDang97
TinDang97 changed the base branch from feat/affordance-truth to main September 1, 2026 08:13
@TinDang97
TinDang97 force-pushed the feat/gate-seal-enforcement branch from ecb36f5 to 9abfbd0 Compare September 1, 2026 08:13
@TinDang97
TinDang97 merged commit 2c68c6e into main Sep 1, 2026
8 checks passed
@TinDang97

Copy link
Copy Markdown
Collaborator Author

Correction to the note added to this PR body before merge.

The pre-merge review note claimed add freeze --by "ev{il}" writes a stamp that parses back as zero stamps and loses the whole verified: ledger. That is wrong. The probe behind it froze an unauthored node, so freeze correctly refused on template placeholders and wrote nothing — I read a correct refusal as corruption. Braces in --by round-trip fine.

There is a real defect in the same area, now measured properly. The trigger is an odd number of double-quotes, not a brace — which is precisely why _oneline (which maps " to ') makes --reason safe and leaves --by exposed:

$ add freeze t --by 'O"Brien' --authority human
freeze recorded at authority `human`          # reported as success

stamp keys read back: ['by']                   # act, authority, direction all swallowed
_is_frozen       -> False
sealed_direction -> None

The seal silently does not exist while the human is told it does. It fails closed — the gate then refuses with R:UNSEALED — so nothing is let through; the damage is a verb reporting a record it did not write, across all seven raw by: interpolations (freeze, brief, replan, gate ×2, check, interview). Balanced quotes (Tin "TinDang97" Dang) are unaffected.

Filed and fixed separately before 3.3.0.

TinDang97 added a commit that referenced this pull request Sep 1, 2026
…and 3.3.0 (#209)

* fix(engine): a stamp the notary reports written is a stamp that reads back

`_oneline` exists because an unbalanced `{` in a `--reason` once made the parser
swallow the following stamp. That fix was correct and was applied to exactly one
field. Seven writers interpolated `by:` raw.

Measured on the incumbent engine:

    $ add freeze t --by 'O"Brien' --authority human
    freeze recorded at authority `human`     # reported as SUCCESS
    stamp keys read back: ['by']             # act, authority, direction swallowed
    _is_frozen -> False  ·  sealed_direction -> None

The seal silently does not exist while the human is told it does. The trigger is
an ODD number of `"`; a balanced pair round-trips, which is exactly why this
survived every real use of the engine.

Severity, stated honestly: it fails CLOSED. The gate then refuses with
R:UNSEALED, so nothing is let through. The defect is that a notary whose only
job is to record faithfully reported a record it did not write -> R:LIE.

All seven `by:` interpolations now go through `_oneline` (freeze · brief ·
replan · check · interview · both gate paths), and the reason is written at the
function's definition, since the fix is one careless edit away from reverting.

`test_every_stamp_writer_normalises_its_by` enumerates the writers FROM THE
SOURCE rather than from a hand list — a hand list is precisely how the seventh
writer gets missed, which is the shape of the defect itself.

Corrections to my own work, both disclosed rather than quietly fixed:

  * I first reported this as a BRACE bug that destroyed the whole `verified:`
    ledger. That was wrong. The probe behind it froze an UNAUTHORED node, so
    `freeze` correctly refused on template placeholders and wrote nothing — I
    read a correct refusal as corruption. The claim is corrected on PR #208.
  * M1 (`_oneline` neutralises the quote) was already TRUE before this task;
    `_oneline` has always replaced `"`. It stays as a regression pin, but the
    real rule here is M2 — the writers that never called it.
  * `test_no_existing_stamp_is_rewritten` first compared whole-file text, which
    a later write legitimately grows. It compares the parsed stamp list now.

873 passed across both roots. Twins synced, ENGINE_MD5 re-aimed.

author: Tin Dang

* chore(node): close stamp-field-integrity

The gate refused the first PASS: M4 was declared with no check. The punctuation
walk proves the property, so it now asserts the BICONDITIONAL — what the verb
reports must match what the ledger holds — rather than gaining a stapled covers.

Gated PASS at `plan` on receipt 2, 11/11 bound.

author: Tin Dang

* chore(release): 3.3.0 — the seal means something under every verdict

Six version sources in lockstep: pyproject.toml · package.json ·
package-lock.json (2 places) · .claude-plugin/plugin.json ·
src/add_method/__init__.py · tooling/add.py's ENGINE.

Bumping ENGINE re-aims add.py, so the four engine twins are resynced, ENGINE_MD5
re-aimed, and both dogfood bundles' `engine:`/`tooling_engine:` stamps moved to
add/3.3.0 — those ride every version bump and go stale silently otherwise.

CHANGELOG covers the release honestly, including that the RISK-ACCEPTED hole
failed CLOSED and that the stamp defect made a verb report a record it had not
written.

873 passed / 7 skipped across both roots.

Tag and publish are NOT part of this commit — they remain the human call.

author: Tin Dang
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant