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: 8 additions & 0 deletions AGENTS.tape
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env tape
# ══════════════════════════════════════════════════════════════════════
# .tape v1.2 — grammar primer (cold-read by any LLM / agent / human)
Expand Down Expand Up @@ -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 <verb>` or `hexa run <verifier>.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 <host> <cmd>`, 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]
Expand Down
46 changes: 46 additions & 0 deletions docs/sessions/2026-05-17-pool-list-json-flag.md
Original file line number Diff line number Diff line change
@@ -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.
42 changes: 40 additions & 2 deletions plugins/pool/main.hexa
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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,
Expand All @@ -849,13 +879,21 @@ 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" +
" add a host: `pool_propose action=add host=<slug>` then `/pool apply <id>`"
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
Expand Down
130 changes: 130 additions & 0 deletions tools/agents-banner-sync.sh
Original file line number Diff line number Diff line change
@@ -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 <file> # 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 <host> <cmd>`, 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 <verb>` or `hexa run <verifier>.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"
Loading