Hive detects agent status in two stages: internal/domain/terminal/assess (Stage 1, stateless: pane content -> assess.State) feeds internal/domain/terminal/status (Stage 2, stateful: debounces assess.State into a published terminal.Status). This skill is the closed loop for tuning both against a corpus of committed, deterministic recordings — never against live judgment calls that can't be replayed.
Corpus Layout
internal/domain/terminal/assess/testdata/<tool>/<scenario>.txt— single-frame fixtures for Stage 1 rule classification. Table ininternal/domain/terminal/assess/fixtures_test.go.test/calibration/sequences/<name>.jsonl+<name>.expected.json— multi-frame recordings (the{ts, content, title, inMode}shapehive x assess watch --recordproduces) plus a sidecar of the published-status sequencehive x assess replaymust reproduce exactly. Covers Stage 2 debounce timing (idle-confirmation delay, churn-as-working, approval immediacy, ...) that a single-frame fixture can't.test/calibration/scenarios/<name>.yaml— step/expectation scripts for live, in-container runs against a real pane (hive x assess scenario). Not wired intomise run cli:integration; run by hand.
The Tuning Loop
-
Find mismatches. Run the automated guardrail:
go test ./cmd/hive/internal/commands/... -run TestCalibrationCorpus_ReplayMatchesExpected -vThis replays every
test/calibration/sequences/*.jsonlthrough a freshassess.Engine+status.Trackeron a virtual clock (never wall time, never an ambient config file —status.DefaultOptions()only) and diffs the published-status sequence against its.expected.jsonsidecar. A failing subtest names the sequence and the exact frame index where the sequences diverge.
For a single sequence, hive x assess replay <frames.jsonl> --jsonl prints the full per-frame decision trail (state, rule ID, hold, published, candidate, candidatePolls, churned) — the same information the test compares, but human-readable.
-
Diagnose with
file. Isolate the offending frame's raw content into its own file and run:hive x assess file <frame.txt> --tool claudeThis shows which rule fired (or didn't) and dumps the parsed regions (aboveBox, promptBoxBody, bottomLines, afterLastRule) so you can see exactly what the rule matcher saw. Compare "which rule fired" against "which rule should have fired."
-
Edit the rule sets or debounce options.
- Stage 1 classification bugs (wrong state, wrong hold):
internal/domain/terminal/assess/rules_claude.go,rules_codex.go,rules_generic.go,rules_common.go(shared matchers). - Stage 2 timing bugs (flapping, premature/late confirmation):
internal/domain/terminal/status/options.go'sDefaultOptions()(ConfirmIdle,ConfirmApproval,ChurnWindow) ordebounce.go's transition table.
- Stage 1 classification bugs (wrong state, wrong hold):
-
Re-run everything — no regression trading.
mise run test # the entire fixture + calibration corpus, not just the sequence you were fixingHARD GUARDRAIL: a tuning change that fixes one scenario by breaking any other committed fixture or sequence is rejected. If your fix regresses something else, the rule or debounce policy is under-specified — narrow the match condition or add a distinguishing signal instead of trading one failure for another. Do not edit or delete an existing fixture/sidecar to make it agree with new behavior unless you have independently verified the old expectation was wrong.
-
Promote the fix to a permanent fixture. Every mismatch you resolve must leave a new committed regression test behind, or the next tuning pass can silently re-break it:
- Single-frame classification bug -> new file under
assess/testdata/<tool>/plus a row infixtures_test.go. - Multi-frame timing bug -> new
test/calibration/sequences/<name>.jsonl+ hand-verified<name>.expected.json(verify withhive x assess replay, exactly as described in step 1).
- Single-frame classification bug -> new file under
Optional Live Tier (Container-Only)
Everything above is offline (fixture files, recorded frames) and safe anywhere, including this host. The live tier's mutating commands are only safe inside mise run cli:container; watch, including watch --record, remains pane-read-only and may run on the host when its local capture file is handled as sensitive data:
mise run cli:container
# inside the container:
hive x assess drive test/calibration/sequences/claude-turn-lifecycle.jsonl --target <pane>
hive x assess scenario test/calibration/scenarios/claude-permission-flow.yaml --target <pane>
hive x assess watch <pane> --tool claude --record new-sequence.jsonl
drivereplays a recorded sequence into a real pane by runningtmux respawn-pane -k, which kills and replaces the pane's current process. It then updates the pane title for each frame. This exercises the truecapture-pane -> list-panes -> assesspath with no agent credentials, but it is destructive to the target pane.scenariodrives a pane through a scripted YAML ofsend/key/expectsteps and scores the result: a JSON report with per-expectation pass/fail + detection latency in polls, and a scenario-level flap count — published transitions the scenario steps didn't imply. Flap count is the headline metric the entire debounce design exists to drive to zero; watch it even on scenarios whose expectations all pass.watch --recordcaptures a fresh sequence from a real agent CLI (documented option only, no tooling provided) for promotion into the corpus per step 5. Pass--tool(mirrorsreplay's--tool/-t; empty auto-detects per frame viaterminal.DetectTool): auto-detection reads the tool's identifying text out of the captured content itself, so once that text scrolls out of the visible pane (a long-running session, or a banner near the top), it silently misdetects — e.g. a codex pane with its banner scrolled away detects as "shell" and the codex rule set never runs, so every frame assessesunknown. Pin--toolfor any real calibration run.
Hard Rules
- Anything that mutates a pane runs only inside
mise run cli:container.drivekills and replaces the pane process withrespawn-pane -k;scenariosends real keys. Running either against the host's tmux server has crashed dev environments before (see the repo'sCLAUDE.md"Integration Tests" rule, which this inherits). Both commands require Docker's marker plus the isolation marker set by the repository'smise run cli:containertask; an arbitrary Docker container fails closed by default, regardless of the tmux socket's name, unless you pass--allow-host. --allow-hostis a deliberate, eyes-open exception only. A named socket does not prove isolation, so verify the target yourself before overriding the container gate.hive x assess watchis pane-read-only and host-safe — with or without--record, it only callscapture-pane/display-messageand never sends input. Recording writes the captured pane bytes to a private local file, which can contain source, output, paths, and secrets.hive x assess fileandhive x assess replayare pure offline file processing and always host-safe.- No regression trading (repeated from step 4 because it's the rule most tempting to skip under time pressure): a green corpus after your change must be a strict superset of the green corpus before it, plus your fix.