Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions bin/arc-close
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)"
78 changes: 78 additions & 0 deletions bin/arc-prereg
Original file line number Diff line number Diff line change
Expand Up @@ -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 <arc> <claim_id>.

# Resolve the interpreter by running one — `command -v python3` also finds the Windows Store stub.
Expand Down Expand Up @@ -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 <<EOF
$(printf '%s\n' "$SPEC_FIELDS" | tr '|' '\n')
EOF

if [ -n "$MISSING" ]; then
echo "⛔ prereg refused: the spec does not carry a design, so this seal would be the only key."
printf '%b\n' "$MISSING"
echo " Fill those fields in $SPEC and re-run. (Checked $CHECKED of $CHECKED spec fields.)"
echo " Nothing was written — no .prereg was created."
exit 7
fi
echo " ✅ spec carries a design: $CHECKED/$CHECKED fields hold substance."

printf '%s\n%s\n' "$CLAIM" "$LEDGER" > "$ARC_DIR/.prereg"
echo "🔒 prereg linked: arc $(basename "$ARC_DIR") ← seal $CLAIM"
echo " sealed kill-condition: ${KILL:0:100}$([ "${#KILL}" -gt 100 ] && echo '…')"
Expand Down
6 changes: 5 additions & 1 deletion bin/index-append
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion templates/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading
Loading