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
8 changes: 7 additions & 1 deletion bin/arc-close
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,13 @@ mv "$ARC_DIR" "$ARCHIVE_DIR/"
SEALED="$ARCHIVE_DIR/$ARC/_SUMMARY_${ARC}.md"

# ── optional: seal into append-only ledger (best-effort; skips if `am` absent) ──
SEAL_MSG="no ledger — file only (install mirror-stack for sealing)"
# 🔴 say what was actually tested. The condition below is "is the `am` CLI on PATH",
# which is not the same as "mirror-stack is not installed" — an MCP-only install has
# mirror-stack present with no `am` on PATH, and the old text sent those users to
# reinstall something they already had.
# 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)'
if command -v am >/dev/null 2>&1; then
if am record --agent yeoul --action arc-close --target "$ARC" \
--payload "{\"stop_reason\":\"${STOP}\"}" \
Expand Down
11 changes: 9 additions & 2 deletions bin/arc-list
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
# arc-list [--roots="dir1 dir2"] [--all]
# Find open deliberation arcs (default status=In-Progress only; --all = every arc). _archive excluded.
# Find deliberation arcs. Default: open (status=In-Progress), _archive excluded.
# --all = EVERY arc, archived ones included (they are the closed record; excluding them made --all
# return nothing on a workspace whose only arc had been closed). Archived rows carry status=Archived.
# Default roots = ./projects/*/design/arcs. Output (tab-separated): STATUS \t ARC_DIR \t TITLE \t ROLES

PROJECTS_DIR="${YEOUL_PROJECTS:-./projects}"
Expand All @@ -19,9 +21,14 @@ field() { grep -m1 "^$1:" "$2" 2>/dev/null | sed "s/^$1:[[:space:]]*//; s/\"//g"
for root in $ROOTS; do
[ -d "$root" ] || continue
while IFS= read -r arcfile; do
case "$arcfile" in */_archive/*) continue ;; esac
# 🔴 `--all` has to mean all. This skip used to run BEFORE the OPEN_ONLY test, so the flag
# named a coverage it did not have: with one closed arc on disk, `--all` printed nothing.
archived=0
case "$arcfile" in */_archive/*) archived=1 ;; esac
if [ "$archived" = "1" ] && [ "$OPEN_ONLY" = "1" ]; then continue; fi
arcdir="$(cd "$(dirname "$(dirname "$arcfile")")" && pwd)"
status="$(field status "$arcfile")"
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
Expand Down
14 changes: 13 additions & 1 deletion bin/arc-open
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,18 @@ MDEOF
} > "$ARC_DIR/ROSTER.md"

# ── JOIN_PROMPTS (A = agent prompt / B = paste into external tab; same body) ──
# 🔴 A join prompt is an instruction someone will RUN, from a cwd we do not control. It used to
# emit the workspace-relative `bin/arc-attach "./projects/..."`, which resolves only when the
# reader happens to sit in the repo root — and setup/install.sh explicitly does NOT promise bin/
# on PATH ("add to PATH, or call by path"). An MCP-only install has no ./bin at all, so every
# role's mandated first action failed (field report, 2026-08-24). Emit resolved absolute paths,
# and refuse to print a command we could not confirm is executable.
ATTACH_BIN="$SCRIPT_DIR/arc-attach"
if [ ! -x "$ATTACH_BIN" ]; then
echo "arc-open: cannot find an executable arc-attach at $ATTACH_BIN" >&2
exit 1
fi
ARC_DIR_ABS="$(cd "$ARC_DIR" && pwd)"
{
echo "# 🔗 Join prompts — ${ARC}"
echo
Expand All @@ -177,7 +189,7 @@ MDEOF
echo "You are the '${r}' role of deliberation arc ${ARC}. Topic: ${TOPIC}."
echo ""
echo "★ First action — run ONLY this line and act on its output (no improvisation):"
echo " bin/arc-attach \"${ARC_DIR}\" ${r}"
echo " \"${ATTACH_BIN}\" \"${ARC_DIR_ABS}\" ${r}"
echo " → it assigns your role, starts the watcher, and dictates your ticket order."
echo ""
echo "[invariant] Do NOT edit the deliberation thread directly (relay-only). Your output = answer in tickets/${r}/ + status open→answered."
Expand Down
10 changes: 9 additions & 1 deletion bin/build-handoff
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,12 @@ echo "┌─ dev handoff ─┐"
echo " project: $NAME"
echo " verdict: ${VERDICT:-TBD}"
echo " created: dev/dev-plan.md · dev/TODO.md (fill blanks from _SUMMARY)"
[ -z "$SUMMARY" ] && echo " ⚠️ no closed arc — usually run after a GO close"
if [ -z "$SUMMARY" ]; then
echo " ⚠️ no closed arc — usually run after a GO close"
fi
# 🔴 exit code must report whether the handoff was BUILT, not what the last test happened to
# return. This line used to be `[ -z "$SUMMARY" ] && echo ...`: with a closed arc found the test
# is false, `&&` short-circuits, and the script exited 1 *on the success path only*. Both paths
# here created dev-plan.md + TODO.md, so both are exit 0; the missing arc is a warning, not a
# failure. Real failures above (no project / already exists) still exit non-zero with a cause.
exit 0
8 changes: 7 additions & 1 deletion bin/close-project
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ mkdir -p "$ARCHIVE"
mv "$SRC" "$DEST"

# optional: seal into append-only ledger (best-effort; content-hash = _CLOSED.md)
SEAL_MSG="no ledger — file only (install mirror-stack for sealing)"
# 🔴 say what was actually tested. The condition below is "is the `am` CLI on PATH",
# which is not the same as "mirror-stack is not installed" — an MCP-only install has
# mirror-stack present with no `am` on PATH, and the old text sent those users to
# reinstall something they already had.
# 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)'
if command -v am >/dev/null 2>&1; then
if am record --agent yeoul --action close-project --target "$NAME" \
--payload "{\"open_arcs\":${OPEN},\"reason\":\"closed\"}" \
Expand Down
26 changes: 17 additions & 9 deletions bin/loop-guard
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,50 @@ ARC_DIR="${1:-}"; CMD="${2:-status}"
[ -d "$ARC_DIR" ] || { echo "arc dir not found: $ARC_DIR" >&2; exit 1; }
STATE="$ARC_DIR/loop_state.tsv"

MAXR=""; BUDGET=""; TOKENS=0; PROGRESS="yes"
# 🔴 TOKENS has no safe default. `--tokens` omitted used to mean 0, so a caller that never
# passed it accumulated 0 forever: STOP:budget could not fire, while `status` printed
# "tokens_used=0/200000" — a line shaped exactly like a measurement. Omission and a
# measured zero must not look the same, so track whether it was ever supplied.
MAXR=""; BUDGET=""; TOKENS=0; TOKENS_GIVEN=0; PROGRESS="yes"
for a in "${@:3}"; do case "$a" in
--max-rounds=*) MAXR="${a#*=}" ;;
--token-budget=*) BUDGET="${a#*=}" ;;
--tokens=*) TOKENS="${a#*=}" ;;
--tokens=*) TOKENS="${a#*=}"; TOKENS_GIVEN=1 ;;
--progress=*) PROGRESS="${a#*=}" ;;
esac; done

ROUND=0; MR=3; TB=200000; TU=0; NP=0
ROUND=0; MR=3; TB=200000; TU=0; NP=0; UM=0
load(){
if [ -f "$STATE" ]; then
while IFS=$'\t' read -r k v; do case "$k" in
round) ROUND=$v ;; max_rounds) MR=$v ;; token_budget) TB=$v ;;
tokens_used) TU=$v ;; no_progress) NP=$v ;;
tokens_used) TU=$v ;; no_progress) NP=$v ;; unmeasured) UM=$v ;;
esac; done < "$STATE"
fi
}
save(){ printf 'round\t%s\nmax_rounds\t%s\ntoken_budget\t%s\ntokens_used\t%s\nno_progress\t%s\n' \
"$ROUND" "$MR" "$TB" "$TU" "$NP" > "$STATE"; }
save(){ printf 'round\t%s\nmax_rounds\t%s\ntoken_budget\t%s\ntokens_used\t%s\nno_progress\t%s\nunmeasured\t%s\n' \
"$ROUND" "$MR" "$TB" "$TU" "$NP" "$UM" > "$STATE"; }
# How much of tokens_used is actually backed by a reported count. Printed with a denominator so a
# quiet "tokens=0/200000" can never be mistaken for a measurement of zero.
warn_unmeasured(){ [ "$UM" -gt 0 ] && printf ' unmeasured=%s/%s-ticks' "$UM" "$ROUND"; return 0; }

case "$CMD" in
init)
ROUND=0; MR="${MAXR:-3}"; TB="${BUDGET:-200000}"; TU=0; NP=0; save
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)
load
ROUND=$((ROUND+1)); TU=$((TU+TOKENS))
if [ "$TOKENS_GIVEN" = "0" ]; then UM=$((UM+1)); fi
if [ "$PROGRESS" = "no" ]; then NP=$((NP+1)); else NP=0; fi
save
if [ "$ROUND" -gt "$MR" ]; then echo "STOP:max-rounds (round=$ROUND > max=$MR)"
elif [ "$TU" -gt "$TB" ]; then echo "STOP:budget (used=$TU > budget=$TB)"
elif [ "$NP" -ge 2 ]; then echo "STOP:no-progress (streak=$NP)"
else echo "CONTINUE (round=$ROUND/$MR tokens=$TU/$TB noprog=$NP)"
else echo "CONTINUE (round=$ROUND/$MR tokens=$TU/$TB noprog=$NP$(warn_unmeasured))"
fi ;;
status)
load; echo "round=$ROUND/$MR tokens_used=$TU/$TB no_progress=$NP" ;;
load; echo "round=$ROUND/$MR tokens_used=$TU/$TB no_progress=$NP$(warn_unmeasured)" ;;
*)
echo "usage: loop-guard <arc_dir> init|tick|status [--max-rounds=N --token-budget=T --tokens=N --progress=yes|no]"; exit 1 ;;
esac
39 changes: 35 additions & 4 deletions bin/yeoul-new
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,32 @@ fi

PROJ_DIR="$PROJECTS_DIR/$NAME"
if [ -e "$PROJ_DIR" ]; then echo "already exists: $PROJ_DIR"; exit 1; fi

# 🔴 Build the scaffold in a staging dir and move it into place only once every step succeeded.
# This used to mkdir the real tree first and then render the spec, so a topic containing `/`
# (e.g. "fast/medium/slow") blew up the sed below — `unknown option to 's'` — and left
# projects/<name>/{design/arcs,dev} plus an empty spec.md behind under `set -e`. The retry then
# hit "already exists". Staging keeps failure atomic: nothing lands unless everything worked.
mkdir -p "$PROJECTS_DIR"
STAGE="$(mktemp -d "$PROJECTS_DIR/.staging-$NAME.XXXXXX")" # same filesystem → mv is atomic
cleanup_stage() { [ -n "${STAGE:-}" ] && rm -rf "$STAGE"; }
trap cleanup_stage EXIT

DESIGN_DIR="$PROJ_DIR/design"
mkdir -p "$DESIGN_DIR/arcs" "$PROJ_DIR/dev"
S_DESIGN="$STAGE/design"
mkdir -p "$S_DESIGN/arcs" "$STAGE/dev"

TEMPLATE="$SCRIPT_DIR/../templates/spec.md"
if [ -f "$TEMPLATE" ]; then
sed "s/<topic>/$TOPIC/g" "$TEMPLATE" > "$DESIGN_DIR/spec.md"
# 🔴 literal substitution, not sed: the topic is arbitrary user text and must never be parsed as
# a sed expression. Bash pattern replacement has no delimiter to escape and no regex to escape.
TPL="$(cat "$TEMPLATE")"
printf '%s\n' "${TPL//<topic>/$TOPIC}" > "$S_DESIGN/spec.md"
else
echo "# 📐 Spec — $TOPIC" > "$DESIGN_DIR/spec.md"
printf '# 📐 Spec — %s\n' "$TOPIC" > "$S_DESIGN/spec.md"
fi

cat > "$PROJ_DIR/README.md" << MDEOF
cat > "$STAGE/README.md" << MDEOF
# $NAME

> Yeoul incubator project. Topic: $TOPIC
Expand All @@ -45,6 +60,10 @@ cat > "$PROJ_DIR/README.md" << MDEOF
- when mature: \`yeoul-graduate $NAME\`.
MDEOF

# everything rendered; publish the staged tree in one move, then stop cleaning it up.
mv "$STAGE" "$PROJ_DIR"
STAGE=""; trap - EXIT

echo "┌─ Yeoul project created ─┐"
echo " project: $NAME"
echo " dirs : $PROJ_DIR/{design,dev}"
Expand All @@ -53,6 +72,18 @@ echo " spec : $DESIGN_DIR/spec.md"
if [ "$MAKE_ARC" -eq 1 ]; then
SLUG="$(echo "$NAME" | tr ' ' '_')"
echo
# 🔴 if the arc fails to open, the project must not survive half-built — same reason as above.
# 🔴 capture the real code. `if ! cmd; then rc=$?` records the *negation's* status (always 0),
# so a failing arc-open was rolled back and then reported as "exit 0" — the same class of bug
# this commit exists to remove. Caught by the rollback test below, which asserted the code.
set +e
bash "$SCRIPT_DIR/arc-open" "$SLUG" --topic="$TOPIC" --roles="$ROLES" --backend="$BACKEND" \
--arcs-dir="$DESIGN_DIR/arcs"
rc=$?
set -e
if [ "$rc" -ne 0 ]; then
rm -rf "$PROJ_DIR"
echo "arc-open failed (exit $rc) — rolled back $PROJ_DIR so a retry starts clean" >&2
exit "$rc"
fi
fi
24 changes: 24 additions & 0 deletions docs/BOOTSTRAP_PROMPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ HARD RULES (do not violate — these are the point of the tool)
error you can quote; say "applying the discipline, I suspect X" for your own reasoning. Never borrow the
tool's credibility for a judgment call.

CONTINUATION (who drives the next step)
- Once I have approved the finalized spec, run the arc through to its next honest stopping point without
asking me to say "continue". Report progress as commentary; give a final answer only when you stop.
- Scaffolding a project, opening an arc, assigning roles, issuing and collecting tickets, running the
bounded rounds, drafting the summary, and the two-phase close are internal steps. None of them is the
end of a request. If the roles run as subagents, my approval of the spec covers spawning them — confirm
the runtime once at the interview, not once per role and not once per round.
- Stop and hand back only for: a tool or connection that actually failed; a gate refusal or loop-guard
STOP; a genuine technical blocker you can reproduce; an action needing authority you were not given
(anything outward-facing, destructive, or costly); a decision that changes the goal rather than
executing it; the arc reaching close/archive. Name which one when you stop.
- This governs sequencing, not judgment. It does NOT let you seal a pre-registration, declare PASS/KILL,
call convergence, graduate, or publish on your own — those stay mine, and "keep going" is never
authority to decide one. Convergence and the verdict are read off the role evidence by you as relay and
put to me; a fixed rule must not stand in for that reading. If continuing would require one of those
decisions, that is a stop.

Confirm you've read METHODOLOGY.md and are ready, then ask me for the seed idea (or which project to resume).
```

Expand All @@ -47,3 +64,10 @@ Confirm you've read METHODOLOGY.md and are ready, then ask me for the seed idea
(blank-refusal, KILL-defense, verify-gate). If you skip a gate manually, the enforcement no longer holds —
that is why the enforced pieces exist as tools, not just instructions.
- Runtime-independent: any agent that can run shell commands and (optionally) call MCP tools can follow this.
- The CONTINUATION block exists because the first external user had to type "continue" at every scaffold
step, and an arc left open when they stepped away (field report, 2026-08-24). The prompt previously named
exactly one legitimate stop (Gate-1) and said nothing about the rest, so an agent reasonably treated each
step as the end of a request. Like the rest of this prompt it is **advisory**: nothing enforces it, and it
deliberately does not — an enforced "keep going" would be a mechanism for skipping the gates. What is
enforced is the opposite direction (blank-refusal, KILL-defense, verify-gate, loop-guard), which is why
continuing is safe to ask for: the stops that matter are in the scripts, not in the agent's manners.
53 changes: 40 additions & 13 deletions setup/pre-publish-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,74 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$SCRIPT_DIR/.." && pwd)"
FAIL=0

# 🔴 Scope the scans to what would actually ship. These used to walk/grep the whole working tree,
# so a gitignored runtime artifact (KNOWLEDGE_INDEX.md, written by every arc close) made the guard
# report "personalization leaks found" about a file git will never publish. A red that does not
# mean what it says gets ignored, which is worse than no guard.
publishable_files() { # publishable_files [-z]
if git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git -C "$REPO" ls-files ${1:+-z}
else
if [ "${1:-}" = "-z" ]; then (cd "$REPO" && find . -type f -not -path './.git/*' -print0)
else (cd "$REPO" && find . -type f -not -path './.git/*'); fi
fi
}

echo "── 1) personalization leak scan ──"
# Generic de-personalization checks (no internal codenames are enumerated here, so this file ships clean):
# (a) any Hangul — this repo is English-only, so any Korean text is a leak;
# (b) private absolute paths (/home/... or /data/...) that must not ship.
# Intentional localizations are allowed (README_KO.md, *.ko.md, docs/ko/); Hangul anywhere else is a leak.
# Hangul detection via python (portable — GNU grep's -P is unavailable on macOS/BSD).
HANGUL="$(python3 - "$REPO" <<'PY'
# NB: the file list goes through a temp file, not a pipe. `python3 - <<PY` makes the heredoc
# itself stdin, so a piped list never arrives and the scan silently reads nothing —
# a guard that inspected zero files and still reported "clean" (caught by its positive control).
FILELIST="$(mktemp)"; publishable_files > "$FILELIST"
# 🔴 the denominator, enforced in the shell. A scan over zero files reports "clean" for every
# section, so an empty list has to fail the run outright — not merely print a note that the
# surrounding `if [ -n "$LEAKS" ]` then treats as clean.
SCANNED="$(grep -c . "$FILELIST" || true)"
if [ "${SCANNED:-0}" -eq 0 ]; then
echo " ✗ scanned 0 files — an empty scan is a failure, not a pass"
FAIL=1
fi
HANGUL="$(python3 - "$REPO" "$FILELIST" <<'PY'
import os, re, sys
root = sys.argv[1]; h = re.compile('[\uac00-\ud7a3]') # Hangul syllables (escaped → this file stays Hangul-free)
for dp, _, fns in os.walk(root):
if '/.git' in dp or dp.endswith('/ko') or '/ko/' in dp: continue
for f in fns:
if f.endswith(('_KO.md', '.ko.md')): continue
p = os.path.join(dp, f)
try:
if h.search(open(p, encoding='utf-8', errors='ignore').read()): print(p)
except Exception: pass
rels = [x.rstrip('\n') for x in open(sys.argv[2], encoding='utf-8') if x.strip()]
if not rels: sys.exit('pre-publish: file list is empty - refusing to report clean')
for rel in rels:
if rel.endswith(('_KO.md', '.ko.md')) or rel.startswith('ko/') or '/ko/' in rel: continue
p = os.path.join(root, rel)
try:
if h.search(open(p, encoding='utf-8', errors='ignore').read()): print(p)
except Exception: pass
PY
)"
PATHS="$(grep -rlE '(/home|/data)/[A-Za-z]' "$REPO" --include='*.sh' --include='*.py' --include='*.md' --include='*.json' --exclude-dir=.git 2>/dev/null || true)"
PATHS="$( (cd "$REPO" && tr '\n' '\0' < "$FILELIST" | xargs -0 -r grep -lE '(/home|/data)/[A-Za-z]' --include='*.sh' --include='*.py' --include='*.md' --include='*.json' -- ) 2>/dev/null || true)"
LEAKS="$(printf '%s\n%s\n' "$HANGUL" "$PATHS" | grep -v '^$' | sort -u)"
if [ -n "$LEAKS" ]; then
echo "$LEAKS" | sed 's/^/ /'
echo " ✗ personalization leaks found (Hangul or private absolute path)"
FAIL=1
else
echo " ✓ clean"
echo " ✓ clean ($SCANNED files scanned)"
fi

echo "── 2) over-claim copy scan ──"
# Superlatives / unfalsifiable marketing the discipline forbids in our own docs.
CLAIM_RE='revolutionary|world.?first|state.of.the.art|SOTA|guarantee[sd]?|never fails|solves? (the )?reproducibility|best.in.class|game.?chang|unprecedented|10x|breakthrough'
if CLAIMS="$(grep -rInE -i "$CLAIM_RE" "$REPO" --include='*.md' --exclude-dir=.git 2>/dev/null \
| grep -vE '/setup/pre-publish-check\.sh')"; then
if CLAIMS="$( (cd "$REPO" && tr '\n' '\0' < "$FILELIST" | xargs -0 -r grep -InE -i "$CLAIM_RE" --include='*.md' -- ) 2>/dev/null \
| grep -vE '(^|/)setup/pre-publish-check\.sh')"; then
echo "$CLAIMS" | sed 's/^/ /'
echo " ✗ over-claim copy found (rewrite to bounded, honest wording)"
FAIL=1
else
echo " ✓ clean"
fi

rm -f "$FILELIST"

echo "── 3) empty-scaffolding guard ──"
if [ -d "$REPO/examples" ] && [ -n "$(find "$REPO/examples" -type f 2>/dev/null | head -1)" ]; then
echo " ✓ examples/ present"
Expand Down
Loading
Loading