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
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ jobs:
- name: Lint — every bash script parses
run: |
for f in bin/* setup/*.sh examples/*.sh tests/*.sh; do
[ -f "$f" ] || continue
case "$f" in *.py) continue ;; esac
bash -n "$f"
done
- name: Gate smoke tests
run: bash tests/test_gates.sh
- name: Adversarial integrity and bounded-loop contracts
run: python tests/test_hardening.py
- name: Worked example
run: bash examples/demo.sh
- name: Pre-publish gate (leaks / over-claim / worked example)
Expand Down Expand Up @@ -75,3 +78,36 @@ jobs:
# The import check above proves the module loads; it never reads the handshake. This
# step launches the server and reads what it actually says on the wire.
run: python mcp/tests/test_serverinfo_version.py
- name: Install compatible Mirror contract for integration tests
run: pip install "git+https://github.com/mirror-stack/mirror-stack-mcp@v0.2.14"
- name: CLI/MCP parity and real Mirror result contract
run: python mcp/tests/test_surface_contract.py
- name: Installed harness works away from the checkout
shell: bash
run: |
cd "$RUNNER_TEMP"
python - <<'PY'
import tempfile
from pathlib import Path
from yeoul_mcp import server
assert '_harness' in str(server.BIN), server.BIN
with tempfile.TemporaryDirectory() as tmp:
result = server.yeoul_new('installed-smoke', workspace=tmp, no_arc=True)
assert result['exit_code'] == 0, result
spec = Path(tmp)/'projects'/'installed-smoke'/'design'/'spec.md'
assert '**Goal**' in spec.read_text(encoding='utf-8')
PY
- name: Source archive builds a self-contained wheel too
shell: bash
run: |
python -m pip install build
python -m build ./mcp --sdist --outdir "$RUNNER_TEMP/yeoul-sdist"
python -m pip wheel "$RUNNER_TEMP"/yeoul-sdist/*.tar.gz --no-deps --wheel-dir "$RUNNER_TEMP/yeoul-wheel"
python - <<'PY'
import os, zipfile
from pathlib import Path
wheel = next((Path(os.environ['RUNNER_TEMP'])/'yeoul-wheel').glob('*.whl'))
with zipfile.ZipFile(wheel) as archive:
for path in ('bin/arc-close', 'bin/prereg_check.py', 'bin/ralph_core.py', 'templates/spec.md', 'LICENSE'):
assert 'yeoul_mcp/_harness/'+path in archive.namelist(), path
PY
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ projects/
graduated/
KNOWLEDGE_INDEX.md
.venv/
build/
dist/
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@

All notable changes to this project are documented here.

## [0.3.0] — 2026-09-09

### Integrity contracts and compatibility changes

- Verify every ledger hash/link and pin the first registration; reject unsealed,
tampered, malformed, duplicate-key, removed and changed bindings. Legacy links
require an explicit verified upgrade; existing archives/ledgers are untouched.
- Freeze original TODO criteria, reject command replacement/item deletion before
executing verification, and share CLI/MCP eligibility. Standalone verification
now requires a supervisor baseline; `--current-only` is a labelled manual diagnostic.
- Keep closure drafts pending until archival; reject deleted defense fields,
mismatched verdict metadata, concurrent closes and archive collisions. Record
archive-recording state separately, and show active arcs before historical verdicts.
- Bound Ralph rounds without an extra invocation; add timeouts and stop on unknown
token usage. Budgets remain between-round controls, not hard provider caps.
- Add an explicit `arc-result` bridge to Mirror's bound result contract. It records
the supplied status/evidence and reports a publication gate, but never publishes.
- Bundle harness/templates in wheels and sdists, isolate tests from production
ledgers/indexes, and test installation away from the checkout.
- Prefer Git Bash on Windows over the System32 WSL launcher. Keep per-run logs
without overwriting older evidence; require defense answers inside their checked section.
- Refresh active docs, command references, installation failure handling and migration
guidance. See [INTEGRITY.md](docs/INTEGRITY.md) for limitations and upgrade steps.

## [0.2.0] — 2026-08-28

**The first tagged release.** `0.1.0` was never cut: the section below has sat marked *unreleased*
Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
**Yeoul is a file-based harness for running deliberation → pre-registration → autonomous work loops
with integrity gates that resist self-deception and premature closure.** It is the *practice layer*
on top of a measurement-discipline primitive (pre-registration + a tamper-evident ledger): the
primitive answers *"is this claim honest?"*; Yeoul answers *"how do I run a disciplined idea loop end to end?"*
primitive checks recorded evidence and integrity; Yeoul answers *"how do I run a disciplined idea loop end to end?"*

> ⚠️ **Work in progress — not yet released.** This README is a positioning draft, not a launch page.
> **v0.3.0: integrity-contract hardening.** See the [changelog](CHANGELOG.md) and
> [trust boundaries, installation and migration guide](docs/INTEGRITY.md).

## Install

Expand All @@ -33,7 +34,7 @@ Register both MCP servers with your client (Claude Desktop/Code — merge `setup
} }
```

mirror-stack is optional — without it, sealing degrades to a no-op and everything else still runs
mirror-stack is optional — without a recorder, discussion closes are explicitly file-only
(`setup/install.sh --no-mirror-stack`).

## Quickstart
Expand Down Expand Up @@ -70,7 +71,8 @@ New to the method? Read [`docs/METHODOLOGY.md`](docs/METHODOLOGY.md), then paste
- **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
a way out. (A verify command that cannot fail is still a hole: that part is on the author.)
a way out. Original TODO criteria are pinned before work; changing commands or deleting items is refused.
(Test adequacy and protection of the test implementations remain the supervisor's responsibility.)

## Layers

Expand All @@ -79,11 +81,12 @@ New to the method? Read [`docs/METHODOLOGY.md`](docs/METHODOLOGY.md), then paste
| Primitive | pre-registration + tamper-evident ledger | *(optional dependency — the mirror-stack)* |
| Practice | deliberation arcs, gates, dev handoff | **this repo** |

The mirror-stack dependency is optional: without it, sealing degrades to a no-op and everything else runs.
Mirror is optional for discussion. Linked claims must still pass offline hash verification.
Use `arc-result` only for an explicit, supervisor-judged measurement result; discussion closure is not publication.

## Status / honesty notes

- The autonomous development loop ("Ralph"-style) is the **least-validated, most-commodity** part; its safety
comes from the machine-verification gate, not the loop itself. Do not treat it as a proven technique.
- No claim here is that Yeoul improves research *outcomes*. The claim is narrower: it makes honest
discipline the **default and hard-to-skip**, which is exactly what current autonomous-research systems lack.
discipline explicit and mechanically checked in the supported paths. It is not an OS security boundary.
18 changes: 12 additions & 6 deletions README_KO.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@

**Yeoul은 심의 → 사전등록 → 자율 작업 루프를 자기기만과 성급한 종결에 저항하는 무결성 게이트와 함께
돌리는 파일 기반 하네스입니다.** 측정 규율 프리미티브(사전등록 + 변조 감지 원장) 위에 얹힌 *실천 계층*이에요 —
프리미티브는 *"이 주장이 정직한가?"*에 답하고, Yeoul은 *"규율 있는 아이디어 루프를 처음부터 끝까지 어떻게 돌리나?"*에 답합니다.
프리미티브는 기록된 증거의 무결성을 확인하고, Yeoul은 *"규율 있는 아이디어 루프를 처음부터 끝까지 어떻게 돌리나?"*에 답합니다.

> ⚠️ **진행 중 — 아직 정식 릴리스 아님.** 이 문서는 포지셔닝 초안이지 출시 페이지가 아닙니다.
> **v0.3.0: 무결성 계약 보완.** [변경 기록](CHANGELOG.md)과
> [검증 범위·설치·기존 아크 마이그레이션 안내](docs/INTEGRITY.md)를 확인하세요.

## 설치

Expand All @@ -32,7 +33,7 @@ setup/install.sh # mirror-stack(봉인 프리미티브) + yeoul-
} }
```

mirror-stack은 선택입니다 — 없으면 봉인이 no-op으로 완화되고 나머지는 그대로 돕니다
mirror-stack은 선택입니다 — 기록기가 없으면 토론 종결은 파일 전용으로 명시됩니다
(`setup/install.sh --no-mirror-stack`).

## 빠른 시작
Expand Down Expand Up @@ -77,11 +78,16 @@ tests/test_gates.sh # 모든 게이트의 단언 기반 스모크 테스트
| 프리미티브 | 사전등록 + 변조 감지 원장 | *(선택 의존성 — mirror-stack)* |
| 실천 | 심의 아크·게이트·개발 핸드오프 | **이 레포** |

mirror-stack 의존성은 선택입니다: 없으면 봉인이 no-op으로 완화되고 나머지는 그대로 돕니다.
토론에는 mirror-stack이 필수가 아닙니다. 다만 봉인을 연결하면 원장 전체 해시·체인과 최초 봉인을 검증합니다.
토론 종결은 측정 결과나 출판 승인이 아닙니다. 측정 결과는 감독자가 `arc-result`에 상태와 증거를 명시해 넘깁니다.

개발 검증은 작업 전에 `verify-baseline`으로 기준을 고정한 뒤 `verify-gate`로 확인합니다.
Ralph는 최초 TODO를 부모 프로세스 메모리에 보관해 명령 변경·항목 삭제를 거부합니다.
기존 2줄 `.prereg`는 자동 인증하지 않으며, 같은 원장과 주장으로 명시적인 재연결이 필요합니다.

## 상태 / 정직 노트

- 자율 개발 루프("Ralph" 방식)는 **가장 덜 검증됐고 가장 commodity한** 부분입니다. 그 안전성은 루프 자체가 아니라
기계 검증 게이트에서 나와요. 검증된 기법으로 과신하지 마세요.
- Yeoul이 연구 *결과*를 개선한다는 주장은 하지 않습니다. 주장은 더 좁아요: 정직한 규율을 **기본값이자 건너뛰기
어렵게** 만든다는 것 — 현재 자율 연구 시스템에 정확히 빠져 있는 바로 그것.
- Yeoul이 연구 *결과*를 개선한다는 주장은 하지 않습니다. 지원하는 실행 경로에서 규율을 명시하고
기계적으로 검사한다는 뜻이며, 운영체제 수준의 보안 경계나 진실성 증명은 아닙니다.
56 changes: 29 additions & 27 deletions bin/arc-close
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ fi
[ -d "$ARC_DIR" ] || { echo "arc dir not found: $ARC_DIR"; exit 1; }

ARC_DIR="$(cd "$ARC_DIR" && pwd)"
LOCK_PATH="$ARC_DIR/.close.lock"
mkdir "$LOCK_PATH" 2>/dev/null || { echo 'close already in progress (or stale .close.lock; inspect before retry)'; exit 9; }
trap 'rmdir "$LOCK_PATH" 2>/dev/null || true' EXIT
ARC="$(basename "$ARC_DIR")"
ARCS_DIR="$(cd "$ARC_DIR/.." && pwd)"
ARCHIVE_DIR="$ARCS_DIR/_archive"
Expand All @@ -36,9 +39,8 @@ sed_inplace() { # sed_inplace <sed-expr> <file>
sed "$1" "$2" > "$2.tmp" && mv "$2.tmp" "$2"
}

# thread status In-Progress → Closed
# Do not mark a draft or refusal Closed. Commit status only after archiving.
ARC_FILE="$ARC_DIR/ARC/${ARC}.md"
[ -f "$ARC_FILE" ] && sed_inplace 's/^status: "In-Progress"/status: "Closed"/' "$ARC_FILE"

# is this a KILL-type close? (stop=falsified or verdict mentions KILL)
IS_KILL="no"
Expand All @@ -50,26 +52,15 @@ case "$VERDICT" in *KILL*|*kill*) IS_KILL="yes" ;; esac
# the verbatim sealed kill-condition into the record instead of trusting an agent-typed field. The agent
# is not the author of the condition, so it cannot be silently widened at close (post-hoc goalpost move).
# NOTE: this fixes the *condition* by reference; whether the result triggers it remains a judgment.
extract_kill() { # extract_kill <ledger> <claim_id> → verbatim kill_condition (whitespace-collapsed), else empty
[ -f "$1" ] || return 0
$PY - "$1" "$2" 2>/dev/null <<'PY'
import json, sys
led, cid = sys.argv[1], sys.argv[2]
for line in open(led, encoding='utf-8', errors='ignore'):
line = line.strip()
if not line: continue
try: d = json.loads(line)
except Exception: continue
if d.get('claim_id') == cid and d.get('kill_condition'):
print(' '.join(str(d['kill_condition']).split())); break
PY
}
PREREG="$ARC_DIR/.prereg"
SEAL_CLAIM=""; SEAL_LEDGER=""; SEAL_KILL=""
if [ -f "$PREREG" ]; then
SEAL_CLAIM="$(sed -n '1p' "$PREREG")"; SEAL_LEDGER="$(sed -n '2p' "$PREREG")"
SEAL_KILL="$(extract_kill "$SEAL_LEDGER" "$SEAL_CLAIM")"
fi
CHECKER="$(dirname "${BASH_SOURCE[0]}")/prereg_check.py"
SEAL_BROKEN="no"
if RESOLVED="$($PY "$CHECKER" resolve "$ARC_DIR")"; then
SEAL_CLAIM="$(printf '%s' "$RESOLVED" | sed -n '1p')"
SEAL_LEDGER="$(printf '%s' "$RESOLVED" | sed -n '2p')"
SEAL_KILL="$(printf '%s' "$RESOLVED" | sed -n '3p')"
else SEAL_BROKEN="yes"; 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
Expand All @@ -86,7 +77,6 @@ fi
# 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"

# ★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
Expand All @@ -98,7 +88,7 @@ SEAL_BROKEN="no"; [ -f "$PREREG" ] && [ -z "$SEAL_KILL" ] && SEAL_BROKEN="yes"
# 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"
printf -- '- **Second key (pre-registration)**: sealed — claim `%s` in %s (hash-verified, first seal pinned; identity/time/truth unverified)\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
Expand Down Expand Up @@ -145,6 +135,7 @@ SUBSTANCE="$(dirname "${BASH_SOURCE[0]}")/substance_check.py"
TAB="$(printf '\t')" # NOTE: ${v%%\t*} is a literal `t` in shell patterns — this must be a real tab

check_answers() { # check_answers <section start marker>
"$PY" "$(dirname "${BASH_SOURCE[0]}")/closure_check.py" "$SUMMARY" "$IS_KILL" "$SEAL_GATE" || exit 5
# ★ The positive control runs at the HEAD OF THE GATE'S EXECUTION PATH, not in a test file.
# If the checker cannot separate planted violations from planted genuine answers, we do NOT
# interpret the real answers — we die. An instrument is not trusted because it is green;
Expand Down Expand Up @@ -185,9 +176,9 @@ SUMMARY="$ARC_DIR/_SUMMARY_${ARC}.md"
# ── 1st run: draft only, stop (no seal) ──
if [ ! -f "$SUMMARY" ]; then
cat > "$SUMMARY" << MDEOF
# 🏛️ Arc closed — ${ARC}
# 🏛️ Arc close draft — ${ARC}

- **Closed**: ${TODAY} (${TS_ISO})
- **Drafted**: ${TODAY} (${TS_ISO})
- **stop_reason**: ${STOP} (falsified / no-progress / converged)
- **Verdict**: ${VERDICT}
$(seal_state_line)
Expand All @@ -206,6 +197,7 @@ $(seal_state_line)
MDEOF
if [ "$IS_KILL" = "yes" ]; then emit_kill_defense >> "$SUMMARY"
elif [ "$SEAL_GATE" = "yes" ]; then emit_seal_check >> "$SUMMARY"; fi
[ ! -f "$ARC_FILE" ] || sed_inplace 's/^status: .*/status: "Close-Pending"/' "$ARC_FILE"
echo "📝 _SUMMARY drafted (not sealed): $SUMMARY"
if [ "$SEAL_GATE" = "yes" ]; then
echo " 🔒 sealed kill-condition injected from seal $SEAL_CLAIM (not agent-authored)."
Expand All @@ -229,7 +221,7 @@ fi
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)."
echo " Restore the bound files. For a legacy link, explicitly re-link the same verified claim with arc-prereg."
exit 5
fi

Expand Down Expand Up @@ -275,6 +267,7 @@ fi
# 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".
"$PY" "$(dirname "${BASH_SOURCE[0]}")/closure_check.py" "$SUMMARY" --record "$VERDICT" "$STOP" || exit 8
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)')"
Expand All @@ -283,11 +276,16 @@ if ! grep -qxF -- "$(seal_state_line)" "$SUMMARY"; then
exit 8
fi

echo "ARC_CLOSED ${TODAY} stop=${STOP} — ${VERDICT}" >> "$ARC_DIR/STATE.md"

mkdir -p "$ARCHIVE_DIR"
[ ! -e "$ARCHIVE_DIR/$ARC" ] || { echo "archive destination already exists — refusing overwrite"; exit 9; }
mv "$ARC_DIR" "$ARCHIVE_DIR/"
LOCK_PATH="$ARCHIVE_DIR/$ARC/.close.lock"
SEALED="$ARCHIVE_DIR/$ARC/_SUMMARY_${ARC}.md"
ARC_FILE="$ARCHIVE_DIR/$ARC/ARC/${ARC}.md"
[ ! -f "$ARC_FILE" ] || sed_inplace 's/^status: .*/status: "Closed"/' "$ARC_FILE"
sed_inplace 's/Arc close draft/Arc closed/' "$SEALED"
printf '\n- **Closed**: %s (%s)\n' "$TODAY" "$TS_ISO" >> "$SEALED"
echo "ARC_CLOSED ${TODAY} stop=${STOP} — ${VERDICT}" >> "$ARCHIVE_DIR/$ARC/STATE.md"

# ── optional: seal into append-only ledger (best-effort; skips if `am` absent) ──
# 🔴 say what was actually tested. The condition below is "is the `am` CLI on PATH",
Expand All @@ -297,15 +295,19 @@ SEALED="$ARCHIVE_DIR/$ARC/_SUMMARY_${ARC}.md"
# NB: single-quoted on purpose — backticks inside double quotes are command substitution,
# which ran `am` and leaked its exit status into the close (caught by tests/test_gates.sh).
SEAL_MSG='not sealed — file only (`am` CLI not on PATH; mirror-stack may still be installed)'
RECORD_STATE="file-only"
if command -v am >/dev/null 2>&1; then
if am record --agent yeoul --action arc-close --target "$ARC" \
--payload "{\"stop_reason\":\"${STOP}\"}" \
--content-file "$SEALED" >/dev/null 2>&1; then
SEAL_MSG="✅ sealed into ledger (content-hash = _SUMMARY → tamper-evident)"
RECORD_STATE="recorded"
else
SEAL_MSG="⚠️ am record failed (skipped)"
RECORD_STATE="record-failed"
fi
fi
printf 'ARCHIVE_RECORD %s\n' "$RECORD_STATE" >> "$ARCHIVE_DIR/$ARC/STATE.md"

# ── auto-growing knowledge index (hands-off; appends on close) ──
[ -x "$(dirname "${BASH_SOURCE[0]}")/index-append" ] && \
Expand Down
2 changes: 1 addition & 1 deletion bin/arc-list
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ for root in $ROOTS; do
if [ "$archived" = "1" ]; then status="Archived"; fi
title="$(field title "$arcfile")"
roles="$(field roles "$arcfile")"
if [ "$OPEN_ONLY" = "1" ] && [ "$status" != "In-Progress" ]; then continue; fi
if [ "$OPEN_ONLY" = "1" ] && [ "$status" != "In-Progress" ] && [ "$status" != "Close-Pending" ]; then continue; fi
printf '%s\t%s\t%s\t%s\n' "${status:-?}" "$arcdir" "${title:-?}" "${roles:-?}"
done < <(find "$root" -type f -path '*/ARC/*.md' 2>/dev/null)
done
Loading
Loading