Skip to content

feat(hub): pane_explain — why the fleet thinks an agent is idle/working/blocked (P4) - #542

Merged
physercoe merged 1 commit into
mainfrom
feat-panestate-p4
Aug 10, 2026
Merged

feat(hub): pane_explain — why the fleet thinks an agent is idle/working/blocked (P4)#542
physercoe merged 1 commit into
mainfrom
feat-panestate-p4

Conversation

@physercoe

Copy link
Copy Markdown
Owner

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 --file mode: 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.

GET on 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 as false; 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 vendor or overlay, the screen's size, and the OSC title (empty means every osc_title rule 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_lines let 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:

Mutation Why it first survived
delete the agent-token refusal the test saw 403 — from a different layer. auth.Middleware allowlists bearer kinds and rejects agents outright, so my route test never reached my guard.
delete the mode-exclusivity check the "both fields" fixture omitted family, so it 400'd for a missing family instead.
delete the winner-first sort the fixture's winner was also the top-priority rule, so priority order alone produced the same list.

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. TestPaneExplainRefusesAgentTokensInProcess reaches it by marking the context the way the self-call does.

Smaller notes

  • P1 had already built the evidence side. Explain has 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.
  • One type, not two. Explain gained 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 — so TestExplainWireKeysAreDeliberate pins the exact key set. Adding a field is fine; adding it silently is not.
  • Live mode ignores the capture gate and the startup grace. Both exist to skip work nobody asked for; this call is the asking.
  • datasetVerbJSON/datasetVerbErrorverbJSON/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.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 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.
  • Desktop 805/805, npm run typecheck clean, full lint set (12 runnable) clean, token ratchet unchanged (65 baseline, 0 phantom).
  • All 37 CSS classes the two new components emit are checked against the stylesheet — no phantom classes.
  • New how-to: debug-pane-state.md, indexed in docs/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

…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>
@physercoe
physercoe merged commit c6da369 into main Aug 10, 2026
8 checks passed
@physercoe
physercoe deleted the feat-panestate-p4 branch August 10, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant