For agents, on top of README.md, which they read first.
- Keep this shellcheck-clean (
nix develop -c shellcheck tools/watch/*.sh, per~/.claude/rules/code-bash.md) and re-run--self-testafter any change to the parsing functions - a script that grows past the rule's size ceiling or gains real branching moves totools/hotpatch's Rust tier, not into a longer bash file; this one has stayed under it by keeping ALL parsing/formatting logic in small, individually-testable functions rather than inline inmain. - A variable set inside a function called via
$(...)is invisible to the caller the instant the subshell exits.parse_tool_response/mcp_toolreport their failure reason throughlast_error(a file,set_errorwrites it), not a plain variable, for exactly this reason - a first version used a plainTOOL_ERRORvariable and it worked in every self-test case (each one calls the function directly, no subshell) and failed for real against the live window ("TOOL_ERROR: unbound variable") the first time a caller wrotebudget="$(mcp_tool budget)". Any test that only exercises a function directly, never through$(...), does not exercise this path - the self-test's "missing tool" cases are written throughparse_tool_responsedirectly for readability, so they would NOT have caught this by themselves. What did catch it was running the script for real against the live window, which is why that stays part of verifying a change here, not a nice-to-have. - The MCP stream's first SSE event can be a bare keepalive -
data:with nothing after it,id: 0,retry: 3000- found for real against the live window 2026-09-20.grep -m1 '^data: '(no trailing{) matches that line and returns nothing to parse;^data: {is what skips it, the same pattern../hotpatch/patch.sh'smcp_rawalready used.--self-test's "leading keepalive" case is the regression test for this. - Every field this prints goes through
get, which returns the literal string"unknown"for a missing OR a null field - never a number, never0, neverfalse.jq -e '.foo'alone cannot tell "missing" apart from "false" (both are falsy to-e), which is exactly the bug a watchdog cannot afford:stopped_for_goodbeing unreadable must never print asstopped=no. Adding a new printed field means adding it throughget/aformat_*wrapper, never a barejq -r '.foo // 0'. - If a new field is added to the
budgetorstateMCP tool, it does not need to be added HERE - this prints a fixed, small set of fields for a glance, not everything the tools carry.budget.sh --jsondoes not exist on purpose; a caller that wants the whole object calls the tool itself. format_lifetimereadslifetime_remaining_usd, which thebudgettool no longer has (the lifetime cap was removed 2026-09-22; the field is nowlifetime_usd, spent so far). Against a live window it printsunknown, asgetpromises. The self-test's fixtures still carry the old field, so they pass; a fix changes both the script and its fixtures together.