diff --git a/AGENTS.tape b/AGENTS.tape index e9078ac..0588429 100644 --- a/AGENTS.tape +++ b/AGENTS.tape @@ -46,6 +46,14 @@ @V := "tape" :: spec [active] version = "1.2" +@X hexa_verify := "hexa verify — verification via hexa CLI only" :: guide [active] + tldr = "Any correctness / purity / grade / identity claim → verify with `hexa ` or `hexa run .hexa` (e.g. `hexa run tool/atlas_verify.hexa`) and paste the verdict VERBATIM. NEVER cite sympy / PyPhi / Wolfram / Mathematica output as evidence." + detail = "~/core/atlas/VERIFY.tape" + +@X wilson_pool := "wilson pool — resource utilization" :: guide [active] + tldr = "Heavy / deterministic work (build · drill · probe · compile · test-sweep · GPU fire) → do NOT serialize on localhost; route to the host pool: `wilson pool list` then `wilson pool on `, or a ToolUse `\"host\"` field. Empty roster = degrade-to-local, fine." + detail = "~/core/atlas/POOL.tape" + # ─── §1 Identity ───────────────────────────────────────────────────── @I id001 := "wilson" :: identity-claim [d=2026-05-14 active] diff --git a/docs/sessions/2026-05-17-pool-list-json-flag.md b/docs/sessions/2026-05-17-pool-list-json-flag.md new file mode 100644 index 0000000..dda7534 --- /dev/null +++ b/docs/sessions/2026-05-17-pool-list-json-flag.md @@ -0,0 +1,46 @@ +# 2026-05-17 — `pool list --json` dead-param fix + +## 문제 +`ws pool list --json` 이 human-readable 출력만 내고 JSON 을 안 냄. + +## 진단 +- CLI 레이어 정상: `core/main.hexa:796` `_pool_flags_only(sub, ["fresh", "json"])` → + `args = #{ "json": true }` 로 파싱. +- 스키마도 정상: `plugins/pool/main.hexa` `pool_list` desc 가 `{"json":{"type":"boolean"}}` 광고. +- 버그: `pool_invoke_list` 가 `args["fresh"]` 만 읽고 `args["json"]` 은 한 번도 안 봄 → + dead-param. JSON 출력 분기가 구현부에 없었음. + +## 수정 (`plugins/pool/main.hexa::pool_invoke_list`) +- `want_json` 플래그 추가. +- 네 경로 전부에 JSON 분기 추가: cache-hit · hosts.json malformed · empty roster · + roster fallback. `json_stringify(#{tool,source,count,hosts,...})` 출력. + +## 검증 +- `wilson build` OK (Darwin-arm64). +- `wilson pool list --json` → 단일 라인 JSON, exit 0. +- `wilson pool list` (플래그 없음) → 기존 human-readable 출력 그대로. +- `wilson test` 23/23 PASS. + +## 상태 +- 커밋: `f01598c` fix(pool-list-json). branch `feat/git-guard`. + +--- + +# 후속 — cache staleness guard + +## 동기 +`pool on ubu-1` 이 WireGuard mesh(10.142.0.1) timeout — mesh 다운. 그런데 +`cache.json` 은 ~30h stale 라 `pool list`/`--json` 이 호스트를 `reachable:true` +로 계속 보고 → consumer 가 죽은 호스트로 라우팅. `reachable` 필드가 조용히 거짓. + +## 수정 (`plugins/pool/main.hexa::pool_invoke_list`, cache 경로) +- `_pool_fmt_age()` 헬퍼 추가 (Ns/Nm/Nh). +- `probed_at` vs `timestamp()` 로 `age_sec` 계산 · `stale = probed_at==0 || age_sec>3600` (1h 임계). +- human 출력: stale 시 `⚠ STALE cache …` 경고 라인 + 헤더에 `age` 표기. +- `--json`: `stale` + `age_sec` 필드 추가. metadata 에도 `stale`. + +## 검증 +- stale (probed_at −99999s): human `⚠ STALE` 라인 출력 · `--json` `stale:true,age_sec:100414`. +- fresh (age 6m): 경고 없음 · `stale:false`. +- 테스트용 cache.json 백업→위조→복원, 복원본 백업과 byte-identical 확인. +- `wilson build` OK · `wilson test` 23/23 PASS. diff --git a/plugins/pool/main.hexa b/plugins/pool/main.hexa index 97be25d..79e152e 100644 --- a/plugins/pool/main.hexa +++ b/plugins/pool/main.hexa @@ -806,18 +806,44 @@ fn pool_invoke_mesh_status(payload: any) -> ToolResult { // roster read from `~/.wilson/pool/hosts.json` when the cache is absent. // `--fresh` (args.fresh=true) skips the cache entirely. Empty pool // (missing/empty hosts.json) renders as "no remote peers configured". +// +// Staleness guard: a cache older than 1h (or with no probed_at) is flagged +// stale — its `reachable` field can silently lie (e.g. mesh went down since +// the probe). Human output prints a ⚠ line; --json carries `stale`+`age_sec`. + +// Human-readable age string for cache staleness display. +fn _pool_fmt_age(sec: int) -> string { + if sec > 3600 { return str(sec / 3600) + "h" } + if sec > 60 { return str(sec / 60) + "m" } + return str(sec) + "s" +} fn pool_invoke_list(payload: any) -> ToolResult { let args = payload["args"] let fresh = has_key(args, "fresh") && args["fresh"] == true + let want_json = has_key(args, "json") && args["json"] == true let cache_path = _pool_cache_path() if fresh == false && file_exists(cache_path) { let text = fs_read_text(cache_path) let cache = json_parse(text) if type_of(cache) == "map" && has_key(cache, "hosts") { let hosts = cache["hosts"] - let mut body = "[pool] hosts (cache: " + cache_path + ", probed_at " + str(cache["probed_at"]) + ")\n" let n = len(hosts) + let probed_raw = if has_key(cache, "probed_at") { cache["probed_at"] } else { 0 } + let probed_at = if type_of(probed_raw) == "int" { probed_raw } else { 0 } + let age_sec = timestamp() - probed_at + let stale = probed_at == 0 || age_sec > 3600 // 1h staleness threshold + if want_json { + let doc = #{ "tool": "pool_list", "source": "cache", "cache_path": cache_path, + "probed_at": probed_at, "age_sec": age_sec, "stale": stale, + "count": n, "hosts": hosts } + return ToolResult { ok: true, content: json_stringify(doc), is_error: false, + metadata: #{ "tool": "pool_list", "source": "cache", "count": n, "stale": stale } } + } + let mut body = "[pool] hosts (cache: " + cache_path + ", probed_at " + str(probed_at) + ", age " + _pool_fmt_age(age_sec) + ")\n" + if stale { + body = body + " ⚠ STALE cache — `reachable` may be wrong (e.g. mesh down); run `pool_probe`\n" + } let mut i = 0 while i < n { let h = hosts[i] @@ -835,12 +861,16 @@ fn pool_invoke_list(payload: any) -> ToolResult { } body = body + "\n(refresh: `pool_list fresh=true` or `pool_probe`)" return ToolResult { ok: true, content: body, is_error: false, - metadata: #{ "tool": "pool_list", "source": "cache", "count": n } } + metadata: #{ "tool": "pool_list", "source": "cache", "count": n, "stale": stale } } } } // Fresh path: read the in-process roster. let parsed = _pool_hosts_list_safe() if type_of(parsed) == "map" && has_key(parsed, "error") { + if want_json { + return ToolResult { ok: false, content: json_stringify(#{ "tool": "pool_list", "error": str(parsed["error"]) }), is_error: true, + metadata: #{ "tool": "pool_list", "error": str(parsed["error"]) } } + } let body = "[pool_list] hosts.json malformed: " + str(parsed["error"]) + "\nhint: see `pool_doctor`; file = " + _pool_hosts_path() return ToolResult { ok: false, content: body, is_error: true, @@ -849,6 +879,10 @@ fn pool_invoke_list(payload: any) -> ToolResult { let hosts = parsed["hosts"] let n = len(hosts) if n == 0 { + if want_json { + return ToolResult { ok: true, content: json_stringify(#{ "tool": "pool_list", "source": "hosts.json", "count": 0, "hosts": [] }), is_error: false, + metadata: #{ "tool": "pool_list", "source": "hosts.json", "entries": [], "count": 0 } } + } let body = "[pool] no remote peers configured\n" + " roster file: " + _pool_hosts_path() + " (empty or missing)\n" + " bootstrap: `cp $WILSON_ROOT/plugins/pool/_hosts.json.template " + _pool_hosts_path() + "`\n" + @@ -856,6 +890,10 @@ fn pool_invoke_list(payload: any) -> ToolResult { return ToolResult { ok: true, content: body, is_error: false, metadata: #{ "tool": "pool_list", "source": "hosts.json", "entries": [], "count": 0 } } } + if want_json { + return ToolResult { ok: true, content: json_stringify(#{ "tool": "pool_list", "source": "hosts.json", "count": n, "hosts": hosts }), is_error: false, + metadata: #{ "tool": "pool_list", "source": "hosts.json", "count": n } } + } let mut out = "[pool] hosts (source: " + _pool_hosts_path() + " — no probe cache; run `pool_probe` for full axes)\n" let mut entries: [string] = [] let mut i = 0 diff --git a/tools/agents-banner-sync.sh b/tools/agents-banner-sync.sh new file mode 100755 index 0000000..a29a9b8 --- /dev/null +++ b/tools/agents-banner-sync.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# tools/agents-banner-sync.sh — sync the "wilson banner" entries into project +# AGENTS.tape files, right below the `@V` spec entry. +# +# Why: until wilson is the primary harness, Claude Code CLI sessions open +# projects and read CLAUDE.md (→ AGENTS.tape). A handful of one-line `@X` +# entries near the top nudge every session toward wilson's operating norms. +# +# The banner is a LIST of `@X` entries — extend BANNER_MARKERS / BANNER_TEXT +# below to add more. Currently: +# @X wilson_pool → ~/core/atlas/POOL.tape (host-pool resource utilization) +# @X hexa_verify → ~/core/atlas/VERIFY.tape (verification via hexa CLI only) +# +# Idempotent PER ENTRY — a file already carrying an entry's marker keeps it; +# only missing entries are inserted. Safe to re-run when new repos or new +# banner entries appear. +# +# Usage: +# tools/agents-banner-sync.sh # sweep all ~/core/*/AGENTS.tape, edit only +# tools/agents-banner-sync.sh --commit # also `git commit --only -- AGENTS.tape` per repo +# tools/agents-banner-sync.sh # operate on one AGENTS.tape +# +# Commit policy (--commit): a repo's AGENTS.tape is committed in isolation +# (`git commit --only`) ONLY when its working-tree diff is exactly our +# insertions (no removed lines, added == 4 lines per entry inserted this +# run) — so an in-flight edit by another session is never swept in. +# Local commit only; never pushed. + +set -u + +# ── banner entries — marker (unique @X id) + the entry text (3 lines each: +# the @X header + tldr body + detail body; insertion prepends one blank). ── +BANNER_MARKERS=( + 'wilson_pool :=' + 'hexa_verify :=' +) +BANNER_TEXT=( +'@X wilson_pool := "wilson pool — resource utilization" :: guide [active] + tldr = "Heavy / deterministic work (build · drill · probe · compile · test-sweep · GPU fire) → do NOT serialize on localhost; route to the host pool: `wilson pool list` then `wilson pool on `, or a ToolUse `\"host\"` field. Empty roster = degrade-to-local, fine." + detail = "~/core/atlas/POOL.tape"' +'@X hexa_verify := "hexa verify — verification via hexa CLI only" :: guide [active] + tldr = "Any correctness / purity / grade / identity claim → verify with `hexa ` or `hexa run .hexa` (e.g. `hexa run tool/atlas_verify.hexa`) and paste the verdict VERBATIM. NEVER cite sympy / PyPhi / Wolfram / Mathematica output as evidence." + detail = "~/core/atlas/VERIFY.tape"' +) +LINES_PER_ENTRY=4 # 1 blank + 3 entry lines, per insertion + +commit=0 +targets=() +for arg in "$@"; do + case "$arg" in + --commit) commit=1 ;; + *) targets+=("$arg") ;; + esac +done + +if [ "${#targets[@]}" -eq 0 ]; then + for f in "$HOME"/core/*/AGENTS.tape; do + [ -f "$f" ] && targets+=("$f") + done +fi + +added=0; skipped=0; committed=0; held=0 +for f in "${targets[@]}"; do + if [ ! -f "$f" ]; then + echo " MISS $f (not found)" + continue + fi + repo=$(dirname "$f") + name=$(basename "$repo") + + inserted=0 + for i in "${!BANNER_MARKERS[@]}"; do + if grep -qF "${BANNER_MARKERS[$i]}" "$f"; then + continue + fi + # locate the end of the @V block (the @V line + its 2-space body) + vline=$(grep -n '^@V' "$f" | head -1 | cut -d: -f1) + if [ -n "$vline" ]; then + vend=$(awk -v s="$vline" 'NR>s { if ($0 !~ /^ /) { print NR-1; exit } } END { if (NR<=s) print NR }' "$f") + else + vend=$(awk '/^#/ { last=NR; next } { exit } END { print last+0 }' "$f") + fi + [ -z "$vend" ] && vend=0 + [ "$vend" -lt 1 ] && vend=0 + tmp="$f.bannersync.tmp" + { head -n "$vend" "$f"; printf '\n%s\n' "${BANNER_TEXT[$i]}"; tail -n +"$((vend + 1))" "$f"; } > "$tmp" + mv "$tmp" "$f" + inserted=$((inserted + 1)) + done + + if [ "$inserted" -eq 0 ]; then + echo " SKIP $name (all banner entries present)" + skipped=$((skipped + 1)) + continue + fi + echo " ADD $name (+$inserted)" + added=$((added + 1)) + + if [ "$commit" -eq 1 ]; then + if ! git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo " not a git repo — edit only" + held=$((held + 1)) + continue + fi + ns=$(git -C "$repo" diff --numstat -- AGENTS.tape 2>/dev/null | head -1) + da=$(echo "$ns" | cut -f1); dr=$(echo "$ns" | cut -f2) + want=$((LINES_PER_ENTRY * inserted)) + if [ "$da" = "$want" ] && [ "$dr" = "0" ]; then + if git -C "$repo" commit -q --only \ + -m "docs(agents): wilson banner — operating-norm pointers + +@X wilson_pool / @X hexa_verify entries below the @V spec — nudge Claude +Code CLI / wilson sessions toward host-pool resource utilization and +verification-via-hexa-CLI for heavy / verification work. Mini-guides: +~/core/atlas/{POOL,VERIFY}.tape. Synced by wilson tools/agents-banner-sync.sh." \ + -- AGENTS.tape 2>/dev/null; then + committed=$((committed + 1)) + else + echo " commit FAILED (hook? — left as working-tree edit)" + held=$((held + 1)) + fi + else + echo " commit skipped — AGENTS.tape diff not exactly our insertion (numstat=$ns, want +$want/-0); left as working-tree edit" + held=$((held + 1)) + fi + fi +done + +echo +echo "agents-banner-sync: ADD $added · SKIP $skipped · committed $committed · held(working-tree) $held"