diff --git a/home_root/.claude/CLAUDE.md b/home_root/.claude/CLAUDE.md index 9e5203e..79e7112 100644 --- a/home_root/.claude/CLAUDE.md +++ b/home_root/.claude/CLAUDE.md @@ -4,10 +4,26 @@ Loaded into every Claude Code session regardless of project. Project- local `CLAUDE.md` files take precedence when they conflict; this file is the baseline. +⚠ **This file is a symlink into the dotfiles repo** +(`~/dotfiles/home_root/.claude/CLAUDE.md`, gitea `vh/dotfiles`). +Edit the **target path** directly. Claude Code's Edit tool refuses to +write through the link and names the target, so use that path. **Never +use `sed -i` or any other write-temp-then-rename tool on +`~/.claude/CLAUDE.md`:** that replaces the link with a detached copy, +and the dotfiles backup silently goes stale. It happened once already, +around 2026-09-18, and went unnoticed until 2026-09-24. The same +applies to `statusline-command.sh`, `CONTEXT.md` and +`keybindings.json` in `~/.claude`. + ## Operator identity -The operator is **Vuong Hoang** (lkraven@lkraven.com), addressed as -"Vuong". Roles across three organizations: +The operator is **Vuong Hoang** (lkraven@lkraven.com), aka **Prime**, +callsign **Papa Romeo Mike**. Every session may address him as "Prime" or +"Vuong", and anywhere you would write "the operator" you may write +"Prime". Existing "operator" text stays valid, so do not rewrite docs to +swap the word. **Legal names stay legal:** license text, copyright +lines, git authorship and attribution, and anything legal or contractual +use **Vuong Hoang**, never "Prime". Roles across three organizations: - **Corviduo AI** — Founder, Principal Developer, Chief Systems Architect. - **PhaseFinal, Inc** — Founder, Principal Developer, Chief Systems Architect. @@ -29,6 +45,27 @@ consequence calls she relays remain Vuong's own — Miranda is the authorized conduit, not a substitute decision-maker — so surface a genuine concern to her the same way you would to him, then execute. +**Miranda is also the way to reach Prime when he is not in your +session.** She can notify him either way. Her althing handle is +`miranda` (Ledger's chief-of-staff persona agent). **Tell her about +high-priority matters only:** things that need Prime's attention or +decision before he would otherwise see your session. Examples: an +outage, a security or data-loss event, a blocker on time-critical or +irreversible work, or anything he would be unhappy to learn about late. +**No noise:** no status updates, completions, FYIs or routine questions +unless Prime has asked for that stream. A message to `miranda` has one +recipient, so it is not a broadcast and needs no pre-approval under +§ Althing → "Broadcasts need the operator's okay FIRST". The priority +bar, not the permission rule, is what gates it. + +How it works, per Miranda (route verified end to end on 2026-09-24): +**every message to her becomes a summary on Prime's phone**, so each +send costs his attention. Put **URGENT** in the subject only if it +really cannot wait (broken, blocked, or work held on his decision); she +will then also phone him. In the body, say what happened, what state +it is in now, and whether Prime needs to act. Send one matter per +message, and do not reply just to thank her or acknowledge. + ## Operator judgment — legality & fair use **You're free to refuse unethical and illegal requests, but please @@ -215,7 +252,7 @@ What every session needs before it loads the skill: not the single source of truth. 2. **Check the handoff.** `persistent-memory.md` opens with a handoff-check header naming `/tmp/-handoff.md`. If - that file exists and its `Written:` stamp is under an hour old, read + that file exists and its `Written:` stamp is under 8 hours old, read it, then delete it. Older: stale — delete unread. This is the post-`/clear` resume vector; no pasted prompt required. 3. **Never bulk-read `persistent-memory.d/`.** The dated log sections @@ -343,8 +380,8 @@ the skill. Always triage Heid output before acting on it. stateful component. Operator page at /. client postbox (+ `althing-mcp` for the stdio tools) herald althing-po-herald one per box, supervised, ops-owned - waiter althing-listen one per session — THIS is what makes a - session pokeable; without it you are pull-only + waiter althing-listen NOT NEEDED — delivery to Claude Code is + PUSH (see below). Legacy, pull-era only. **Both env vars are required and there is no default address** — a bare `postbox status` errors rather than guessing: @@ -362,47 +399,57 @@ says it could not reach the post office, that is the fault — do not read it as "no mail". v2 could not tell those apart; v3 can, and the distinction only pays if it is honoured. -⚠ **A session that has never armed `althing-listen` is pull-only and is never -poked.** The herald pokes FIFOs; no waiter means no FIFO means no poke target. -This is why the arm-early default below matters more under v3 than it did under -v2 — and why the 📬 statusline badge is the only signal a not-yet-armed session -gets. +**Delivery to Claude Code is PUSH — do not arm a waiter (operator, +2026-09-11).** The herald injects an `[ALTHING-HERALD] New mail` message +straight into the session, so peer mail reaches you with no `althing-listen` +running. **`/althing:monitor` and `/althing:sign-off` are both unnecessary**; +proposing them is noise. `postbox status` reports the mode: -**Default posture — keep the monitor armed (operator directive, -2026-07-14).** Unless a session says otherwise, every session should -**attempt to keep its althing monitor armed for the session's -duration** — arm it early (`/althing:monitor`) and re-arm after each -fire (drain the fire via `/althing:inbox`, then re-arm), so peer mail -is caught proactively rather than only surfacing on the next user -prompt. The arm-on-dispatch rule below is the *minimum* case of this -broader default; the default is to stay armed **even without a -specific pending reply**, because peer agents reach out unprompted. + handle: infra-ops unread: 0 mode: push reachable: True node: nh3-dev -When you ship an althing message that you expect to come back with a -reply — a consult (`/heid*`), a question to a peer agent, a request -awaiting an answer — and you do **not** already have an althing -monitor running for this session, **always start one** -(`/althing:monitor`). The dispatch and the monitor are one action: you -opened a loop, so arm the thing that closes it. +⚠ **`mode:` is authoritative — read it, don't reason about it.** This section +used to carry a keep-the-monitor-armed default (operator directive 2026-07-14) +written for the pull era, plus an arm-on-dispatch rule. Both are **retired**. On +2026-09-11 a session recommended arming the monitor **three times in a row while +three pushed messages were landing in its own context with no waiter running** — +doctrine overriding what was visibly happening. If the delivery mode is ever +genuinely in doubt, ask `postbox status`; do not infer it from this file. -Why: in an interactive session the inbox banner (📬) only fires on the -*next* user prompt. If the operator steps away after you dispatch an -async consult, the reply can sit unseen indefinitely — the loop you -opened never closes on its own. A monitor actively pings when the -reply lands, so the await-reply promise ("reply will land in your -inbox") is backed by a real notification, not by hoping the operator -prompts again. +⚠ Do not check for a waiter with `pgrep -f althing-listen` — the pattern matches +your own command line and reports a waiter that isn't there. Same trap as +`pkill -f` over ssh. -Scope of the *arm-on-dispatch* rule: it fires for any message where -you're waiting on a response (a pure fire-and-forget FYI doesn't -*oblige* a fresh arm) — but under the keep-armed default the monitor is -typically already up regardless. Do **not** sign off after each -exchange (that fights the default); sign off (`/althing:sign-off`) at -session wind-down or on operator direction. Waiters don't accumulate -— the lock is one-per-handle, so re-arming freely is safe. -Stop a stuck one with `althing-listen --stop`, which SIGTERMs the pid in -that handle's lock file — never `pkill`, which is handle-blind and kills -every agent's waiter on the box, not just yours. +**Unchanged by the mode, and the part that actually matters:** when mail arrives +or you ship a message expecting a reply, the loop is yours to close. Drain it +with `/althing:inbox`, **act** on what it asks, then **answer** the peer who +wrote it — reading without acting, and acting without answering, both leave it +open. One exception worth naming: do not reply to a pure close-out ("thread +closed from my end"), because acking an ack re-pokes their session. + +If a legacy waiter is somehow running and stuck, stop it with +`althing-listen --stop` (SIGTERMs the pid in that handle's lock file) — never +`pkill`, which is handle-blind and kills every agent's waiter on the box. + +### ⚠ Broadcasts need the operator's okay FIRST (operator, 2026-09-22) + +**Any fleetwide or multi-recipient althing announcement requires explicit +operator approval before it is sent.** Ask, then send — never send and report. + +The cost is the reason: one broadcast wakes every recipient session, each of +which then reads, reasons about, and often replies to it. **That is a great way +to burn a shit ton of tokens and generate noise** — the spend is multiplied by +the recipient count and paid out of budgets the sender never sees. + +- **Gated:** anything addressed to more than one handle, anything shaped as an + announcement/FYI/heads-up to the fleet, release notes, migration notices, + convention changes, "everyone should know" posts. +- **NOT gated:** a single-recipient message — a reply to a peer who wrote to + you, a direct ask to one handle, a handover to the one agent taking over. + Closing a loop with the peer at the other end of it stays at your discretion + and needs no approval. + +When a broadcast genuinely seems warranted, surface it as a decision with the +draft attached and the recipient count named, and let the operator call it. ## Development workflow shapes @@ -552,184 +599,42 @@ enough," keep the gate rather than drop it. ## Global tools available -Tools standing ready in the working environment — assume present and use -them without a setup detour. (Most Claude Code sessions run natively on -**nh3-dev** `10.100.10.50`, the dev box; "available" means there unless a -note says otherwise. On other boxes, check first.) +**The fleet tool + service index is `/home/lkraven/FLEETTOOLS.md`** (canonical: +`~/development/eshpfi-management/docs/fleettools/`, git-tracked). It is an *index* — +read it whole, it is short, then open only the detail file for the tool you actually +need. It covers althing, the Booth, the `secret` vault CLI, the LiteLLM gateway, +inference seats + the Asset Engine control plane, TTS/ASR, Arbo, elway, fleet SSH, +observability, graphify, Playwright, the Henge, and the Claude-only harness tools. +The same file is autoloaded by Codex (`~/.codex/AGENTS.md`) and Grok +(`~/.grok/rules/00-fleettools.md`) via a shared bootstrap pointer, so every agent +family on this box works from one source of truth. -- **Playwright + headless Chromium** — installed **box-wide on nh3-dev**: - the system shared-libs (apt, via `playwright install-deps`), the browser - binaries in shared **`/opt/ms-playwright`** (chromium + headless-shell, - root-owned + world-readable), and - `PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright` wired globally - (`/etc/environment` + `/etc/profile.d/`). A project only needs the - `playwright` module (`npm i playwright` — the browser download is - skippable, it resolves the shared binary); **no per-project - `playwright install`**. Use for anything that needs a real browser - engine: true DOM/SVG layout geometry, screenshots, headless rendering, - browser-based tests or scraping. Add a browser / bump via - `ssh infra-ops@10.100.10.50 'sudo env PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright npx -y playwright install '`. +⚠ **Query live inventories, never a written list** — Homepage +`http://10.0.50.45:5100/api/services`, asset-engine +`http://10.250.50.70:8200/api/v1/services`, LiteLLM +`http://10.250.50.70:4000/v1/models`, and any FastAPI seat's `/openapi.json`. -- **`ratecheck` — how close this session is to its rate limits** (`~/.claude/bin/ratecheck`, - reading `~/.claude/rate-limits.json`). **Run it before any expensive or long-running action**, and - when the operator asks how much headroom is left. Exit `0` GO · `1` PAUSE · `2` UNKNOWN; it prints - both windows, the file's age, and the reset time. +**Shared all-agents LiteLLM key:** `sk-eA_XOdcs6nIkyYXonohtEQ` (alias +`all-agents-local`). ⚠ It reaches **every** model including paid GLM/Kimi +passthroughs, and a paid call spends real vendor credits shared across every session +holding it. For isolated, auditable spend, ask infra-ops for a project-scoped key. - ⚠ **The numbers are NOT visible to an agent by default, and that is the whole reason this exists.** - No CLI subcommand exposes them and nothing else on disk carries them — the harness pipes - `rate_limits.{five_hour,seven_day}.{used_percentage,resets_at}` **to the statusline and nowhere - else.** So `~/.claude/statusline-command.sh` tees them atomically to `~/.claude/rate-limits.json` - on every render (added 2026-09-05). **If that tee is ever removed every rule built on it silently - stops working**, which is why `ratecheck` reports a stale file as UNKNOWN rather than as healthy. +Three directives from that set stay here because they govern *behaviour*, not lookup: - **What to do with it, in ascending cost of the action:** - - **Under ~85%** — proceed normally. - - **~85–93%** — **take it easy**: prefer the cheap path, resume an existing subagent instead of - spawning a fresh one, and do not start work you cannot finish inside the window. Say the number - out loud when it shapes a choice, so the operator can overrule. - - **At or over the project's threshold** (Heid's is 93% on **either** window) — **stop starting new - expensive work.** Let anything in flight finish; killing it burns what was already spent. - - **UNKNOWN** — ⚠ **proceed, loudly.** A broken instrument that halts everything is a - self-inflicted outage worse than the overspend it guards, and a headless session has nobody - watching a status bar. **Say plainly that the limits were unreadable and you continued blind.** - Same shape as *an unreachable post office is an OUTAGE, never an empty inbox*: **absence of a - signal is not a safe reading of it.** - - ⚠ **The check happens before the spend, and the spend lands after.** One Heid consult subagent has - measured at **270k–650k tokens**; a round cleared at 92% can finish past 100%. **A GO is margin, - not permission for an arbitrarily large action.** ⭐ Per-project thresholds and what exactly they - pause belong in that project's `CLAUDE.md` — this entry owns the instrument, not the policy. - -- **Graphify** — open-source knowledge-graph skill for AI coding assistants - (`uv tool install graphifyy`; CLI `graphify`, MCP `graphify-mcp`). Turns a - repo into a queryable graph. The **free, deterministic path** is - `graphify update `: tree-sitter AST extraction + Leiden clustering, - **zero LLM / zero tokens**, ~25s for ~900 files; writes - `graphify-out/{graph.json,GRAPH_REPORT.md}`. Highest-value free output is - the **God Nodes** list (core-abstraction surfacing) plus `graphify explain - ` / `query` / `affected` / `path` traversals over `EXTRACTED` call - edges. The LLM layer (community **labeling** = low-caliber, and **semantic - extraction** of docs/INFERRED edges = higher-caliber + noisier) is optional - and points at any backend via `~/.graphify/providers.json` — pair labeling - with the cheap LiteLLM/Granite endpoint below; leave deep semantic - extraction off unless needed (its INFERRED/AMBIGUOUS edges fight the - explicit-over-implicit floor). Complements a hand-authored `docs/CODEBASE.md` - (curated geography), doesn't replace it. Pilot-validated on Worldtree - 2026-06-10. - -- **LiteLLM gateway (local vLLM + GLM/Kimi passthrough)** — OpenAI-compatible gateway at - `http://10.250.50.70:4000/v1` (Logs UI `:4000/ui`) fronting ana-ml2 / irv-ml1 vLLM services - plus paid passthroughs. Essentially-**free local compute** for low-caliber, high-volume, - parallelizable LLM work. The **always-available summarizer / classifier endpoint** is the - `summarizer` (+ `summarizer-large`) / `classifier` alias — the go-to for summarization, - naming/labeling, classification, and triage. Since 2026-08-15 it's backed by - **`qwen3.8-27b-uncensored`** (JonathanColetti Heretic-abliterated Qwen3.8-27B, quantized - in-house to NVFP4 W4A16 + grafted MTP head, vision-intact, 262K ctx) on ana-ml2 `:8015` — - compose project `gen-seat` / container `vllm-gen` — the same "gen" seat that also serves - `gen` (general chat), `gen-reasoning` (thinking variant), `image-judge`, and `qwen-image-bench`. - (Prior `qwen3.6-35b-a3b-heretic` MoE and `granite-4.1-8b` are retired from the roster.) Treat the summarizer as a standing dependency you can reach any time. The - roster drifts — query the live list at `:4000/v1/models` (~30 entries as of 2026-08-14); the - current tool-relevant set: - - **Embeddings / rerankers (local, free):** `qwen3-embedding`, `qwen3-reranker`, `reranker` - (BAAI bge-v2-m3), `reranker-a4-gte-modernbert`. - - **Small fast local (free):** `coder-fast` (qwen2.5-coder-1.5b), `lfm2.5-2.6b` (Liquid) — - cheap for high-volume mechanical work. - - **Judge (local, free):** `selene-1-mini-8b` / `chat-judge` (Atla Selene reward/quality judge). - - **Frontier passthrough (PAID — spends vendor credits):** `gen-frontier` / - `gen-frontier-reasoning` → GLM 5.2; the GLM family `glm-5.2` (+`-reasoning`), `glm-5.1` - (+`-reasoning`), `glm-5-turbo`, `glm-4.7`, `glm-4.5-air` (z.ai); and `kimi-k3` / - `kimi-k3-gen-api` (Moonshot — 1M-context **reasoning** models: answer in `content`, - chain-of-thought in `reasoning_content`, and they 400 on any `temperature` other than 1, - so omit the field entirely rather than sending 0). - - **TTS:** `ext-tts` → the fleet dots gateway (irv-ml1 `:8198`; voices donut/emmie/glados/ - miranda/sindra + emotion variants, or OpenAI aliases nova/alloy — repointed off the parked - zonos 2026-08-13; an unknown voice 404s and can trip the router cooldown). The gateway 401s without a **virtual API key**. **Shared - all-agents key** — `sk-eA_XOdcs6nIkyYXonohtEQ` (alias `all-agents-local`): - reaches **every** model behind the gateway, paid passthroughs included - (corrected 2026-07-25 — this entry previously read "scoped to free local - models only... the shared key deliberately can't spend z.ai cost," which - infra-ops says was never actually true: the key already spent GLM/z.ai - credits before this correction, and now reaches kimi-k3 the same way). - **A paid-model call on this key spends real vendor credits, shared across - every session and project holding it** — there is no isolation between - what your session runs and what anyone else's does. Use it directly for - the always-available free local endpoints — no per-project provisioning - needed. Example: `curl http://10.250.50.70:4000/v1/chat/completions -H - "Authorization: Bearer sk-eA_XOdcs6nIkyYXonohtEQ" -d - '{"model":"summarizer","messages":[…]}'`. For spend isolated from the - shared pool — auditable per-project cost, blast-radius containment — still - request a project-scoped key from infra-ops via althing; that remains the - right move for anything where "shows up on someone else's tab" matters, it's - just no longer a hard technical requirement for reaching a paid model. - Rotatable via infra-ops if it leaks. Reach for the free local endpoints - before spending premium API tokens on tasks a small fast local model - handles fine (the LLM-last-resort floor still applies — deterministic - first). - -- **The Booth** — ephemeral media board for surfacing **A/B renders, smoke - results, screenshots, audio/video** to the operator (live at - `http://10.100.10.50:8090/`, Homepage → *Apps → The Booth*). A booth is - *just a folder* — no upload API: any session drops files into - `~/booth-data//` on **nh3-dev** (or from another host - `rsync -a ./out/ nh3-dev:booth-data//`), then hands the operator - `http://10.100.10.50:8090/b//`. A folder's own `index.html` is served - verbatim (build a custom report); otherwise it auto-renders a gallery of - images / webm+mp4 video / audio, with a `.txt` sidecar folded in as - that item's caption (labels A/B pairs). Wiped **24h after last activity**. - No auth — LAN/WG-internal + ephemeral, so don't drop secrets in a booth. - Reach for it instead of describing results in prose when a picture, clip, or - side-by-side would land faster. Code + helper CLI: eshpfi - `services/booth/` (`scripts/booth {new|add|url|ls|rm|keep|unkeep|link}`); - user-level systemd on nh3-dev (`systemctl --user status booth.service`). - - **STANDING CONVENTION (operator, 2026-08-19) — POST EVERY OPERATOR-FACING - LINK TO THE BOOTH LINK BOARD.** Any URL you hand the operator that is worth - more than the next thirty seconds — a booth of renders, a PR, an issue, a - newly-deployed service, an artifact page, a dashboard — **also** goes on the - standing link board, in the same turn you mention it: - - ```bash - booth link "" # on nh3-dev - ssh nh3-dev 'booth link ""' # from anywhere else - ``` - - Still put the link in your reply — this is *in addition to*, not instead of. - The reason is simple and the operator raised it directly: links in chat drown - in terminal scrollback and are gone. The board is a **kept** booth (a - `.forever` sentinel exempts it from the 24h sweep) rendered in its own lane at - the top of `http://10.100.10.50:8090/`, so it survives the session that - produced it. Entries carry provenance automatically (`ALTHING_HANDLE` or the - hostname, plus a timestamp), and the board is a plain markdown file the - operator prunes by hand — `booth unkeep links` or deleting the folder ends it. - - Don't post noise: transient junk (a scratch path, a URL only meaningful for - the next two minutes) stays out. If you wouldn't expect him to click it a week - from now, it doesn't belong on the board. Any booth you want to outlive its - 24h can be kept the same way (`booth keep `), but keep that rare — the - ephemeral default is what makes the Booth cleanup-free. - -- **`secret` — Vaultwarden credential store (per dev box)** — a CLI for stashing - and looking up secrets that shouldn't live in a git repo, backed by - **`vaultwarden.phasefinal.com`** (the `infra-ops` org, visible from the operator's - primary account). On nh3-dev the CLI is - `~/development/eshpfi-management/services/secrets-broker/secret` (bw-backed; auth - bootstraps from `~/.config/secrets-broker/bootstrap.env`, `0600`). Usage: - `secret put (--file P | --stdin) [--folder C] [--field k=v]`, - `secret get [--field F] [--file OUT]` (the only command that prints a - value), `secret list [--prefix P]`, `secret backfill [--dry-run]` (scans THIS - box's `~/development/*/{env.sh,.env}` + `~/.config` creds and upserts each). - Items are hostname-namespaced (`/…`); small text → item note, small binary - → base64 hidden field, >6000 B → a bw attachment. **STANDING DIRECTIVE (operator, - 2026-08-11): the vault is the credential source of truth — store durable secrets - in it AND pull the creds you need FROM it.** Any token / API key / cert / env-secret - worth keeping — not ephemeral, doesn't belong in git — should be `secret put` into - the vault so it survives and is findable, never left single-copy on a box; and when - a task needs a credential (e.g. a gitea token to create a repo), **`secret get` it - from the vault** rather than reading an on-disk copy where practical. The one - exception is `bootstrap.env` itself (secrets-zero — it unlocks the vault, so it - can't live inside it; keep it `0600`). **Not a fleet service / no daemon** — each - new dev box duplicates this stack against its own local secrets (setup steps in - the service `README.md`). Details: auto-memory `reference_secrets_broker_cli`. +- **Run `ratecheck` before any expensive or long-running action** + (`~/.claude/bin/ratecheck`; exit `0` GO / `1` PAUSE / `2` UNKNOWN). Under ~85% + proceed; ~85–93% take the cheap path and resume an existing subagent rather than + spawning one; at/over the project threshold stop starting new expensive work; + **UNKNOWN — proceed, loudly, and say plainly that the limits were unreadable.** + A GO is margin, not permission for an arbitrarily large action — one consult + subagent has measured at 270k–650k tokens. +- **POST EVERY OPERATOR-FACING LINK TO THE BOOTH LINK BOARD** (operator, 2026-08-19): + `booth link ""`, in the same turn you mention it, *in + addition to* putting it in your reply. Links in chat drown in terminal scrollback; + the board is kept and survives the session. Don't post transient junk. +- **The vault is the credential source of truth** (operator, 2026-08-11): `secret put` + anything durable so it is never single-copy on one box, and `secret get` the creds a + task needs rather than reading an on-disk copy. ## DevOps delegation diff --git a/home_root/.claude/statusline-command.sh b/home_root/.claude/statusline-command.sh index aa2acce..4e5702a 100644 --- a/home_root/.claude/statusline-command.sh +++ b/home_root/.claude/statusline-command.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash # Claude Code statusline. Layout: -# [📬N] [🔔/🔕] | ⎇ * ↑ | | ctx: | $ | 5h:% 7d:% +# [📬N] [🔔/🔕/📵] | ⎇ * ↑ | | ctx: | $ | 5h:% 7d:% +# +# 🔔 reachable — the post office will push to this session +# 🔕 pull-only — nothing will poke it; mail waits until it looks +# 📵 the post office could not be asked — an OUTAGE, not an empty inbox # ctx% and rate-limit %s are threshold-colored: green <60, yellow 60-90, red >90. # All segments degrade gracefully (missing tool / non-git dir / no handle => segment omitted). input=$(cat) @@ -54,6 +58,21 @@ IFS=$'\x1f' read -r model used_pct input_tok five_pct week_pct cwd fast cost_usd ) [ -z "$model" ] && model="unknown" +# --- persist the rate-limit window for agents (added 2026-09-05, operator directive) --- +# The percentages arrive here on stdin and nowhere else: no CLI subcommand exposes them and +# nothing else on disk carries them, so an agent that must throttle on them cannot see them. +# Written atomically (tmp + mv) so a reader never catches a half-written file. Empty fields +# are preserved as null rather than 0 -- a session with no rate_limits must read as UNKNOWN, +# never as "plenty left", which is the one way this file could cause the harm it prevents. +{ + _rl_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"; _rl_tmp="$_rl_dir/rate-limits.json.$$" + printf '{"five_hour_pct":%s,"seven_day_pct":%s,"five_hour_resets_at":%s,"seven_day_resets_at":%s,"written_at":%s,"written_at_iso":"%s"}\n' \ + "${five_pct:-null}" "${week_pct:-null}" "${five_reset:-null}" "${week_reset:-null}" \ + "$(date +%s)" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$_rl_tmp" 2>/dev/null \ + && mv -f "$_rl_tmp" "$_rl_dir/rate-limits.json" 2>/dev/null + rm -f "$_rl_tmp" 2>/dev/null +} || true + # --- model (compact) + fast-mode flag --- model="${model%% (*}" # "Opus 4.8 (1M context)" -> "Opus 4.8" [ "$fast" = "true" ] && model="⚡$model" @@ -103,30 +122,28 @@ if [ -n "$cwd" ]; then fi fi -# --- althing: unread count (📬 N) + monitor-armed (🔔 armed / 🔕 not) --- +# --- althing: unread count (📬 N) + reachability (🔔 push / 🔕 pull / 📵 outage) --- +# NOT implemented here any more. `althing-statusline` is the canonical segment, +# shipped as a console script by the althing package (3.4.0+). +# +# This block used to be one of THREE hand-maintained copies of the same twelve +# lines -- this file, althing's plugin/scripts/statusline.sh, and infra-ops' +# copy in eshpfi-management. An audit on 2026-09-02 found DIFFERENT defects in +# each and all three were fixed separately, by hand, on the same day. That is a +# drift surface with a countdown on it, and it is the same failure that left +# this very block dead for a month after the v3 cutover deleted the binary it +# gated on. A script, unlike a document, has somewhere to be installed: PATH. +# +# The payload goes in on STDIN -- the segment resolves the handle from the cwd +# it carries, and falls back to nothing rather than guessing. +# +# The outer `timeout` is deliberately LOOSER than the program's own 2s budget. +# If the outer one fired first we would get an empty segment, which reads as +# "not an althing directory" -- the outage conflation, reintroduced by the +# guard meant to prevent a hang. althing=""; mon="" -if command -v althing-cli >/dev/null 2>&1; then - althing=$(althing-cli statusline 2>/dev/null /dev/null) - if [ -n "$h" ]; then - mon="🔕" - # Three independent arming mechanisms, one lock each — any live one means "armed": - # monitor-$h.lock <- `althing-cli monitor` (the CC-native exit-0-re-arm loop) - # wake-listener-$h.lock <- `althing-wake-listener` (herald inbound) - # light-monitor-$h.lock <- `althing-light-monitor` (standing watch) - # The kill -0 liveness gate below discards stale locks, so listing all three is safe. - for lk in "$HOME/.althing/monitor-$h.lock" "$HOME/.althing/wake-listener-$h.lock" "$HOME/.althing/light-monitor-$h.lock"; do - [ -f "$lk" ] || continue - pid=$(tr -dc '0-9' < "$lk" 2>/dev/null) - if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then mon="🔔"; break; fi - done - fi +if command -v althing-statusline >/dev/null 2>&1; then + althing=$(printf '%s' "$input" | timeout 5 althing-statusline 2>/dev/null) fi # --- assemble ---