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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ All notable changes to this project are documented here.

Initial extraction of the Yeoul harness (de-personalized structure only).

- **Deliberation engine**: `arc-open`/`arc-close` (2-phase close with blank-refusal + KILL-defense 5-check),
- **Deliberation engine**: `arc-open`/`arc-close` (2-phase close with blank-refusal + KILL-defense 5-check
+ a label-independent sealed-condition cross-check whenever a prereg seal is linked),
ticket/attach/watch/list/roles/join relay helpers, `loop-guard` (round/token/no-progress bounds).
- **Lifecycle**: `yeoul-new`, `build-handoff` (verify-gated TODO), `ralph` (autonomous dev loop with a
verify-gate), `graduate`, `close-project`, `status`.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ New to the method? Read [`docs/METHODOLOGY.md`](docs/METHODOLOGY.md), then paste
- **2-phase close** — closing drafts a summary; it only seals once the blanks are filled.
- **KILL-defense 5-check** — a "failed" verdict cannot be sealed until anchor-reproduction, ≥2 independent
angles, implementation-defect ruled out, catalog cross-check, and verbatim kill-wording are all recorded.
- **Sealed-condition cross-check** — if a pre-registered kill-condition is linked to the arc, closing always
has to answer it, *whatever the close is labelled*. Otherwise closing as `converged` would switch off the
only signal the closing agent did not write. A seal that no longer resolves is refused, not skipped.
- **Verify-gated dev loop** — each automated development round advances one TODO item and may only check it
off after its own machine verification command exits 0, re-run by the harness rather than reported by the
agent. Items without a verify command are refused — including checked ones, so deleting the clause is not
Expand Down
3 changes: 3 additions & 0 deletions README_KO.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ tests/test_gates.sh # 모든 게이트의 단언 기반 스모크 테스트
- **2단계 종결** — 종결은 요약 초안을 만들고, 빈칸이 채워져야만 봉인된다.
- **KILL 방어 5항 체크** — "실패" 판정은 앵커 재현·독립각도 ≥2·구현결함 배제·도감 대조·kill 문언 일치가 전부
기록되기 전엔 봉인되지 않는다.
- **봉인 조건 대조** — 사전등록된 kill 조건이 아크에 연결돼 있으면, 종결이 **어떤 라벨이든** 그 조건에 답해야
한다. 안 그러면 `converged`로 닫는 것만으로 *닫는 사람이 쓰지 않은 유일한 신호*가 꺼진다. 더 이상 읽히지 않는
봉인은 건너뛰지 않고 거부한다.
- **검증 게이트 개발 루프** — 각 자동 개발 라운드는 TODO 한 항목을 진행하고, 그 항목의 기계 검증 명령이 exit 0일
때만 체크할 수 있다. 판정은 에이전트 보고가 아니라 **하네스 재실행**이다. 검증 명령 없는 항목은 거부되며
**체크된 항목도 마찬가지** — 절을 지우고 체크하는 우회가 막힌다. (실패할 수 없는 검증 명령은 여전히 구멍이고,
Expand Down
121 changes: 91 additions & 30 deletions bin/arc-close
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
set -euo pipefail
# arc-close <arc_dir> "<one-line verdict>" [--stop=falsified|no-progress|converged|<other>]
# ★2-phase close: 1st run drafts _SUMMARY only (no seal) → fill blanks → re-run gate-checks & archives.
# Gates: ① "(fill in)" left = refuse ② falsified/KILL → KILL-defense 5-check ("(unfilled)" left = refuse)
# Gates: ① "(fill in)" left = refuse ② falsified/KILL → KILL-defense 5-check ("(unfilled)" left = refuse)
# ③ a linked .prereg seal → 🔒 sealed-condition cross-check fires REGARDLESS of the label (PASS closes too);
# a .prereg whose condition cannot be read is a broken anchor and is refused. Both paths share one bar.
# → guards against premature closure. Then STATE records CLOSED, _SUMMARY is sealed, arc moves to ../_archive/.
# ★stop_reason distinguishes 'falsified' vs 'no-progress' vs 'converged' (generic ⇒ no-progress only).
# ★Optional: seal the close into an append-only ledger (mirror-stack `am`, content-hash = _SUMMARY). Best-effort.
Expand Down Expand Up @@ -65,6 +67,23 @@ if [ -f "$PREREG" ]; then
SEAL_KILL="$(extract_kill "$SEAL_LEDGER" "$SEAL_CLAIM")"
fi

# ── An external anchor must not depend on a label the examinee writes ──────────────────
# The defect: even when a sealed kill-condition existed, nothing was injected or checked
# unless IS_KILL was set. IS_KILL comes from `--stop` and the verdict string — both written
# by the closing agent, at closing time. So the one signal the examinee cannot author (a
# condition sealed in an append-only ledger *before* the compute) was gated behind one they
# could. Closing as a PASS switched the anchor off.
# The fix: SEAL_GATE keys off the sealed condition existing — never off the label.
# KILL close → full 🛡️ 5-check (anti-premature-closure: "did you kill it too early?")
# other close → 🔒 sealed-condition cross-check, one item (the mirror question:
# "did the result actually stay clear of the bar you sealed — or did you move it?")
# Note: triggering on the word "KILL" appearing in the body was considered and rejected — on the
# archive it misfired on 3 of 4 (citations of other arcs' seals, kill-conditions being *designed*)
# while missing the closes that mattered, which never use the word at all.
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"

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 All @@ -84,6 +103,39 @@ emit_kill_defense() { # KILL-defense section: sealed → inject verbatim conditi
echo "- **Catalog cross-check**: (unfilled) ← known measurement-illusion pattern? (catalog id, or 'none')."
}

emit_seal_check() { # non-KILL close with a seal linked — cross-check it regardless of the label
echo ""
echo "## 🔒 Sealed-condition cross-check (label-independent — fires whenever .prereg is linked)"
echo "**Sealed kill-condition** (verbatim from seal \`$SEAL_CLAIM\` — harness-injected, do not edit):"
printf ' > %s\n' "$SEAL_KILL"
echo "- **Result triggers the sealed condition?**: (unfilled) ← if it does, this close should be a KILL. If it does not, give the evidence (number / seal). Do not answer by rewriting the condition (goalpost move)."
}

# Trivial-evasion check. Both paths share ONE bar — kept apart, they drift (proved 2026-08-05:
# the same answer sealed internally and was refused by this file).
check_answers() { # check_answers <section start marker>
while IFS= read -r vline; do
case "$vline" in "- **"*"**:"*) ;; *) continue ;; esac
vlabel="$(printf '%s' "$vline" | sed -E 's/^- \*\*([^*]+)\*\*.*/\1/')"
# answer extraction via python (Unicode-safe — sed's multibyte ← match is locale-fragile)
vans="$(printf '%s' "$vline" | python3 -c 'import sys,re; s=sys.stdin.read(); s=re.sub(r"^- \*\*[^*]+\*\*:\s*","",s); s=re.sub(r"\s*←.*$","",s); sys.stdout.write(s.strip())')"
vlow="$(printf '%s' "$vans" | tr '[:upper:]' '[:lower:]')"
case "$vlow" in ""|y|yes|ok|okay|na|n/a|done|.|-|x|pass|true|good|yep|sure|n)
echo "⛔ seal refused: '$vlabel' answer too trivial: '$vans'"; exit 5 ;;
esac
case "$vlabel" in
*[Cc]atalog*) # a catalog id, or the literal 'none'
case "$vlow" in none) : ;; *) printf '%s' "$vans" | grep -qE '[A-Za-z0-9_]{3,}' \
|| { echo "⛔ seal refused: catalog cross-check needs a catalog id or 'none': '$vans'"; exit 5; } ;; esac ;;
*[Aa]nchor*) # a number, or a seal/reproduction reference
printf '%s' "$vlow" | grep -qE '[0-9]|seal|anchor|reproduc|converg|hash' \
|| { echo "⛔ seal refused: anchor answer needs a number or a seal/reproduction reference: '$vans'"; exit 5; } ;;
*) # otherwise require a little substance
[ "${#vans}" -ge 6 ] || { echo "⛔ seal refused: '$vlabel' answer too short: '$vans'"; exit 5; } ;;
esac
done < <(sed -n "/$1/,\$p" "$SUMMARY")
}

SUMMARY="$ARC_DIR/_SUMMARY_${ARC}.md"

# ── 1st run: draft only, stop (no seal) ──
Expand All @@ -108,11 +160,18 @@ if [ ! -f "$SUMMARY" ]; then

> Discipline: negatives and retractions are recorded indelibly. Numbers only after checking against evidence.
MDEOF
[ "$IS_KILL" = "yes" ] && emit_kill_defense >> "$SUMMARY"
if [ "$IS_KILL" = "yes" ]; then emit_kill_defense >> "$SUMMARY"
elif [ "$SEAL_GATE" = "yes" ]; then emit_seal_check >> "$SUMMARY"; fi
echo "📝 _SUMMARY drafted (not sealed): $SUMMARY"
[ "$IS_KILL" = yes ] && [ -n "$SEAL_KILL" ] && echo " 🔒 sealed kill-condition injected from seal $SEAL_CLAIM (not agent-authored)."
[ "$IS_KILL" = yes ] && [ -z "$SEAL_KILL" ] && echo " ⚠️ UNSEALED — no prereg linked (arc-prereg); kill-wording is attestation-only."
echo " Fill the blanks (fill in$([ "$IS_KILL" = yes ] && echo ' / unfilled')), then re-run the same command to gate-check and archive."
if [ "$SEAL_GATE" = "yes" ]; then
echo " 🔒 sealed kill-condition injected from seal $SEAL_CLAIM (not agent-authored)."
[ "$IS_KILL" = "yes" ] || echo " note: not a KILL close, but a seal is linked — the 🔒 cross-check fires regardless of the label."
elif [ "$SEAL_BROKEN" = "yes" ]; then
echo " 🔴 .prereg is linked but its kill_condition could not be read — anchor broken (claim=$SEAL_CLAIM, ledger=$SEAL_LEDGER)."
elif [ "$IS_KILL" = "yes" ]; then
echo " ⚠️ UNSEALED — no prereg linked (arc-prereg); kill-wording is attestation-only."
fi
echo " Fill the blanks (fill in$([ "$IS_KILL" = yes ] || [ "$SEAL_GATE" = yes ] && echo ' / unfilled')), then re-run the same command to gate-check and archive."
exit 0
fi

Expand All @@ -122,43 +181,45 @@ if grep -q '(fill in)' "$SUMMARY"; then
grep -n 'fill in' "$SUMMARY" | sed 's/^/ /'
exit 4
fi
# A broken anchor is a silent way around the gate — refuse rather than pass quietly.
if [ "$SEAL_BROKEN" = "yes" ]; then
echo "⛔ seal refused: .prereg is linked but its sealed kill-condition could not be read — anchor broken."
echo " claim=$SEAL_CLAIM · ledger=$SEAL_LEDGER"
echo " Re-link with arc-prereg, or remove .prereg deliberately if there really is no seal (both are auditable acts)."
exit 5
fi

# ① the harness owns the sealed-condition line — it must remain verbatim (no silent widening). Both paths.
if [ "$SEAL_GATE" = "yes" ] && ! grep -qxF " > $SEAL_KILL" "$SUMMARY"; then
echo "⛔ seal refused: the harness-injected sealed kill-condition line was edited or removed — it must stay verbatim from seal \`$SEAL_CLAIM\`."
exit 5
fi

if [ "$IS_KILL" = "yes" ]; then
if ! grep -q 'KILL-defense check' "$SUMMARY"; then
emit_kill_defense >> "$SUMMARY"
echo "⛔ seal refused: KILL-type close without a 🛡️ KILL-defense check — section added. Fill it and re-run."
exit 5
fi
# ① the harness owns the sealed-condition line — it must remain the verbatim seal (no silent widening)
if [ -n "$SEAL_KILL" ] && ! grep -qxF " > $SEAL_KILL" "$SUMMARY"; then
echo "⛔ seal refused: the harness-injected sealed kill-condition line was edited — it must stay verbatim from seal \`$SEAL_CLAIM\`."
if grep -q '(unfilled)' "$SUMMARY"; then
echo "⛔ seal refused: 🛡️ KILL-defense items remain unfilled — anti-premature-closure gate."
grep -n 'unfilled' "$SUMMARY" | sed 's/^/ /'
exit 5
fi
check_answers 'KILL-defense check'
elif [ "$SEAL_GATE" = "yes" ]; then
# non-KILL close with a seal linked — the cross-check fires independently of the label
if ! grep -q 'Sealed-condition cross-check' "$SUMMARY"; then
emit_seal_check >> "$SUMMARY"
echo "⛔ seal refused: a seal (.prereg) is linked but the 🔒 sealed-condition cross-check is missing — section added. Fill it and re-run."
exit 5
fi
if grep -q '(unfilled)' "$SUMMARY"; then
echo "⛔ seal refused: 🛡️ KILL-defense items remain unfilled — anti-premature-closure gate."
echo "⛔ seal refused: 🔒 sealed-condition cross-check unfilled — state whether the result met the bar you sealed."
grep -n 'unfilled' "$SUMMARY" | sed 's/^/ /'
exit 5
fi
# minimal substance check — can't judge truth, but reject self-evident evasions (a bare "yes", ".", etc.)
while IFS= read -r vline; do
case "$vline" in "- **"*"**:"*) ;; *) continue ;; esac
vlabel="$(printf '%s' "$vline" | sed -E 's/^- \*\*([^*]+)\*\*.*/\1/')"
# answer extraction via python (Unicode-safe — sed's multibyte ← match is locale-fragile)
vans="$(printf '%s' "$vline" | python3 -c 'import sys,re; s=sys.stdin.read(); s=re.sub(r"^- \*\*[^*]+\*\*:\s*","",s); s=re.sub(r"\s*←.*$","",s); sys.stdout.write(s.strip())')"
vlow="$(printf '%s' "$vans" | tr '[:upper:]' '[:lower:]')"
case "$vlow" in ""|y|yes|ok|okay|na|n/a|done|.|-|x|pass|true|good|yep|sure|n)
echo "⛔ seal refused: KILL-defense '$vlabel' answer too trivial: '$vans'"; exit 5 ;;
esac
case "$vlabel" in
*[Cc]atalog*) # a catalog id, or the literal 'none'
case "$vlow" in none) : ;; *) printf '%s' "$vans" | grep -qE '[A-Za-z0-9_]{3,}' \
|| { echo "⛔ seal refused: catalog cross-check needs a catalog id or 'none': '$vans'"; exit 5; } ;; esac ;;
*[Aa]nchor*) # a number, or a seal/reproduction reference
printf '%s' "$vlow" | grep -qE '[0-9]|seal|anchor|reproduc|converg|hash' \
|| { echo "⛔ seal refused: anchor answer needs a number or a seal/reproduction reference: '$vans'"; exit 5; } ;;
*) # otherwise require a little substance
[ "${#vans}" -ge 6 ] || { echo "⛔ seal refused: KILL-defense '$vlabel' answer too short: '$vans'"; exit 5; } ;;
esac
done < <(sed -n '/KILL-defense check/,$p' "$SUMMARY")
check_answers 'Sealed-condition cross-check'
fi

echo "ARC_CLOSED ${TODAY} stop=${STOP} — ${VERDICT}" >> "$ARC_DIR/STATE.md"
Expand Down
23 changes: 18 additions & 5 deletions docs/METHODOLOGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,28 @@ Each round, the relay summarizes and judges. **Every round summary must include
call, not the guard's job. **More rounds do not improve quality** — the guards exist because debate drifts.

Close with `bin/arc-close` (2-phase): it drafts a summary; you fill the blanks; re-running seals only when
they're filled. A **falsified/KILL** close additionally requires the **🛡️ KILL-defense 5-check**:
they're filled. A **falsified/KILL** close (and only that close) additionally requires the **🛡️ KILL-defense 5-check**:
anchor (positive control) reproduced · ≥2 independent angles converged · implementation defect ruled out ·
catalog cross-check · verbatim kill-wording. This is the anti-premature-closure gate.

**Sealed kill-condition injection.** If you sealed the kill-condition (mirror-stack `mm_preregister`) and
linked it to the arc (`bin/arc-prereg <arc_dir> <claim_id> [ledger]`), the KILL-defense check no longer asks
the agent to *type* whether the verdict matches the pre-registration. Instead the harness reads the sealed
kill-condition from the ledger and **injects it verbatim** into the record; on close it refuses if that line
was edited. The agent is not the author of the condition, so it cannot be silently widened at close.
linked it to the arc (`bin/arc-prereg <arc_dir> <claim_id> [ledger]`), the check no longer asks the agent to
*type* whether the verdict matches the pre-registration. Instead the harness reads the sealed kill-condition
from the ledger and **injects it verbatim** into the record; on close it refuses if that line was edited or
removed. The agent is not the author of the condition, so it cannot be silently widened at close.

This injection is **independent of how the close is labelled**. That matters more than it sounds: the label
(`--stop`, the verdict string) is written by the closing agent at closing time, so keying the anchor to it
would let a `converged` close switch off the one signal the agent cannot author. So a linked seal always
produces a check — the 🛡️ 5-check on a KILL close, and on any other close a one-item **🔒 sealed-condition
cross-check**: *did the result stay clear of the bar you sealed, or did you move the bar?* A `.prereg` whose
condition can no longer be read is treated as a broken anchor and refused rather than passed in silence.

We considered instead triggering on the word "KILL" appearing anywhere in the summary. Measured against our
own archive it was wrong in both directions — it fired on citations of other arcs' seals and on kill-conditions
being *designed* for the next stage (3 of 4 hits), while the closes that actually buried an original claim
never used the word at all. It would also have taxed exactly the honest reporting we want. A trigger the
examinee writes is not a trigger; anchoring on the seal is the only signal here that predates the close.
Scope, honestly: this fixes the *condition* by reference — whether the result actually triggers it remains a
judgment the agent asserts, and this is **not** "machine-verified honesty", only a removed goalpost. Without a
linked seal the field stays attestation-only and the close is stamped `⚠️ UNSEALED` — the missing seal is itself
Expand Down
9 changes: 6 additions & 3 deletions mcp/yeoul_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,12 @@ def loop_guard_init(arc_dir: str, max_rounds: int = 3, token_budget: int = 20000

@mcp.tool()
def arc_close(arc_dir: str, verdict: str, stop: str = "converged") -> dict:
"""Close an arc (2-phase, GATE-ENFORCED). 1st call drafts _SUMMARY; fill blanks (+ KILL-defense 5-check
if stop=falsified) then call again to seal. Returns the script's refusal (exit 4 blanks / exit 5 KILL-defense)
if not ready — that refusal is authoritative, do not override it."""
"""Close an arc (2-phase, GATE-ENFORCED). 1st call drafts _SUMMARY; fill the blanks, then call again to seal.
Extra sections are required depending on the close: a KILL close (stop=falsified, or KILL in the verdict) gets
the 🛡️ 5-check; ANY close on an arc with a linked prereg seal gets the 🔒 sealed-condition cross-check —
that one fires regardless of the label, so closing as `converged` does not switch the anchor off.
Returns the script's refusal (exit 4 blanks / exit 5 gate) if not ready — that refusal is authoritative,
do not override it."""
return _run("arc-close", arc_dir, verdict, f"--stop={stop}")


Expand Down
Loading
Loading