From 1faf165fe104d01355d17ce58c61a9492bf1f6a3 Mon Sep 17 00:00:00 2001 From: ghost Date: Sun, 17 May 2026 19:16:38 +0900 Subject: [PATCH 1/6] docs(agents): wilson pool resource-utilization pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One-line @X wilson_pool entry below the @V spec — nudges Claude Code CLI / wilson sessions toward host-pool resource utilization for heavy work. Mini-guide: ~/core/atlas/POOL.tape. Synced by wilson tools/pool-banner-sync.sh. --- AGENTS.tape | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AGENTS.tape b/AGENTS.tape index e9078ac..6b63bab 100644 --- a/AGENTS.tape +++ b/AGENTS.tape @@ -46,6 +46,10 @@ @V := "tape" :: spec [active] version = "1.2" +@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] From d5364d5a3a02c41a27813102fa7d59bcfed7e3e8 Mon Sep 17 00:00:00 2001 From: ghost Date: Sun, 17 May 2026 19:18:45 +0900 Subject: [PATCH 2/6] tools(pool-banner-sync): sweep @X wilson_pool pointer into project AGENTS.tape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Idempotent bash sweep — appends a one-line @X wilson_pool resource- utilization pointer (→ ~/core/atlas/POOL.tape) right below the @V spec entry of each ~/core/*/AGENTS.tape. Until wilson is the primary harness, Claude Code CLI sessions read CLAUDE.md (→ AGENTS.tape) on project open; this nudges every session toward host-pool resource utilization. --commit flag commits each repo's AGENTS.tape in isolation (git commit --only), guarded so a repo with pre-existing AGENTS.tape changes is left as a working-tree edit, never swept into our commit. Re-runnable when new repos appear. Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/pool-banner-sync.sh | 103 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100755 tools/pool-banner-sync.sh diff --git a/tools/pool-banner-sync.sh b/tools/pool-banner-sync.sh new file mode 100755 index 0000000..61b7eba --- /dev/null +++ b/tools/pool-banner-sync.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# tools/pool-banner-sync.sh — append the `@X wilson_pool` resource-utilization +# pointer to 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 one-line `@X wilson_pool` +# entry near the top nudges every session toward `wilson pool` resource +# utilization. Full guide: ~/core/atlas/POOL.tape. +# +# Idempotent — re-running skips files that already carry the entry. Safe to +# re-run when new repos appear. +# +# Usage: +# tools/pool-banner-sync.sh # sweep all ~/core/*/AGENTS.tape, edit only +# tools/pool-banner-sync.sh --commit # also `git commit --only -- AGENTS.tape` per repo +# tools/pool-banner-sync.sh # operate on one AGENTS.tape +# tools/pool-banner-sync.sh --commit +# +# Commit policy (--commit): a repo's AGENTS.tape is committed in isolation +# (`git commit --only`) ONLY when it had no pre-existing uncommitted changes +# — so an in-flight edit by another session is never swept into our commit. +# Local commit only; never pushed. + +set -u + +MARKER='wilson_pool :=' +ENTRY='@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"' + +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; dirty=0; committed=0 +for f in "${targets[@]}"; do + if [ ! -f "$f" ]; then + echo " MISS $f (not found)" + continue + fi + if grep -qF "$MARKER" "$f"; then + echo " SKIP $f (already has @X wilson_pool)" + skipped=$((skipped + 1)) + continue + fi + + repo=$(dirname "$f") + # was AGENTS.tape dirty before we touched it? + pre_dirty=0 + if git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + if ! git -C "$repo" diff --quiet -- AGENTS.tape 2>/dev/null; then + pre_dirty=1 + fi + fi + + # locate the end of the @V block: the @V line + its 2-space-indented 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 + # no @V — fall back to the last contiguous top comment line + vend=$(awk '/^#/ { last=NR; next } { exit } END { print last+0 }' "$f") + fi + [ -z "$vend" ] || [ "$vend" -lt 1 ] && vend=0 + + tmp="$f.poolbanner.tmp" + { head -n "$vend" "$f"; printf '\n%s\n' "$ENTRY"; tail -n +"$((vend + 1))" "$f"; } > "$tmp" + mv "$tmp" "$f" + echo " ADD $f (after line $vend)" + added=$((added + 1)) + + if [ "$commit" -eq 1 ]; then + if [ "$pre_dirty" -eq 1 ]; then + echo " ↳ commit skipped — AGENTS.tape had pre-existing changes" + dirty=$((dirty + 1)) + elif git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + git -C "$repo" commit --only -q -- AGENTS.tape \ + -m "docs(agents): wilson pool resource-utilization pointer + +One-line @X wilson_pool entry below the @V spec — nudges Claude Code CLI +/ wilson sessions toward host-pool resource utilization for heavy work. +Mini-guide: ~/core/atlas/POOL.tape. Synced by wilson tools/pool-banner-sync.sh." \ + && { echo " ↳ committed"; committed=$((committed + 1)); } \ + || echo " ↳ commit FAILED" + else + echo " ↳ not a git repo — edit only" + fi + fi +done + +echo +echo "pool-banner-sync: ADD $added · SKIP $skipped · committed $committed · commit-skipped(dirty) $dirty" From 2cce1f4bc8735295c83df6011a5717b431b983f0 Mon Sep 17 00:00:00 2001 From: ghost Date: Sun, 17 May 2026 19:30:07 +0900 Subject: [PATCH 3/6] =?UTF-8?q?docs(agents):=20wilson=20banner=20=E2=80=94?= =?UTF-8?q?=20operating-norm=20pointers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @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 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AGENTS.tape b/AGENTS.tape index 6b63bab..0588429 100644 --- a/AGENTS.tape +++ b/AGENTS.tape @@ -46,6 +46,10 @@ @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" From 83349328779ba1e398e34a8058ebaab399deaba5 Mon Sep 17 00:00:00 2001 From: ghost Date: Sun, 17 May 2026 19:31:04 +0900 Subject: [PATCH 4/6] =?UTF-8?q?tools(agents-banner-sync):=20generalize=20p?= =?UTF-8?q?ool-banner-sync=20=E2=86=92=20N-entry=20banner;=20add=20hexa=5F?= =?UTF-8?q?verify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed tools/pool-banner-sync.sh → tools/agents-banner-sync.sh and generalized from a single hardcoded entry to a BANNER_MARKERS / BANNER_TEXT list — per-entry idempotent (a file keeps the entries it already has; only missing ones are inserted). Banner now carries two @X entries swept below the @V spec of every ~/core/*/AGENTS.tape: @X wilson_pool → ~/core/atlas/POOL.tape (host-pool resource use) @X hexa_verify → ~/core/atlas/VERIFY.tape (verification via hexa CLI) --commit guard generalized: commits only when the AGENTS.tape diff is exactly our insertion (added == 4·entries-inserted, removed == 0) so a repo with pre-existing AGENTS.tape edits is left as a working-tree edit. Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/agents-banner-sync.sh | 130 ++++++++++++++++++++++++++++++++++++ tools/pool-banner-sync.sh | 103 ---------------------------- 2 files changed, 130 insertions(+), 103 deletions(-) create mode 100755 tools/agents-banner-sync.sh delete mode 100755 tools/pool-banner-sync.sh 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" diff --git a/tools/pool-banner-sync.sh b/tools/pool-banner-sync.sh deleted file mode 100755 index 61b7eba..0000000 --- a/tools/pool-banner-sync.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env bash -# tools/pool-banner-sync.sh — append the `@X wilson_pool` resource-utilization -# pointer to 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 one-line `@X wilson_pool` -# entry near the top nudges every session toward `wilson pool` resource -# utilization. Full guide: ~/core/atlas/POOL.tape. -# -# Idempotent — re-running skips files that already carry the entry. Safe to -# re-run when new repos appear. -# -# Usage: -# tools/pool-banner-sync.sh # sweep all ~/core/*/AGENTS.tape, edit only -# tools/pool-banner-sync.sh --commit # also `git commit --only -- AGENTS.tape` per repo -# tools/pool-banner-sync.sh # operate on one AGENTS.tape -# tools/pool-banner-sync.sh --commit -# -# Commit policy (--commit): a repo's AGENTS.tape is committed in isolation -# (`git commit --only`) ONLY when it had no pre-existing uncommitted changes -# — so an in-flight edit by another session is never swept into our commit. -# Local commit only; never pushed. - -set -u - -MARKER='wilson_pool :=' -ENTRY='@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"' - -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; dirty=0; committed=0 -for f in "${targets[@]}"; do - if [ ! -f "$f" ]; then - echo " MISS $f (not found)" - continue - fi - if grep -qF "$MARKER" "$f"; then - echo " SKIP $f (already has @X wilson_pool)" - skipped=$((skipped + 1)) - continue - fi - - repo=$(dirname "$f") - # was AGENTS.tape dirty before we touched it? - pre_dirty=0 - if git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1; then - if ! git -C "$repo" diff --quiet -- AGENTS.tape 2>/dev/null; then - pre_dirty=1 - fi - fi - - # locate the end of the @V block: the @V line + its 2-space-indented 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 - # no @V — fall back to the last contiguous top comment line - vend=$(awk '/^#/ { last=NR; next } { exit } END { print last+0 }' "$f") - fi - [ -z "$vend" ] || [ "$vend" -lt 1 ] && vend=0 - - tmp="$f.poolbanner.tmp" - { head -n "$vend" "$f"; printf '\n%s\n' "$ENTRY"; tail -n +"$((vend + 1))" "$f"; } > "$tmp" - mv "$tmp" "$f" - echo " ADD $f (after line $vend)" - added=$((added + 1)) - - if [ "$commit" -eq 1 ]; then - if [ "$pre_dirty" -eq 1 ]; then - echo " ↳ commit skipped — AGENTS.tape had pre-existing changes" - dirty=$((dirty + 1)) - elif git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1; then - git -C "$repo" commit --only -q -- AGENTS.tape \ - -m "docs(agents): wilson pool resource-utilization pointer - -One-line @X wilson_pool entry below the @V spec — nudges Claude Code CLI -/ wilson sessions toward host-pool resource utilization for heavy work. -Mini-guide: ~/core/atlas/POOL.tape. Synced by wilson tools/pool-banner-sync.sh." \ - && { echo " ↳ committed"; committed=$((committed + 1)); } \ - || echo " ↳ commit FAILED" - else - echo " ↳ not a git repo — edit only" - fi - fi -done - -echo -echo "pool-banner-sync: ADD $added · SKIP $skipped · committed $committed · commit-skipped(dirty) $dirty" From f01598cababcddb575ead371ad1834b75a4dc147 Mon Sep 17 00:00:00 2001 From: ghost Date: Sun, 17 May 2026 19:33:58 +0900 Subject: [PATCH 5/6] fix(pool-list-json): wire the advertised --json flag in pool_invoke_list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pool_list's schema advertised {"json":{"type":"boolean"}} and the CLI layer parsed `pool list --json` into args.json=true, but pool_invoke_list only ever read args.fresh — the json param was dead. Added a want_json branch to all four exit paths (cache-hit · hosts.json malformed · empty roster · roster fallback) emitting json_stringify of the host data. wilson test 23/23 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-17-pool-list-json-flag.md | 25 +++++++++++++++++++ plugins/pool/main.hexa | 21 +++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 docs/sessions/2026-05-17-pool-list-json-flag.md 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..d9c75f3 --- /dev/null +++ b/docs/sessions/2026-05-17-pool-list-json-flag.md @@ -0,0 +1,25 @@ +# 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. + +## 상태 +- 커밋 안 함 (사용자 요청 대기). branch `feat/git-guard`. diff --git a/plugins/pool/main.hexa b/plugins/pool/main.hexa index 97be25d..ce69b3e 100644 --- a/plugins/pool/main.hexa +++ b/plugins/pool/main.hexa @@ -810,14 +810,21 @@ fn pool_invoke_mesh_status(payload: any) -> ToolResult { 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) + if want_json { + let doc = #{ "tool": "pool_list", "source": "cache", "cache_path": cache_path, + "probed_at": cache["probed_at"], "count": n, "hosts": hosts } + return ToolResult { ok: true, content: json_stringify(doc), is_error: false, + metadata: #{ "tool": "pool_list", "source": "cache", "count": n } } + } + let mut body = "[pool] hosts (cache: " + cache_path + ", probed_at " + str(cache["probed_at"]) + ")\n" let mut i = 0 while i < n { let h = hosts[i] @@ -841,6 +848,10 @@ fn pool_invoke_list(payload: any) -> ToolResult { // 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 +860,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 +871,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 From 046718bf11ee19aae85bc3499afddb365c7b1699 Mon Sep 17 00:00:00 2001 From: ghost Date: Sun, 17 May 2026 19:50:03 +0900 Subject: [PATCH 6/6] feat(pool-list-stale-guard): flag stale probe cache in pool_list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A cache older than 1h (or missing probed_at) can have `reachable` silently lie — e.g. the WireGuard mesh goes down after the probe, so `pool list` keeps reporting a dead host as reachable and consumers route work to it. pool_invoke_list's cache path now computes age_sec from probed_at vs timestamp() and flags stale=true past the 1h threshold. Human output gains a `⚠ STALE cache` warning line + `age` in the header; --json gains `stale` + `age_sec` fields (and metadata.stale). Verified: forged-old cache → ⚠ + stale:true; fresh cache → no warning. wilson test 23/23 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-17-pool-list-json-flag.md | 23 +++++++++++++++- plugins/pool/main.hexa | 27 ++++++++++++++++--- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/docs/sessions/2026-05-17-pool-list-json-flag.md b/docs/sessions/2026-05-17-pool-list-json-flag.md index d9c75f3..dda7534 100644 --- a/docs/sessions/2026-05-17-pool-list-json-flag.md +++ b/docs/sessions/2026-05-17-pool-list-json-flag.md @@ -22,4 +22,25 @@ - `wilson test` 23/23 PASS. ## 상태 -- 커밋 안 함 (사용자 요청 대기). branch `feat/git-guard`. +- 커밋: `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 ce69b3e..79e152e 100644 --- a/plugins/pool/main.hexa +++ b/plugins/pool/main.hexa @@ -806,6 +806,17 @@ 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"] @@ -818,13 +829,21 @@ fn pool_invoke_list(payload: any) -> ToolResult { if type_of(cache) == "map" && has_key(cache, "hosts") { let hosts = cache["hosts"] 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": cache["probed_at"], "count": n, "hosts": hosts } + "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 } } + 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 body = "[pool] hosts (cache: " + cache_path + ", probed_at " + str(cache["probed_at"]) + ")\n" let mut i = 0 while i < n { let h = hosts[i] @@ -842,7 +861,7 @@ 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.