feat(gates): make the second key real — lock the seal to a filled spec, and record it on every close - #10
Merged
Merged
Conversation
…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
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>
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.
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-preregattached a seal to an arc whose spec had every field blank — Goal, Success condition, Kill-condition, Constraints — withexit 0and not a word printed:arc-preregnow refuses unless those four fields carry substance, judged by the samesubstance_check.pythe 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:
- **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-10is 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-11fails 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: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.**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.blanks & KILL-defense checkedas 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.Sealed claimrow 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 saysnone (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 onPATH, every other command still runs:yeoul-new·arc-open·arc-list·status·arc-roles·build-handoff·verify-gate·loop-guard· a fullarc-closeround trip → 9/9 rc=0DK-09pins it. Putting the same refusal inarc-closewould have broken all nine and turned a lock into an installation prerequisite.Tests
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.