1@README.md 2 3- Keep this shellcheck-clean (`nix develop -c shellcheck tools/watch/*.sh`, 4 per `~/.claude/rules/code-bash.md`) and re-run `--self-test` after any 5 change to the parsing functions - a script that grows past the rule's size 6 ceiling or gains real branching moves to `tools/hotpatch`'s Rust tier, not 7 into a longer bash file; this one has stayed under it by keeping ALL 8 parsing/formatting logic in small, individually-testable functions rather 9 than inline in `main`. 10- **A variable set inside a function called via `$(...)` is invisible to the 11 caller the instant the subshell exits.** `parse_tool_response`/`mcp_tool` 12 report their failure reason through `last_error` (a file, `set_error` 13 writes it), not a plain variable, for exactly this reason - a first version 14 used a plain `TOOL_ERROR` variable and it worked in every self-test case 15 (each one calls the function directly, no subshell) and failed for real 16 against the live window ("TOOL_ERROR: unbound variable") the first time a 17 caller wrote `budget="$(mcp_tool budget)"`. **Any test that only exercises 18 a function directly, never through `$(...)`, does not exercise this path - 19 the self-test's "missing tool" cases are written through `parse_tool_response` 20 directly for readability, so they would NOT have caught this by themselves. 21 What did catch it was running the script for real against the live window, 22 which is why that stays part of verifying a change here, not a nice-to-have.** 23- **The MCP stream's first SSE event can be a bare keepalive** - `data: ` 24 with nothing after it, `id: 0`, `retry: 3000` - found for real against the 25 live window 2026-09-20. `grep -m1 '^data: '` (no trailing `{`) matches that 26 line and returns nothing to parse; `^data: {` is what skips it, the same 27 pattern `../hotpatch/patch.sh`'s `mcp_raw` already used. `--self-test`'s 28 "leading keepalive" case is the regression test for this. 29- **Every field this prints goes through `get`, which returns the literal 30 string `"unknown"` for a missing OR a null field - never a number, never 31 `0`, never `false`.** `jq -e '.foo'` alone cannot tell "missing" apart from 32 "`false`" (both are falsy to `-e`), which is exactly the bug a watchdog 33 cannot afford: `stopped_for_good` being unreadable must never print as 34 `stopped=no`. 35 Adding a new printed field means adding it through `get`/a `format_*` 36 wrapper, never a bare `jq -r '.foo // 0'`. 37- If a new field is added to the `budget` or `state` MCP tool, it does not 38 need to be added HERE - this prints a fixed, small set of fields for a 39 glance, not everything the tools carry. `budget.sh --json` does not exist 40 on purpose; a caller that wants the whole object calls the tool itself. 41- **`format_lifetime` reads `lifetime_remaining_usd`, which the `budget` tool 42 no longer has** (the lifetime cap was removed 2026-09-22; the field is now 43 `lifetime_usd`, spent so far). Against a live window it prints `unknown`, 44 as `get` promises. The self-test's fixtures still carry the old field, so 45 they pass; a fix changes both the script and its fixtures together.