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
48 changes: 48 additions & 0 deletions bin/_pybin.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Resolve a Python interpreter by RUNNING one — not by finding a name.
#
# 🔴 `command -v python3` is satisfied by the Microsoft Store stub that ships on Windows: the name
# resolves, and the "interpreter" then exits 49 without running anything. Existence is not
# execution. Measured on two Windows machines (2026-08-25): the gate suite scored 34/48 on both,
# and on the second one a perfectly good `python` 3.11.15 was on PATH the whole time — the only
# blocking name was `python3`. The failures printed as `✗ expected 0, got 49`, which reads like a
# verdict but is the absence of a measurement.
#
# Usage — source it, then use $PY unquoted (the value may carry an argument, e.g. `py -3`):
# . "$(dirname "${BASH_SOURCE[0]}")/_pybin.sh"
# PY="$(yeoul_pybin)" || yeoul_pybin_die
# $PY script.py
#
# Override with YEOUL_PYTHON if you need a specific interpreter.

# 🔴 Every gate script here shells out to Python and echoes text back through it — verdicts, kill
# conditions, arc titles. Python encodes stdout with the *caller's console encoding*, so on a
# console that is not UTF-8 a single non-ASCII character (an em-dash was enough) raises
# UnicodeEncodeError, kills the child, and hands the caller EMPTY output. The MCP wrapper already
# pins these for the tools it launches; the shell scripts called Python directly and bypassed it.
# Pin them here, where every script picks up the interpreter.
export PYTHONUTF8=1
export PYTHONIOENCODING=utf-8

yeoul_pybin() {
local c
for c in ${YEOUL_PYTHON:+"$YEOUL_PYTHON"} python3 python "py -3"; do
# word-splitting on $c is intentional: "py -3" is a command plus an argument.
# shellcheck disable=SC2086
if $c -c 'import sys; raise SystemExit(0)' >/dev/null 2>&1; then
printf '%s' "$c"
return 0
fi
done
return 1
}

# Fail loudly. A missing interpreter must stop the run, not quietly skip the step it powers —
# a skipped check that reads as a pass is how this stayed invisible on Windows.
yeoul_pybin_die() {
echo "no working Python interpreter found." >&2
echo " tried: ${YEOUL_PYTHON:+$YEOUL_PYTHON, }python3, python, py -3 — each by running \`-c 'import sys'\`," >&2
echo " not by looking it up. On Windows the Microsoft Store stub answers to \`python3\` and exits 49." >&2
echo " Install Python, or point YEOUL_PYTHON at a real interpreter." >&2
exit 127
}
10 changes: 7 additions & 3 deletions bin/arc-close
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ set -euo pipefail
# ★Optional: seal the close into an append-only ledger (mirror-stack `am`, content-hash = _SUMMARY). Best-effort.
# Negatives / retractions are recorded indelibly (no silent edits).

# Resolve the interpreter by running one — `command -v python3` also finds the Windows Store stub.
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_pybin.sh"
PY="$(yeoul_pybin)" || yeoul_pybin_die

ARC_DIR="${1:-}"; VERDICT="${2:-}"; STOP="converged"
for arg in "${@:3}"; do
case "$arg" in --stop=*) STOP="${arg#--stop=}" ;; esac
Expand Down Expand Up @@ -48,7 +52,7 @@ case "$VERDICT" in *KILL*|*kill*) IS_KILL="yes" ;; esac
# 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
python3 - "$1" "$2" 2>/dev/null <<'PY'
$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'):
Expand Down Expand Up @@ -127,7 +131,7 @@ check_answers() { # check_answers <section start marker>
# 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;
# it is trusted because it just proved it can still say no.
if ! st="$(python3 "$SUBSTANCE" --selftest 2>&1)"; then
if ! st="$($PY "$SUBSTANCE" --selftest 2>&1)"; then
echo "⛔ seal refused: the substance checker failed its own positive control — the instrument is broken, so the real verdict is not interpreted."
printf '%s\n' "$st" | sed 's/^/ /'
exit 6
Expand All @@ -139,7 +143,7 @@ check_answers() { # check_answers <section start marker>
# 🔴 Read the verdict from the emitted CODE, not the exit status. If the checker cannot run at
# all the output is empty — which is not OK — so this **fails closed**. A false default would
# turn "never measured" into "measured and fine".
vout="$(printf '%s' "$vline" | python3 "$SUBSTANCE" --label "$vlabel" 2>/dev/null || true)"
vout="$(printf '%s' "$vline" | $PY "$SUBSTANCE" --label "$vlabel" 2>/dev/null || true)"
vcode="${vout%%"$TAB"*}"; vans="${vout#*"$TAB"}"
[ "$vcode" = "OK" ] && continue
case "$vcode" in
Expand Down
10 changes: 8 additions & 2 deletions bin/arc-open
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ set -euo pipefail
# ARC/<arc>.md (deliberation thread) + tickets/<role>/ + STATE.md + ROSTER.md + JOIN_PROMPTS.md + 0001_spec.md
# Generic file-based deliberation engine. Runtime-agnostic. Callers pass --arcs-dir to place the arc.

# Resolve the interpreter by running one — `command -v python3` also finds the Windows Store stub.
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_pybin.sh"
PY="$(yeoul_pybin)" || yeoul_pybin_die

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

SLUG="${1:-}"; TOPIC=""; ROLES="analysis impl repro"; BACKEND="both"; ARCS_DIR=""; RELAY="orchestrator"
Expand All @@ -27,8 +31,10 @@ case "$BACKEND" in a|b|both) ;; *) echo "backend must be a|b|both"; exit 1 ;; es

# ── Gate-1: prior-art / closed-question check (optional; warn-only, non-blocking; skipped if no registry) ──
REGISTRY="${YEOUL_CLOSED_REGISTRY:-$SCRIPT_DIR/../registry/closed_questions.jsonl}"
if command -v python3 >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/closed_check.py" ] && [ -f "$REGISTRY" ]; then
G1WARN="$(python3 "$SCRIPT_DIR/closed_check.py" "$TOPIC" "$REGISTRY" 2>/dev/null || true)"
# no interpreter name-check here: $PY was resolved by running one, and a run with no working
# interpreter has already stopped at yeoul_pybin_die.
if [ -f "$SCRIPT_DIR/closed_check.py" ] && [ -f "$REGISTRY" ]; then
G1WARN="$($PY "$SCRIPT_DIR/closed_check.py" "$TOPIC" "$REGISTRY" 2>/dev/null || true)"
if [ -n "$G1WARN" ]; then
echo "── ⚠️ Gate-1 closed-question match ──"
echo "$G1WARN"
Expand Down
6 changes: 5 additions & 1 deletion bin/arc-prereg
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ set -euo pipefail
#
# 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.
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_pybin.sh"
PY="$(yeoul_pybin)" || yeoul_pybin_die

ARC_DIR="${1:-}"; CLAIM="${2:-}"; LEDGER="${3:-${YEOUL_LEDGER:-}}"
if [ -z "$ARC_DIR" ] || [ -z "$CLAIM" ]; then
echo "usage: arc-prereg <arc_dir> <claim_id> [ledger] (ledger defaults to \$YEOUL_LEDGER)"; exit 1
Expand All @@ -16,7 +20,7 @@ fi
[ -n "$LEDGER" ] || { echo "no ledger given (pass one or set \$YEOUL_LEDGER)"; exit 1; }
[ -f "$LEDGER" ] || { echo "ledger not found: $LEDGER"; exit 1; }

KILL="$(python3 - "$LEDGER" "$CLAIM" 2>/dev/null <<'PY'
KILL="$($PY - "$LEDGER" "$CLAIM" 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'):
Expand Down
10 changes: 7 additions & 3 deletions bin/index-append
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ set -uo pipefail
# (never rots; not a new store). Index path = $YEOUL_INDEX, default <repo>/KNOWLEDGE_INDEX.md.
# Best-effort: a failure here never affects the close.

# Resolve the interpreter by running one — `command -v python3` also finds the Windows Store stub.
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_pybin.sh"
PY="$(yeoul_pybin)" || yeoul_pybin_die

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ARC_DIR="${1:-}"; [ -n "$ARC_DIR" ] && [ -d "$ARC_DIR" ] || exit 0
ARC="$(basename "$ARC_DIR")"
Expand All @@ -14,15 +18,15 @@ INDEX="${YEOUL_INDEX:-$SCRIPT_DIR/../KNOWLEDGE_INDEX.md}"

CLAIM="-"; [ -f "$ARC_DIR/.prereg" ] && CLAIM="$(sed -n '1p' "$ARC_DIR/.prereg")"

read -r DATE STOP < <(python3 - "$SUM" <<'PY'
read -r DATE STOP < <($PY - "$SUM" <<'PY'
import re,sys
t=open(sys.argv[1],encoding='utf-8',errors='ignore').read()
def g(p,d='-'):
m=re.search(p,t); return m.group(1).strip() if m else d
print(g(r'\*\*Closed\*\*:\s*([0-9-]+)'), g(r'\*\*stop_reason\*\*:\s*(\S+)'))
PY
)
VERDICT="$(python3 - "$SUM" <<'PY'
VERDICT="$($PY - "$SUM" <<'PY'
import re,sys
t=open(sys.argv[1],encoding='utf-8',errors='ignore').read()
m=re.search(r'\*\*Verdict\*\*:\s*(.+)',t); print((m.group(1).strip() if m else '-')[:160])
Expand All @@ -31,7 +35,7 @@ PY
# The old regex assumed the first item was a `- ` bullet, so a numbered list (`1. …`) matched
# nothing → '-'. The close still succeeded and the log still printed "appended" — the failure
# was invisible. Take the section's first substantive line and strip only the list marker.
CLOSED="$(python3 - "$SUM" <<'PY'
CLOSED="$($PY - "$SUM" <<'PY'
import re,sys
t=open(sys.argv[1],encoding='utf-8',errors='ignore').read()
m=re.search(r'##\s*What was closed[^\n]*\n(.*?)(?=\n##\s|\Z)', t, re.S)
Expand Down
8 changes: 6 additions & 2 deletions bin/ralph
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ set -euo pipefail
# ⚠️ Loop-forbidden (belongs to the human/session): measurement runs, sealing, PASS/KILL judgment.
# Items without a `verify:` command are refused (exit 3).

# Resolve the interpreter by running one — `command -v python3` also finds the Windows Store stub.
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_pybin.sh"
PY="$(yeoul_pybin)" || yeoul_pybin_die

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECTS_DIR="${YEOUL_PROJECTS:-./projects}"

Expand Down Expand Up @@ -76,8 +80,8 @@ $(cat "$TODO")
"$SCRIPT_DIR/verify-gate" "$TODO" --revert --require-verify || echo " ⚠ harness reverted a checked item (verify failed on re-run, or its verify clause was missing)"

TOK=0
if command -v python3 >/dev/null 2>&1; then
TOK=$(python3 -c "
if [ -n "$PY" ]; then
TOK=$($PY -c "
import json,sys
try:
d=json.load(open('$OUT')); u=d.get('usage',{})
Expand Down
8 changes: 6 additions & 2 deletions bin/status
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ set -euo pipefail
# One line per active project (path without _archive): name · latest arc verdict · dev TODO progress.
# Projects root ./projects (override YEOUL_PROJECTS).

# Resolve the interpreter by running one — `command -v python3` also finds the Windows Store stub.
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_pybin.sh"
PY="$(yeoul_pybin)" || yeoul_pybin_die

PROJECTS_DIR="${YEOUL_PROJECTS:-./projects}"
MD=0
for a in "$@"; do case "$a" in --md) MD=1 ;; esac; done

[ -d "$PROJECTS_DIR" ] || { echo "no projects dir: $PROJECTS_DIR"; exit 0; }

# truncate to 100 chars on a codepoint boundary (never splits a multibyte char).
# python3 is already a project dependency; if absent, degrade to no truncation (never mojibake).
# $PY is already a project dependency; if absent, degrade to no truncation (never mojibake).
_trunc100() {
python3 -c 'import sys; s=sys.stdin.read().strip(); print(s[:100]+("…" if len(s)>100 else ""))' 2>/dev/null || cat
$PY -c 'import sys; s=sys.stdin.read().strip(); print(s[:100]+("…" if len(s)>100 else ""))' 2>/dev/null || cat
}
verdict_of() {
local proj="$1"
Expand Down
39 changes: 38 additions & 1 deletion bin/substance_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,47 @@ def selftest(verbose=False):
return total - len(fails), total, fails


def emit(code, ans):
"""The verdict channel. ASCII code, TAB, answer — written as UTF-8 BYTES.

🔴 This used to be `sys.stdout.write(...)`, whose encoding is the caller's console encoding. On a
console that is not UTF-8, echoing back an answer containing any non-ASCII character (an
em-dash was enough) raised UnicodeEncodeError, killed the process, and left stdout EMPTY. The
gate reads its verdict from this output and correctly refuses on empty ("an unmeasured field is
not a passing field") — so a genuine answer was refused, on one machine and not another, and
nothing in the refusal pointed at encoding. Measured on Windows 2026-08-25; reproduced with
PYTHONIOENCODING=ascii. The contract is bytes, so it cannot depend on where it is being read.
"""
sys.stdout.buffer.write((code + "\t" + ans).encode("utf-8", "replace"))
sys.stdout.buffer.flush()


def main(argv):
if "--selftest" in argv:
ok, total, fails = selftest(verbose=True)
# 🔴 Print the DENOMINATOR. A checker that measured nothing also prints green.
# 🔴 Echo a non-ASCII answer through the real output path before declaring the checker sound.
# The selftest prints an ASCII-only summary, so it scored 27/27 on a machine where every
# real call carrying an em-dash died writing its result. A positive control that never
# exercises the path under test vouches for nothing.
# 🔴 through emit(), the SAME function the real call uses. An earlier version of this
# control wrote the probe with sys.stdout.buffer directly — it therefore vouched for a
# path the product does not take, and stayed green with the defect reinstated.
probe = "em-dash \u2014 and hangul \uac00 must survive the verdict channel"
try:
emit("SELFTEST_ECHO", probe)
sys.stdout.buffer.write(b"\n")
sys.stdout.buffer.flush()
except Exception as e: # pragma: no cover - the failure this exists to catch
# 🔴 report on stderr, in pure ASCII. The first version used print(... %r) — repr of a
# UnicodeEncodeError contains the offending character, sent through the very channel
# that just failed, so the diagnostic died while reporting the fault it exists to
# report. A failure message must not depend on what it is reporting about.
msg = ("substance_check selftest: FAILED to write a non-ASCII verdict: %s\n"
% type(e).__name__)
sys.stderr.buffer.write(msg.encode("ascii", "replace"))
sys.stderr.buffer.flush()
return 9
print("substance_check selftest: %d/%d (violations %d - genuine %d - raw %d)"
% (ok, total, len(_PLANT_VIOLATIONS), len(_PLANT_GENUINE), len(_PLANT_RAW)))
return 0 if not fails else 9
Expand All @@ -306,7 +343,7 @@ def main(argv):
code, ans = extract(sys.stdin.buffer.read())
if code == "OK":
code, _ = judge(label, ans)
sys.stdout.write(code + "\t" + ans)
emit(code, ans)
return 0 if code == "OK" else 1


Expand Down
6 changes: 5 additions & 1 deletion bin/verify-gate
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ set -uo pipefail
# has already established that every item in a loop-driven TODO carries a verify command.
# Default stays off so a mixed TODO (loop items + manual items) can still be scanned standalone.

# Resolve the interpreter by running one — `command -v python3` also finds the Windows Store stub.
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_pybin.sh"
PY="$(yeoul_pybin)" || yeoul_pybin_die

TODO="${1:-}"; REVERT=0; REQUIRE=0
for a in "${@:2}"; do case "$a" in
--revert) REVERT=1 ;;
Expand All @@ -37,7 +41,7 @@ while IFS= read -r line || [ -n "$line" ]; do
elif printf '%s' "$line" | grep -qE '^- \[x\].*verify:[[:space:]]*`[^`]+`'; then
# extract the FIRST verify block via python (sed's greedy `.*verify:` grabbed the LAST block,
# letting an appended decoy `verify: `true`` override a real failing command).
cmd="$(printf '%s' "$line" | python3 -c 'import sys,re; m=re.search(r"verify:\s*`([^`]+)`", sys.stdin.read()); sys.stdout.write(m.group(1) if m else "")')"
cmd="$(printf '%s' "$line" | $PY -c 'import sys,re; m=re.search(r"verify:\s*`([^`]+)`", sys.stdin.read()); sys.stdout.write(m.group(1) if m else "")')"
if ! bash -c "$cmd" >/dev/null 2>&1; then
FAIL=1
short="$(printf '%s' "$line" | sed -E 's/^(- \[x\][^:]{0,50}).*/\1/')"
Expand Down
8 changes: 6 additions & 2 deletions setup/pre-publish-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ set -uo pipefail
# 3) empty-scaffolding guard (a runnable worked example must exist)
# Exit 0 = clean · non-zero = issues found (do not publish yet).

# Resolve the interpreter by running one — `command -v python3` also finds the Windows Store stub.
. "$(cd "$(dirname "${BASH_SOURCE[0]}")/../bin" && pwd)"/_pybin.sh
PY="$(yeoul_pybin)" || yeoul_pybin_die

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$SCRIPT_DIR/.." && pwd)"
FAIL=0
Expand All @@ -29,7 +33,7 @@ echo "── 1) personalization leak scan ──"
# (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).
# NB: the file list goes through a temp file, not a pipe. `python3 - <<PY` makes the heredoc
# NB: the file list goes through a temp file, not a pipe. `$PY - <<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"
Expand All @@ -41,7 +45,7 @@ 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'
HANGUL="$($PY - "$REPO" "$FILELIST" <<'PY'
import os, re, sys
root = sys.argv[1]; h = re.compile('[\uac00-\ud7a3]') # Hangul syllables (escaped → this file stays Hangul-free)
rels = [x.rstrip('\n') for x in open(sys.argv[2], encoding='utf-8') if x.strip()]
Expand Down
Loading
Loading