From 98bc3c4cce9a1047f15376f8e69fcf40f2f634cf Mon Sep 17 00:00:00 2001 From: Mother Seara Date: Wed, 26 Aug 2026 12:38:15 +0900 Subject: [PATCH] =?UTF-8?q?feat(gates):=20make=20the=20second=20key=20real?= =?UTF-8?q?=20=E2=80=94=20lock=20the=20seal=20to=20a=20filled=20spec,=20an?= =?UTF-8?q?d=20record=20it=20on=20every=20close?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- bin/arc-close | 47 +++++++++++++++- bin/arc-prereg | 78 ++++++++++++++++++++++++++ bin/index-append | 6 +- templates/spec.md | 2 +- tests/test_gates.sh | 131 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 260 insertions(+), 4 deletions(-) diff --git a/bin/arc-close b/bin/arc-close index 88a5943..121401d 100755 --- a/bin/arc-close +++ b/bin/arc-close @@ -88,6 +88,24 @@ SEAL_GATE="no"; [ -n "$SEAL_KILL" ] && SEAL_GATE="yes" # A .prereg that no longer resolves is a silent way around the gate. Never pass it in silence. SEAL_BROKEN="no"; [ -f "$PREREG" ] && [ -z "$SEAL_KILL" ] && SEAL_BROKEN="yes" +# ★2026-08-26 — the second key must be RECORDED, not only enforced. +# Measured before this existed: a KILL close wrote "⚠️ UNSEALED …" into the _SUMMARY, but a GO +# close wrote nothing at all about the seal — `grep -c 'UNSEALED\|prereg\|seal'` = 0. The notice +# was inside emit_kill_defense(), so it only existed on the path that says "this failed". GO is +# the label that OPENS the next step, so the silence was on exactly the wrong side: a half-installed +# environment (yeoul, no mirror-stack) closed arcs that read identically to fully-locked ones. +# This line is emitted for every close and is harness-owned — the 2nd run requires it verbatim, +# so "closed with one key" cannot be quietly edited into "closed with two". +seal_state_line() { + if [ -n "$SEAL_KILL" ]; then + printf -- '- **Second key (pre-registration)**: sealed — claim `%s` in %s\n' "$SEAL_CLAIM" "$SEAL_LEDGER" + elif [ "$SEAL_BROKEN" = "yes" ]; then + printf -- '- **Second key (pre-registration)**: BROKEN — `.prereg` names claim `%s` but it could not be read from %s\n' "$SEAL_CLAIM" "$SEAL_LEDGER" + else + printf -- '- **Second key (pre-registration)**: none — closed with one key only (no seal linked; attestation-only)\n' + fi +} + emit_kill_defense() { # KILL-defense section: sealed → inject verbatim condition; unsealed → attestation-only echo "" echo "## 🛡️ KILL-defense check (anti-premature-closure — all must be filled to seal)" @@ -172,7 +190,7 @@ if [ ! -f "$SUMMARY" ]; then - **Closed**: ${TODAY} (${TS_ISO}) - **stop_reason**: ${STOP} (falsified / no-progress / converged) - **Verdict**: ${VERDICT} - +$(seal_state_line) ## What was closed (the conclusion) - (fill in) @@ -248,6 +266,23 @@ elif [ "$SEAL_GATE" = "yes" ]; then check_answers 'Sealed-condition cross-check' fi +# ⑤ LAST, on purpose. This runs after the blank / broken-anchor / KILL-defense / cross-check +# gates: a summary mangled by an encoding fault fails those with a diagnosis that names the +# real cause, and would fail here too with a mismatch that names the wrong one. The most +# specific gate must speak first. +# The seal-state line is harness-owned. If it drifted from reality — most often because a +# seal was linked AFTER the draft was written — the record would understate or overstate which +# keys were turned. Refuse and show both lines rather than sealing a summary that misreports it. +# NB: `--` is load-bearing. The pattern starts with "- ", so without it grep parses the line as +# options and exits non-zero — which this gate would have read as "the record is wrong". +if ! grep -qxF -- "$(seal_state_line)" "$SUMMARY"; then + echo "⛔ seal refused: the second-key line in _SUMMARY does not match this arc's actual seal state." + echo " in the file : $(grep -m1 -F -- '**Second key (pre-registration)**' "$SUMMARY" || echo '(the line is missing)')" + echo " should read : $(seal_state_line)" + echo " Replace that line with the second one and re-run. (This line is written by the harness, not typed.)" + exit 8 +fi + echo "ARC_CLOSED ${TODAY} stop=${STOP} — ${VERDICT}" >> "$ARC_DIR/STATE.md" mkdir -p "$ARCHIVE_DIR" @@ -281,4 +316,12 @@ echo " arc : $ARC" echo " stop_reason: $STOP" echo " verdict : $VERDICT" echo " seal : $SEAL_MSG" -echo " archived : $ARCHIVE_DIR/$ARC/ (gates passed — blanks & KILL-defense checked)" +# 🔴 Name the gates that RAN. This line used to be a fixed string ending "blanks & KILL-defense +# checked" on every successful close — including GO closes, where emit_kill_defense() never +# produced a section and no KILL-defense was checked at all. A banner that claims a check the +# code skipped is the same defect this repo keeps finding in its own signals. +GATES="blanks" +[ "$IS_KILL" = "yes" ] && GATES="$GATES + KILL-defense" +[ "$SEAL_GATE" = "yes" ] && GATES="$GATES + sealed-condition cross-check" +GATES="$GATES + second-key line" +echo " archived : $ARCHIVE_DIR/$ARC/ (gates run: $GATES)" diff --git a/bin/arc-prereg b/bin/arc-prereg index 4c4769f..9533507 100755 --- a/bin/arc-prereg +++ b/bin/arc-prereg @@ -6,6 +6,16 @@ set -euo pipefail # agent-typed field — closing the "post-hoc widening" hole structurally (the agent isn't its author). # ledger defaults to $YEOUL_LEDGER. Verifies the claim's kill_condition is present before linking. # +# ★ SECOND KEY (2026-08-26): linking is refused unless the arc's spec actually carries a design — +# Goal / Success condition / Kill-condition / Constraints must each hold substance. Before this, +# a spec with every field blank took a seal without a word of warning (measured: exit 0, no +# output), so the "dual key" had a lock on one side only. +# +# 🔴 The lock lives HERE and nowhere else, on purpose. This command is only ever called by +# someone who already has mirror-stack, so a yeoul-only user is not blocked by it — every other +# yeoul command still runs with no ledger present (measured 2026-08-26: eight commands, rc=0). +# Putting the same refusal in arc-close would have made mirror-stack a hard install requirement. +# # Typical flow: seal the kill-condition with mirror-stack (mm_preregister) → arc-prereg . # Resolve the interpreter by running one — `command -v python3` also finds the Windows Store stub. @@ -34,6 +44,74 @@ PY )" [ -n "$KILL" ] || { echo "❌ claim '$CLAIM' with a kill_condition not found in $LEDGER"; exit 1; } +# ── second key: the arc's spec must carry a design before a seal may attach to it ── +# The spec's four load-bearing fields, by the label each is written with. +SPEC_FIELDS="Goal|Success condition|Kill-condition|Constraints" +SPEC="$ARC_DIR/0001_spec.md" +SUBSTANCE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/substance_check.py" + +if [ ! -f "$SPEC" ]; then + echo "⛔ prereg refused: no spec at $SPEC — there is no design for this seal to be the second key of." + exit 6 +fi + +# ★ The judge proves it can still say no BEFORE it is used to say yes. Same rule arc-close follows: +# an instrument is not trusted because it printed green, but because it just separated planted +# violations from planted genuine answers. +if ! st="$($PY "$SUBSTANCE" --selftest 2>&1)"; then + echo "⛔ prereg refused: the substance checker failed its own positive control — the instrument is broken, so the spec is not judged." + printf '%s\n' "$st" | sed 's/^/ /' + exit 6 +fi +echo " 🧪 substance-checker positive control: ${st##*: }" + +# Two spec shapes exist in this repo: `- **Goal**:` (arc-open) and +# `- **Goal** — what do you want to learn / build (observable):` (templates/spec.md). +# substance_check's extractor only strips the first shape, so feeding it a template line verbatim +# would hand the PROMPT to the judge as if it were the ANSWER — a gate that passes on a blank spec. +# Normalise here instead of widening the shared extractor, which ten correct callers depend on: +# take what follows the LAST colon on the line. On an unfilled line that is empty (the colon ends +# the line); on a filled one it can at worst judge a suffix of the answer, which errs strict. +TAB="$(printf '\t')" +MISSING=""; CHECKED=0 +while IFS= read -r label; do + CHECKED=$((CHECKED + 1)) + line="$(grep -m1 -F "**$label" "$SPEC" 2>/dev/null || true)" + if [ -z "$line" ]; then MISSING="$MISSING\n - $label — no such field in the spec"; continue; fi + case "$line" in + *:*) value="${line##*:}" ;; + *) value="" ;; + esac + # An unfilled field is unfilled — do not let the judge's TRIVIAL_VOCAB code name it something + # more specific than what was seen. The reason a person reads must match what actually happened. + if [ -z "$(printf '%s' "$value" | tr -d '[:space:]')" ]; then + MISSING="$MISSING\n - $label — empty (nothing written after the label)"; continue + fi + # Fail closed: empty judge output means the checker could not run, which is not a pass. + vout="$(printf -- '- **%s**: %s' "$label" "$value" | $PY "$SUBSTANCE" --label "$label" 2>/dev/null || true)" + vcode="${vout%%"$TAB"*}" + [ "$vcode" = "OK" ] && continue + case "$vcode" in + ""|DECODE_FAILED) why="could not be judged (empty or undecodable)" ;; + TRIVIAL_VOCAB) why="nothing but trivial vocabulary" ;; + DEFERRAL) why="an honest non-answer (\"TODO\" / \"not decided\") — it cannot ground a seal" ;; + THIN_CONTENT|LOW_DIVERSITY|REPEATED_UNIT) why="too thin to carry a design decision" ;; + *) why="rejected by the substance check ($vcode)" ;; + esac + MISSING="$MISSING\n - $label — $why" +done < "$ARC_DIR/.prereg" echo "🔒 prereg linked: arc $(basename "$ARC_DIR") ← seal $CLAIM" echo " sealed kill-condition: ${KILL:0:100}$([ "${#KILL}" -gt 100 ] && echo '…')" diff --git a/bin/index-append b/bin/index-append index 6f5d1e1..34a01f9 100755 --- a/bin/index-append +++ b/bin/index-append @@ -62,7 +62,11 @@ REL="${ARC_DIR#"$SCRIPT_DIR"/../}" { printf '## %s · `%s` · %s\n' "${DATE:--}" "${STOP:--}" "$ARC" printf -- '- **Verdict**: %s\n' "$VERDICT" - [ "$CLAIM" != "-" ] && printf -- '- **Sealed claim**: `%s`\n' "$CLAIM" + # 🔴 Always emit this row. It used to be printed only when a claim existed, so an unsealed close + # produced an entry with the line simply absent — and the index header promises the column. + # A reader could not tell "no seal" from "this row predates the field" or "the write failed". + if [ "$CLAIM" != "-" ]; then printf -- '- **Sealed claim**: `%s`\n' "$CLAIM" + else printf -- '- **Sealed claim**: none (closed with one key only)\n'; fi printf -- '- **Closed**: %s\n' "$CLOSED" printf -- '- source: %s\n\n' "$REL" } >> "$INDEX" diff --git a/templates/spec.md b/templates/spec.md index a5c4a39..e787e3e 100644 --- a/templates/spec.md +++ b/templates/spec.md @@ -11,7 +11,7 @@ - `wedge` (a real new angle) · `footnote` (known idea, minor twist) · `none` (existing tools suffice → consider rejecting here). - 🚧 If `none`: STOP and confirm [reject / re-frame / proceed anyway]. If not "proceed", stop and record the rejection reason (saves the rest of the interview). - **Success condition** — what does "it worked" look like (observable): -- **Kill-condition (falsifier)** — what does "no / wrong" look like? ★No falsifier = untestable. +- **Kill-condition (falsifier)** — what does "no / wrong" look like? ★No falsifier = untestable: - **Constraints** — resources · substrate · time · out of scope: - **Roles (roster)** — which stances debate (default: analysis[red-team] · impl · repro): - **(optional) Pre-registration seal id** — seal the kill-condition before spending compute. diff --git a/tests/test_gates.sh b/tests/test_gates.sh index b5747d2..da2ab3b 100755 --- a/tests/test_gates.sh +++ b/tests/test_gates.sh @@ -32,6 +32,31 @@ check() { # check — passes if the command succeeds # portable in-place edit (GNU + BSD/macOS) sedi() { sed "$1" "$2" > "$2.t" && mv "$2.t" "$2"; } +fill_spec() { # fill_spec — write a real design into the four load-bearing fields + $PY - "$1" <<'PYEOF' +import sys +p = sys.argv[1] +vals = { + 'Goal': 'measure whether a seal can attach to an arc whose spec was never filled in', + 'Success condition': 'a blank spec is refused and a filled spec links, both shown by running it', + 'Kill-condition (falsifier)': 'a blank spec still links a seal, or a filled spec is refused', + 'Constraints': 'shell only, no mirror-stack code touched, must run with no ledger present', +} +out = [] +for ln in open(p, encoding='utf-8'): + for k, v in vals.items(): + # prefix match: the internal copy labels this field `**Kill-condition (falsifier)**`, + # and an exact match would silently leave it blank — then the gate's correct refusal + # reads as a test failure. + if ln.startswith('- **%s' % k): + ln = ln.rstrip('\n').rstrip() + ' ' + v + '\n' + break + out.append(ln) +open(p, 'w', encoding='utf-8').write(''.join(out)) +PYEOF +} + + # --- arc-close 2-phase KILL gate --- "$BIN/arc-open" g --topic="gate test" --arcs-dir="$WS/arcs" >/dev/null 2>&1 ARC="$(ls -d "$WS"/arcs/*_g)" @@ -59,6 +84,9 @@ LEDGER="$WS/ledger.jsonl" printf '%s\n' '{"claim_id":"c1","metric":"m","kill_condition":"effect size d < 0.2 over >= 3 seeds","kill_threshold":{}}' > "$LEDGER" "$BIN/arc-open" s --topic="sealed gate" --arcs-dir="$WS/arcs" >/dev/null 2>&1 SARC="$(ls -d "$WS"/arcs/*_s)" +# A seal may no longer attach to an arc with a blank spec (see the DUAL KEY block below), +# so this setup fills the design first — the thing under test here is the seal link. +fill_spec "$SARC/0001_spec.md" YEOUL_LEDGER="$LEDGER" "$BIN/arc-prereg" "$SARC" c1 >/dev/null 2>&1; assert "arc-prereg links a valid claim" 0 $? "$BIN/arc-close" "$SARC" "KILL — sealed" --stop=falsified >/dev/null 2>&1 # draft (injects verbatim) SSUM="$(ls "$SARC"/_SUMMARY_*.md)" @@ -82,6 +110,7 @@ sedi 's/^- \*\*Catalog.*/- **Catalog cross-check**: none/' "$SSUM" # closed as a PASS with the pre-registered bar never mentioned. The label is agent-written; the seal is not. "$BIN/arc-open" pv --topic="sealed pass" --arcs-dir="$WS/arcs" >/dev/null 2>&1 PARC="$(ls -d "$WS"/arcs/*_pv)" +fill_spec "$PARC/0001_spec.md" YEOUL_LEDGER="$LEDGER" "$BIN/arc-prereg" "$PARC" c1 >/dev/null 2>&1 "$BIN/arc-close" "$PARC" "converged — design settled" --stop=converged >/dev/null 2>&1 # draft PSUM="$(ls "$PARC"/_SUMMARY_*.md)" @@ -483,6 +512,108 @@ for CP in ascii cp949 cp1252; do else bad "[ENC-03/$CP] selftest did not survive: $(printf '%s' "$ESELF" | tail -1)"; fi done +# ───────────────────────────────────────────────────────────────────────────────────────── +# DUAL KEY — the lock, and the guarantee that it does not become an install requirement. +# +# Measured 2026-08-26, before any of this existed: +# · `arc-prereg` attached a seal to a spec with EVERY field blank — exit 0, not a word printed. +# · A KILL close wrote "UNSEALED" into its _SUMMARY; a GO close wrote nothing about the seal at +# all (grep count 0). The notice lived inside the KILL branch, so the one label that OPENS the +# next step was the silent one. +# · The closing banner said "blanks & KILL-defense checked" on every close, as a fixed string, +# including closes where no KILL-defense section was ever produced. +# ───────────────────────────────────────────────────────────────────────────────────────── +DK="$WS/dk"; mkdir -p "$DK" +printf '%s\n' '{"claim_id":"dk-1","kill_condition":"the dual-key gate lets a blank spec through"}' > "$DK/led.jsonl" + +# --- the lock: a blank spec may not take a seal --- +"$BIN/arc-open" dkblank --arcs-dir="$DK/arcs" >/dev/null 2>&1 +DKB="$(ls -d "$DK"/arcs/*dkblank)" +"$BIN/arc-prereg" "$DKB" dk-1 "$DK/led.jsonl" >/dev/null 2>&1 +assert "[DK-01] blank spec cannot take a seal" 7 $? +if [ ! -f "$DKB/.prereg" ]; then ok "[DK-02] refusal wrote nothing (no .prereg left behind)" +else bad "[DK-02] refused but a .prereg exists anyway"; fi + +# --- and the other direction: a gate that only ever refuses is not a gate --- +"$BIN/arc-open" dkfull --arcs-dir="$DK/arcs" >/dev/null 2>&1 +DKF="$(ls -d "$DK"/arcs/*dkfull)" +fill_spec "$DKF/0001_spec.md" +"$BIN/arc-prereg" "$DKF" dk-1 "$DK/led.jsonl" >/dev/null 2>&1 +assert "[DK-03] a filled spec still links (the lock can say yes)" 0 $? + +# --- the declaring door: EVERY close records which keys were turned --- +"$BIN/arc-open" dkgo --arcs-dir="$DK/arcs" >/dev/null 2>&1 +DKG="$(ls -d "$DK"/arcs/*dkgo)" +"$BIN/arc-close" "$DKG" "GO — dual key test" >/dev/null 2>&1 +DKSUM="$(ls "$DKG"/_SUMMARY_*.md)" +# 🔴 GO, not KILL. This is the path that used to say nothing. +if grep -qF -- '**Second key (pre-registration)**: none' "$DKSUM"; then + ok "[DK-04] a GO close records that it closed with one key only" +else bad "[DK-04] GO close is silent about the seal state again"; fi +sedi 's/- (fill in)/- concrete conclusion here/' "$DKSUM" +DKOUT="$("$BIN/arc-close" "$DKG" "GO — dual key test" 2>&1)"; assert "[DK-05] GO close still seals" 0 $? +# the banner must name the gates that RAN — a GO close has no KILL-defense section to check +if printf '%s' "$DKOUT" | grep -q 'gates run:' && ! printf '%s' "$DKOUT" | grep -q 'KILL-defense'; then + ok "[DK-06] the closing banner does not claim a KILL-defense check that never ran" +else bad "[DK-06] banner still claims KILL-defense on a GO close"; fi +if grep -qF -- '**Sealed claim**: none' "${YEOUL_INDEX:-$WS/KNOWLEDGE_INDEX.md}" 2>/dev/null; then + ok "[DK-07] the knowledge index says 'none' instead of dropping the promised column" +else bad "[DK-07] index row omits the Sealed claim column again"; fi + +# --- the record is harness-owned: it cannot be edited into a nicer story --- +"$BIN/arc-open" dktamper --arcs-dir="$DK/arcs" >/dev/null 2>&1 +DKT="$(ls -d "$DK"/arcs/*dktamper)" +fill_spec "$DKT/0001_spec.md" +"$BIN/arc-prereg" "$DKT" dk-1 "$DK/led.jsonl" >/dev/null 2>&1 +"$BIN/arc-close" "$DKT" "GO — tamper test" >/dev/null 2>&1 +DKTSUM="$(ls "$DKT"/_SUMMARY_*.md)" +sedi 's/^- \*\*Second key (pre-registration)\*\*: sealed.*/- **Second key (pre-registration)**: none — closed with one key only (no seal linked; attestation-only)/' "$DKTSUM" +sedi 's/- (fill in)/- concrete conclusion here/' "$DKTSUM" +sedi 's/^\(- \*\*Result triggers the sealed condition?\*\*\): (unfilled).*/\1: no. The run produced four rows and the sealed condition requires zero, so it is not met./' "$DKTSUM" +"$BIN/arc-close" "$DKT" "GO — tamper test" >/dev/null 2>&1 +assert "[DK-08] a hand-edited second-key line is refused" 8 $? + +# --- the OTHER spec shape: templates/spec.md, where the label is followed by a prompt --- +# Two shapes exist. arc-open writes `- **Goal**:`; templates/spec.md writes +# `- **Goal** — what do you want to learn / build (observable):`. substance_check's extractor only +# strips the first, so handing it a template line verbatim would give the JUDGE THE PROMPT as if it +# were the answer — a lock that opens on a spec nobody filled. DK-10 is that vacuity check. +"$BIN/arc-open" dktpl --arcs-dir="$DK/arcs" >/dev/null 2>&1 +DKP="$(ls -d "$DK"/arcs/*dktpl)" +cp "$(dirname "$BIN")/templates/spec.md" "$DKP/0001_spec.md" +"$BIN/arc-prereg" "$DKP" dk-1 "$DK/led.jsonl" >/dev/null 2>&1 +assert "[DK-10] an unfilled template spec is refused (the prompt is not read as the answer)" 7 $? +# …and the template must have somewhere to write each answer. Its Kill-condition line ended in +# "untestable." with no colon, so there was no slot at all for the one field the seal is about. +fill_spec "$DKP/0001_spec.md" +"$BIN/arc-prereg" "$DKP" dk-1 "$DK/led.jsonl" >/dev/null 2>&1 +assert "[DK-11] a filled template spec links (every field has a slot to write in)" 0 $? + +# --- 🔴 the guarantee: none of this may turn mirror-stack into an install requirement --- +# `arc-prereg` is the ONLY command that needs a ledger, and it is only ever called by someone who +# already has one. Everything else must still run with no mirror-stack anywhere on PATH. +DKN="$WS/nomirror"; mkdir -p "$DKN" +NOMIRROR_FAIL="" +run_bare() { # run_bare + local n="$1"; shift + env PATH=/usr/bin:/bin YEOUL_INDEX="$DKN/index.md" YEOUL_PROJECTS="$DKN/projects" "$@" >/dev/null 2>&1 \ + || NOMIRROR_FAIL="$NOMIRROR_FAIL $n" +} +run_bare yeoul-new "$BIN/yeoul-new" probe +run_bare arc-open "$BIN/arc-open" probe --arcs-dir="$DKN/arcs" +NB="$(ls -d "$DKN"/arcs/*probe 2>/dev/null | head -1)" +run_bare arc-list "$BIN/arc-list" --arcs-dir="$DKN/arcs" +run_bare arc-roles "$BIN/arc-roles" "$NB" +run_bare build-handoff "$BIN/build-handoff" probe +run_bare verify-gate "$BIN/verify-gate" "$DKN/projects/probe/dev/TODO.md" +run_bare loop-guard "$BIN/loop-guard" "$NB" +run_bare arc-close-draft "$BIN/arc-close" "$NB" "GO — no mirror" +[ -n "$NB" ] && sedi 's/- (fill in)/- concrete conclusion here/' "$(ls "$NB"/_SUMMARY_*.md)" +run_bare arc-close-seal "$BIN/arc-close" "$NB" "GO — no mirror" +if [ -z "$NOMIRROR_FAIL" ]; then + ok "[DK-09] with no mirror-stack on PATH, every other command still runs (9/9)" +else bad "[DK-09] the dual key blocked a yeoul-only user:$NOMIRROR_FAIL"; fi + echo if [ "$CHECKS" -eq 0 ]; then echo "⛔ 0/0 — no checks were collected; an empty run is a failure, not a pass"