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
18 changes: 14 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,24 @@ jobs:
tools = [n for n in dir(server) if n in (
'yeoul_new','arc_open','arc_ticket','loop_guard_init','loop_guard_tick',
'arc_close','build_handoff','ralph_gate_check','arc_prereg','verify_gate',
'status','arc_list')]
assert len(tools) == 12, tools
'status','arc_list','workspace_prepare','workspace_execute','workspace_tasks')]
assert len(tools) == 15, tools
# ASCII only: the em-dash renders as "?" on the Windows console, which reads like
# a mojibake defect in the log when nothing is actually wrong.
print("yeoul-mcp OK - 12 tools")
print("yeoul-mcp OK - 15 tools")
PY
- name: _run subprocess contract (stdin never inherited, UTF-8 pinned)
run: python mcp/tests/test_run_contract.py
- name: serverInfo contract (announces its own version, not the SDK's)
# 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
- name: Managed runtime permissions, receipts, process locks and real stdio
run: python mcp/tests/test_runtime_contract.py
- name: Independent product CLI, approvals, recovery and stdio reconnection
# Standalone suite runs FIRST: Mirror must not be required to use Yeoul.
run: python mcp/tests/test_product.py
- name: Install optional 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
Expand All @@ -97,6 +102,11 @@ jobs:
spec = Path(tmp)/'projects'/'installed-smoke'/'design'/'spec.md'
assert '**Goal**' in spec.read_text(encoding='utf-8')
PY
- name: Installed product CLI and MCP outside the checkout
shell: bash
run: |
cd "$RUNNER_TEMP"
PRODUCT_TEST_INSTALLED=1 python "$GITHUB_WORKSPACE/mcp/tests/test_product.py"
- name: Source archive builds a self-contained wheel too
shell: bash
run: |
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

All notable changes to this project are documented here.

## [0.4.0] — 2026-09-10

- Independent `yeoul` setup/new/status/doctor/connect/serve CLI with observe,
discuss and develop modes; Mirror installation is now explicit opt-in.
- CLI/MCP task preparation, durable replay, workspace permissions and cooperative
cross-process locks. Three new workspace MCP tools; twelve business tools retained.
- Operator-only audited recovery, retired interrupted IDs, reviewed and hash-pinned
verification baselines, config history and explicit external read-ledger grants.
- Managed closures do not invoke an external action recorder. Separate product roots
are supported; no LaneStack dependency or cross-product transaction is implied.
- Atomic arc directory allocation, no silent loop-state reset, and conservative
interrupted verification handling. Wheels/sdists bundle the changed harness.
- See [workspace guide](docs/WORKSPACE_GUIDE.ko.md) and [runtime contract](docs/RUNTIME_CONTRACT.md).

## [0.3.0] — 2026-09-09

### Integrity contracts and compatibility changes
Expand Down
37 changes: 20 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,32 @@ with integrity gates that resist self-deception and premature closure.** It is t
on top of a measurement-discipline primitive (pre-registration + a tamper-evident ledger): the
primitive checks recorded evidence and integrity; Yeoul answers *"how do I run a disciplined idea loop end to end?"*

> **v0.3.0: integrity-contract hardening.** See the [changelog](CHANGELOG.md) and
> [trust boundaries, installation and migration guide](docs/INTEGRITY.md).
> **v0.4.0: standalone workspace product.** Yeoul is independent of Mirror and LaneStack.
> See the [workspace guide (Korean)](docs/WORKSPACE_GUIDE.ko.md),
> [changelog](CHANGELOG.md) and [integrity boundaries](docs/INTEGRITY.md).

For stdio MCP permissions, durable retries and cross-process locking, see the
[runtime contract](docs/RUNTIME_CONTRACT.md). Managed mode is opt-in with
`YEOUL_MCP_ROOT`; without it, the server is a trusted local runner without managed
permission enforcement or cooperative concurrency controls.

## Install

```bash
git clone https://github.com/mirror-stack/yeoul
cd yeoul
export PATH="$PWD/bin:$PATH" # the CLI: yeoul-new, arc-open, arc-close, ralph, status, …
setup/install.sh # installs mirror-stack (sealing primitive) + yeoul-mcp, prints MCP config
```

Register both MCP servers with your client (Claude Desktop/Code — merge `setup/mcp-servers.json`):

```json
{ "mcpServers": {
"mirror-stack": { "command": "mirror-stack-mcp" },
"yeoul": { "command": "yeoul-mcp" }
} }
pip install 'git+https://github.com/mirror-stack/yeoul@v0.4.0#subdirectory=mcp'
yeoul setup ./my-discussions --mode discuss
yeoul new example --workspace ./my-discussions
yeoul doctor --workspace ./my-discussions
yeoul connect --workspace ./my-discussions
```

mirror-stack is optional — without a recorder, discussion closes are explicitly file-only
(`setup/install.sh --no-mirror-stack`).
Add the configuration printed by `yeoul connect` to your MCP client. No existing
client config is modified. Python 3.10+ and Bash (Git Bash on Windows) are required.
The checkout installer `setup/install.sh` also installs Yeoul alone;
`--with-mirror-stack` explicitly adds the independent Mirror product.
Linked ledgers may live in separate roots. Managed closures remain file-only and
do not invoke an external recorder. Raw checkout commands remain advanced,
trusted-local interfaces outside the managed product CLI/MCP lock.

## Quickstart

Expand Down
32 changes: 15 additions & 17 deletions README_KO.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,27 @@
돌리는 파일 기반 하네스입니다.** 측정 규율 프리미티브(사전등록 + 변조 감지 원장) 위에 얹힌 *실천 계층*이에요 —
프리미티브는 기록된 증거의 무결성을 확인하고, Yeoul은 *"규율 있는 아이디어 루프를 처음부터 끝까지 어떻게 돌리나?"*에 답합니다.

> **v0.3.0: 무결성 계약 보완.** [변경 기록](CHANGELOG.md)과
> [검증 범위·설치·기존 아크 마이그레이션 안내](docs/INTEGRITY.md)를 확인하세요.
> **v0.4.0: 독립 제품의 간편 설정·실행·복구.** 거울과 레인스택 없이 사용할 수 있습니다.
> [간편 사용 안내](docs/WORKSPACE_GUIDE.ko.md), [변경 기록](CHANGELOG.md),
> [검증 범위·기존 아크 안내](docs/INTEGRITY.md)를 확인하세요.

## 설치

```bash
git clone https://github.com/mirror-stack/yeoul
cd yeoul
export PATH="$PWD/bin:$PATH" # CLI: yeoul-new, arc-open, arc-close, ralph, status, …
setup/install.sh # mirror-stack(봉인 프리미티브) + yeoul-mcp 설치 + MCP 설정 출력
pip install 'git+https://github.com/mirror-stack/yeoul@v0.4.0#subdirectory=mcp'
yeoul setup ./my-discussions --mode discuss
yeoul new example --workspace ./my-discussions
yeoul doctor --workspace ./my-discussions
yeoul connect --workspace ./my-discussions
```

두 MCP 서버를 클라이언트에 등록(Claude Desktop/Code — `setup/mcp-servers.json` 병합):

```json
{ "mcpServers": {
"mirror-stack": { "command": "mirror-stack-mcp" },
"yeoul": { "command": "yeoul-mcp" }
} }
```

mirror-stack은 선택입니다 — 기록기가 없으면 토론 종결은 파일 전용으로 명시됩니다
(`setup/install.sh --no-mirror-stack`).
`yeoul connect`가 출력한 서버 항목을 사용 중인 MCP 클라이언트에 추가하세요.
기존 설정은 자동 수정하지 않습니다. Python 3.10+와 Bash(Windows: Git Bash)가 필요합니다.
소스 설치의 `setup/install.sh`도 여울만 설치하며,
`--with-mirror-stack`을 선택해야 거울을 함께 설치합니다.
원장은 별도 폴더에 두고 파일별 읽기 권한으로 연결할 수 있습니다.
관리 모드 종결은 외부 기록기를 실행하지 않습니다. 기존 원시 CLI·스크립트는
고급 로컬 인터페이스이며 제품 CLI/MCP의 공통 잠금 밖에 있습니다.

## 빠른 시작

Expand Down
4 changes: 3 additions & 1 deletion bin/arc-close
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,9 @@ echo "ARC_CLOSED ${TODAY} stop=${STOP} — ${VERDICT}" >> "$ARCHIVE_DIR/$ARC/STA
# 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 [ "${YEOUL_MCP_MANAGED:-0}" = "1" ]; then
SEAL_MSG='not sealed — managed MCP disables external am recording; file only'
elif 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
Expand Down
8 changes: 6 additions & 2 deletions bin/arc-list
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ for arg in "$@"; do
--all) OPEN_ONLY=0 ;;
esac
done
[ -z "$ROOTS" ] && ROOTS="$PROJECTS_DIR"
if [ -z "$ROOTS" ]; then
SEARCH_ROOTS=("$PROJECTS_DIR")
else
read -r -a SEARCH_ROOTS <<< "$ROOTS"
fi

field() { grep -m1 "^$1:" "$2" 2>/dev/null | sed "s/^$1:[[:space:]]*//; s/\"//g"; }

for root in $ROOTS; do
for root in "${SEARCH_ROOTS[@]}"; do
[ -d "$root" ] || continue
while IFS= read -r arcfile; do
# 🔴 `--all` has to mean all. This skip used to run BEFORE the OPEN_ONLY test, so the flag
Expand Down
9 changes: 8 additions & 1 deletion bin/arc-open
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,14 @@ TS_ISO=$(date -Iseconds 2>/dev/null || date +"%Y-%m-%dT%H:%M:%S")
TODAY=$(date +%Y-%m-%d)
ARC="${TS}_${SLUG}"
ARC_DIR="$ARCS_DIR/$ARC"
mkdir -p "$ARC_DIR/ARC"
# Reserve a unique directory atomically. Same-second calls must never overwrite an arc.
mkdir -p "$ARCS_DIR"
if ! mkdir "$ARC_DIR" 2>/dev/null; then
[ -e "$ARC_DIR" ] || { echo 'cannot create arc directory' >&2; exit 1; }
ARC_DIR="$(mktemp -d "$ARCS_DIR/${TS}_${SLUG}.XXXXXX")"
ARC="$(basename "$ARC_DIR")"
fi
mkdir "$ARC_DIR/ARC"
for r in $ROLES; do mkdir -p "$ARC_DIR/tickets/$r"; done

# ── role assignment ledger (relay = auto-assigned to creator, work roles = unassigned) ──
Expand Down
1 change: 1 addition & 0 deletions bin/loop-guard
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ warn_unmeasured(){ [ "$UM" -gt 0 ] && printf ' unmeasured=%s/%s-ticks' "$UM" "$R

case "$CMD" in
init)
[ ! -e "$STATE" ] || { echo 'guard already initialized; refusing to reset accumulated state' >&2; exit 2; }
ROUND=0; MR="${MAXR:-3}"; TB="${BUDGET:-200000}"; TU=0; NP=0; UM=0; save
echo "loop init: max_rounds=$MR token_budget=$TB" ;;
tick)
Expand Down
3 changes: 3 additions & 0 deletions bin/verify_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ def verify(todo, *, baseline=None, revert=False, require=False, timeout=120):
command = VERIFY.search(line)
if item and item[1] == 'x' and (command or require):
rc = execute([os.environ.get('YEOUL_BASH', 'bash'), '-c', command[1]], timeout=timeout) if command else 1
if rc == 124 or rc < 0:
print('verification interrupted; reconciliation required', file=sys.stderr)
return 124
if rc:
print(f'verify failed (exit {rc}): {line.strip()}', file=sys.stderr)
failed = True
Expand Down
11 changes: 11 additions & 0 deletions docs/INTEGRITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ baseline path is `TODO.md.verify-baseline.json`; creating an existing baseline i
refused. Baseline approval is a supervisor action, not an automatic MCP tool.
Criteria changes require explicit review and a new baseline path.

Ordinary failed verification commands revert their checked boxes when `--revert`
is enabled. An interrupted or timed-out command instead makes `verify-gate`
return **124**, stops later commands, and leaves the TODO unchanged for
reconciliation. A surviving checked box is **not evidence of successful
verification**. Inspect partial command effects and surviving children before
resuming; managed MCP leaves the operation pending and refuses automatic retry.
See the [stdio runtime contract](RUNTIME_CONTRACT.md) for managed permissions,
approved baseline locations, receipt recovery and concurrency limits. The baseline
paths above describe direct CLI/trusted usage; managed MCP requires its configured
baseline under `YEOUL_MCP_ROOT/.yeoul-approved/`.

Keep the standalone baseline and verification implementations outside the worker's
write permissions. A directory name alone does not enforce that boundary.
`--current-only` is an explicit manual diagnostic that prints its weaker scope;
Expand Down
Loading
Loading