feat(hub): pane_explain — why the fleet thinks an agent is idle/working/blocked (P4) - #542
Merged
Conversation
…ng/blocked (P4)
Wedge P4 of docs/plans/pane-state-manifests.md. P2 made screen rules
decide an agent's state and P3 made a blocked one raise attention; when
either looks wrong there was no way to ask why. Now there is, and the
answer is the whole evaluation rather than the verdict: every rule,
whether it matched, what it was looking for, and a bounded preview of
the region it read.
Two modes behind one route. `{agent_id}` captures that agent's pane on
its host and evaluates there — the screen never crosses to the hub.
`{family, screen}` evaluates text the caller already has: upstream's
`--file` mode, which needs no host, works on a screen pasted out of a
bug report, and is the half CI can exercise. They are exclusive, because
"what is that agent doing" and "what would the rules say about this
text" are different questions and a body with both would have to pick
one silently.
`GET` on the same route lists which families have a manifest at all —
D-3's table, previously readable only by opening a YAML compiled into
the binary, and usually the answer to "why is this agent never
classified". Unmapped families are absent rather than listed as false.
Desktop Inspect gets a card: Open → "Explain an agent pane…", or press
"Read as pane state" on a pasted screen. Matched and unmatched rules are
distinguished by a glyph as well as by colour, and each rule expands to
what it wanted and what it read. Parsing is a pure, unit-tested module;
only the layout is unverified.
Notes worth keeping:
- P1 had already built the evidence side — `Explain` has carried
per-rule outcomes with bounded previews since the first wedge. P4 is
transport and surface, not evaluation.
- The record is ONE type: `Explain` gained json tags rather than growing
a parallel wire struct. The price is that a field added for evaluation
would ship by default, so a test pins the exact key set — adding a
field is fine, adding it silently is not.
- Agent-kind tokens are refused (403), and the comment names the layer
it backs up rather than assuming it. I had written that the egress
proxy's lack of a path allowlist let an agent bearer reach this route;
a mutation proved otherwise — `auth.Middleware` allowlists bearer
kinds and rejects agents outright. But that allowlist EXEMPTS the
hub's in-process authority dispatch, which is where an MCP tool
dispatching here would arrive, so the handler guard is real and now
has the direct test that reaches it.
- Live mode ignores the capture gate and the startup grace: both exist
to skip work nobody asked for, and this call is the asking.
- `datasetVerbJSON`/`datasetVerbError` → `verbJSON`/`verbError`; they
were never dataset-specific and P4 is the second caller.
A gap found and deliberately not closed: the state-authority order
(structured driver > screen manifest > nothing) lives only in this
plan's D-2. `spine/agent-lifecycle.md` is an axiom doc that predates
screen rules and does not mention them, so the fleet's real answer to
"how is an agent's state decided" is not in the spine. Recorded rather
than patched inside a feature wedge.
Verification: full `go test ./...` green (33 packages); desktop 805/805
and typecheck clean; full lint set clean; 10 mutations introduced and
all 10 caught — three of them only after the mutation pass exposed
tests that were passing for the wrong reason (a 403 from the wrong
layer, a 400 from a missing field, a winner that was also the
top-priority rule).
Not verified: the Inspect card has never been rendered — no display
here. It joins this lane's standing device-verify debt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wedge P4 of
docs/plans/pane-state-manifests.md. P2 made screen rules decide an agent's state; P3 made a blocked one raise attention. When either looked wrong there was no way to ask why — the rules are 58 matchers across 19 vendored TOMLs and the only observable was the verdict. Now you can ask, and the answer is the whole evaluation.Two modes, one route
POST /v1/teams/{team}/pane_explain{agent_id}— the owning host captures that pane and evaluates it there. The screen never crosses to the hub; what comes back is the record.{family, screen}— evaluate text you already have. Upstream's--filemode: no host, no pane, no agent. It works on a screen pasted out of a bug report, it is how you check a rule change before shipping it, and it is the half CI can actually exercise.Exclusive, not layered: "what is that agent doing" and "what would the rules say about this text" are different questions, and a body with both would have to pick one silently.
GETon the same route is D-3's coverage table — which families have a manifest at all. Previously that was readable only by opening a YAML compiled into the binary, and it is usually the answer to "why is this agent never classified". Unmapped families are absent, not listed asfalse; a row would imply a manifest exists.What the record contains
Every rule, not just the winner — with what it wanted and a bounded preview of the region it read. Without that preview, "did not match" is a claim the reader cannot check. Plus the family→manifest mapping used, the manifest's version and whether it is
vendororoverlay, the screen's size, and the OSC title (empty means everyosc_titlerule read nothing and could not have fired — which otherwise reads as "those rules are broken").Deliberately absent: the screen. Per-rule previews are bounded at 240 chars;
screen_bytes/screen_lineslet you see the pane was 60 rows without shipping 60 rows.Desktop
Inspect → Open → “Explain an agent pane…” lists running agents with a pane; engines with no manifest stay visible and disabled with the reason, because hiding them would leave the reader guessing. For the pasted mode, press “Read as pane state” on a scratch tab. Matched and unmatched rules differ by a glyph as well as a colour; each expands to what it wanted and what it read.
The mutation pass earned its keep
10 mutations, 10 caught — but three only after the pass exposed tests passing for the wrong reason:
auth.Middlewareallowlists bearer kinds and rejects agents outright, so my route test never reached my guard.family, so it 400'd for a missing family instead.The first one corrected a premise I had written into the code, the plan and the changelog: I claimed the egress proxy's lack of a path allowlist let an agent bearer reach this route. It does forward every path — but the bearer is refused by kind before routing. I had checked two layers of three.
The guard stays, and is now honest about what it backs up. That allowlist deliberately exempts
isInProcessDispatch— the hub's own authority-tool self-call, where an agent token is the legitimate credential. The day someone adds an MCP tool that dispatches here, the network guard is not in the path and this one is all that is left.TestPaneExplainRefusesAgentTokensInProcessreaches it by marking the context the way the self-call does.Smaller notes
Explainhas carried per-rule outcomes with bounded previews since the first wedge ("every rule every pass — explain needs the evidence"). P4 is transport and surface, not evaluation.Explaingained json tags rather than growing a parallel wire struct. The price is that a field added for evaluation ships by default — to a payload that already carries pane text — soTestExplainWireKeysAreDeliberatepins the exact key set. Adding a field is fine; adding it silently is not.datasetVerbJSON/datasetVerbError→verbJSON/verbError. Never dataset-specific; P4 is the second caller.A gap found, not closed
The state-authority order — structured driver > screen manifest > nothing — exists only as this plan's D-2.
spine/agent-lifecycle.mdis an axiom doc that predates screen rules and does not mention them, so the fleet's real answer to "how is an agent's state decided" is not in the spine. Recorded in the plan rather than patched inside a feature wedge; an axiom edit deserves its own pass.Verification
go test ./...green — 33 packages, zero failures.npm run typecheckclean, full lint set (12 runnable) clean, token ratchet unchanged (65 baseline, 0 phantom).debug-pane-state.md, indexed indocs/README.md.Not verified
The Inspect card has never been rendered. No display on this machine. The parsing layer is pure and unit-tested (10 tests), the classes are checked against the stylesheet, and everything past that is unproven — it joins this lane's standing device-verify debt, alongside "no engine has been observed live".
Plan: pane-state-manifests P4. Follows #541 (P3), #536 (P2), #534 (P1+Q1), #533 (N1), #532 (plan review).
🤖 Generated with Claude Code