Skip to content

feat(gates): make the second key real — lock the seal to a filled spec, and record it on every close - #10

Merged
bhyi4 merged 1 commit into
mainfrom
feat/dual-key-declare-and-lock
Aug 26, 2026
Merged

feat(gates): make the second key real — lock the seal to a filled spec, and record it on every close#10
bhyi4 merged 1 commit into
mainfrom
feat/dual-key-declare-and-lock

Conversation

@bhyi4

@bhyi4 bhyi4 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Yeoul and mirror-stack are meant to be two keys: yeoul decides the design, mirror-stack seals the kill-condition before compute. Measured 2026-08-26: one side had no lock, and the other had no record.

1. The lock was missing

arc-prereg attached a seal to an arc whose spec had every field blank — Goal, Success condition, Kill-condition, Constraints — with exit 0 and not a word printed:

$ arc-open emptyspec-probe --arcs-dir=$T/arcs      # spec: all fields blank
$ arc-prereg $ARC probe-claim-0001 $T/ledger.jsonl
🔒 prereg linked: arc … ← seal probe-claim-0001     ← rc=0, no warning

arc-prereg now refuses unless those four fields carry substance, judged by the same substance_check.py the close gate uses — with its positive control run first. An instrument is trusted because it just proved it can still say no, not because it printed green.

Two details decide whether the lock is real rather than decorative:

  • Two spec shapes exist. - **Goal**: (arc-open) and - **Goal** — what do you want to learn / build (observable): (templates/spec.md). substance_check's extractor only strips the first, so feeding it a template line verbatim would hand the judge the prompt as if it were the answer — a lock that opens on a spec nobody filled. Normalised at this call site rather than by widening the shared extractor that ten correct callers depend on. DK-10 is that vacuity check.
  • templates/spec.md's Kill-condition line had no colon — it ended ★No falsifier = untestable., so there was nowhere to write the one field the seal is about. DK-11 fails without the fix.

2. The record was missing on the half that matters

A KILL close wrote ⚠️ UNSEALED … into its _SUMMARY. A GO close wrote nothing about the seal at all:

grep -c 'UNSEALED\|prereg\|seal'  <GO close _SUMMARY>   →  0

The notice lived inside emit_kill_defense(), so it existed only on the path that says this failed — while GO is the label that opens the next step. A yeoul-only environment closed arcs that read identically to fully-locked ones.

  • Every close now carries a harness-owned **Second key (pre-registration)** line. The 2nd run requires it verbatim, so "closed with one key" cannot be edited into "closed with two" (DK-08). It runs last of the gates on purpose: a summary damaged by an encoding fault must fail with the gate that names the real cause, not with a mismatch here that names the wrong one.
  • The closing banner said blanks & KILL-defense checked as a fixed string on every successful close — including GO closes where no KILL-defense section was ever produced. It now names the gates that actually ran.
  • The knowledge index printed its Sealed claim row only when a claim existed, so an unsealed close produced an entry with the promised column simply absent — indistinguishable from an old row or a failed write. It now says none (closed with one key only).

3. The guarantee: this must not make mirror-stack an install requirement

The lock lives only in arc-prereg, which is only ever called by someone who already has a ledger. Measured before and after — with no mirror-stack anywhere on PATH, every other command still runs:

yeoul-new · arc-open · arc-list · status · arc-roles · build-handoff · verify-gate · loop-guard · a full arc-close round trip → 9/9 rc=0

DK-09 pins it. Putting the same refusal in arc-close would have broken all nine and turned a lock into an installation prerequisite.

Tests

gates 72/72 (61 before)
revert-to-red all four changed files reverted in turn → suite red for each
pre-publish guard clean

Two pre-existing tests linked a seal to an arc whose spec was never written. That setup is no longer valid, so they fill the design first — the thing under test there is the seal link, not the spec.

…c, and record it on every close

Yeoul and mirror-stack were meant to be two keys: yeoul decides the design, mirror-stack seals the
kill-condition before compute. Measured 2026-08-26, one side had no lock and the other had no record.

**The lock was missing.** `arc-prereg` attached a seal to an arc whose spec had every field blank —
Goal, Success condition, Kill-condition, Constraints — with exit 0 and not a word printed. The
"dual key" turned one key.

`arc-prereg` now refuses unless those four fields carry substance, judged by the same
`substance_check.py` the close gate uses, with its positive control run first: an instrument is
trusted because it just proved it can still say no, not because it printed green.

Two details that decide whether the lock is real:

  · Two spec shapes exist — `- **Goal**:` (arc-open) and `- **Goal** — what do you want to learn
    (observable):` (templates/spec.md). `substance_check`'s extractor only strips the first, so
    feeding it a template line verbatim would hand the JUDGE THE PROMPT as if it were the answer,
    and the lock would open on a spec nobody filled. Normalised at this call site rather than by
    widening the shared extractor that ten correct callers depend on. DK-10 is that vacuity check.
  · `templates/spec.md`'s Kill-condition line ended in "untestable." with no colon — there was
    nowhere to write the one field the seal is about. Fixed; DK-11 fails without it.

**The record was missing on the half that matters.** A KILL close wrote "⚠️ UNSEALED …" into its
_SUMMARY; a GO close wrote nothing about the seal at all (measured: grep count 0). The notice lived
inside `emit_kill_defense()`, so it only existed on the path that says "this failed" — while GO is
the label that OPENS the next step. A yeoul-only environment closed arcs that read identically to
fully-locked ones. Three fixes, all on that silence:

  · every close now carries a harness-owned `**Second key (pre-registration)**` line; the 2nd run
    requires it verbatim, so "closed with one key" cannot be edited into "closed with two" (DK-08).
    It runs last of the gates on purpose — a summary damaged by an encoding fault must fail with
    the gate that names the real cause, not with a mismatch here that names the wrong one.
  · the closing banner said "blanks & KILL-defense checked" as a fixed string on every successful
    close, including GO closes where no KILL-defense section was ever produced. It now names the
    gates that actually ran.
  · the knowledge index printed its `Sealed claim` row only when a claim existed, so an unsealed
    close produced an entry with the promised column simply absent — indistinguishable from an old
    row or a failed write. It now says `none (closed with one key only)`.

**The guarantee.** None of this may turn mirror-stack into an install requirement. The lock lives
only in `arc-prereg`, which is only ever called by someone who already has a ledger. Measured
before and after: with no mirror-stack anywhere on PATH, every other command still runs — yeoul-new,
arc-open, arc-list, status, arc-roles, build-handoff, verify-gate, loop-guard, and a full arc-close
round trip. DK-09 pins that; putting the same refusal in arc-close would have broken all nine.

Two pre-existing tests linked a seal to an arc whose spec was never written. That setup is no longer
valid, so they fill the design first — the thing under test there is the seal link, not the spec.

Gates 72/72 (61 before). Each of the four changed files was reverted in turn and the suite went red
for all four. Pre-publish guard clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bhyi4
bhyi4 merged commit e0b8a8f into main Aug 26, 2026
4 checks passed
bhyi4 added a commit that referenced this pull request Aug 28, 2026
Until now no version number identified a build. `CHANGELOG.md` has said `[0.1.0] — unreleased`
since the first commit on 2026-07-21 while eighteen more went in; there are no tags and no releases.
That was nobody's decision — the number simply carried no weight, so nothing exposed its absence.

#9 gave it weight. Once the server announces its own version in `serverInfo`, that string is what a
client reads to tell builds apart — and it stayed `0.1.0` across the fix, so the server that reported
the SDK's version and the server that reports its own are indistinguishable by the very field the
fix repaired. Bumping is the other half of #9, not bookkeeping.

0.2.0, not 0.1.1: #10 changed behaviour for anyone using both tools — `arc-prereg` now refuses an
arc whose spec is blank, and every close writes a `Second key` line into its `_SUMMARY`.

Also closes the packaging door on the same defect. The version lives in two files: `pyproject.toml`
is what pip records and what a release is cut from, `yeoul_mcp/__init__.py` is what the server
speaks. Every existing check compared the server against `__init__.py`, so the two could drift with
the suite still green — measured: setting pyproject alone to 9.9.9 kept it at 6/6. A check now fails
when they disagree, verified by forcing the drift. That belongs in the suite rather than in a release
checklist nobody runs, because cutting a release is exactly when the two get touched.

The denominator guard added in #9 earned its place immediately: this commit added two checks and
bumped the expected count by one, and the run refused itself with "8 checks accounted for, 7
expected" rather than reporting a green 8/8.

Suites from the repo root: serverInfo 8/8 · _run contract 8/8 · gates 72/72 · pre-publish 43 files
clean.

Co-authored-by: Mother Seara <seara@bhyi4.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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