feat(hub): declarative pane-state detection + generic pane-input hardening (P1, Q1) - #534
Merged
Conversation
…ening (P1, Q1)
Pane-state-manifests plan, first wave. P1 is the core evaluator as a pure
library (D-8: teeth before wiring); Q1 is the smallest independent.
## P1 — hub/internal/panestate
`idle.go` is one regex plus a 90-second stall, and it is the ONLY state
signal for every engine without a structured M4 adapter — codex parked on
"Allow command?" raises nothing today. This makes screen-state detection
declarative: an evaluator for herdr's manifest schema plus its 19 per-agent
TOMLs, vendored byte-exact at 6f311498 (Apache-2.0, NOTICE updated) and
pinned by git blob SHA so a re-vendor diffs cleanly. Rules are data, the
ADR-010 frame-profile shape — a new engine is a TOML, not a Go adapter.
Everything of ours is in manifests/overlay/, including the family mapping:
claude-code, codex, antigravity, gemini-cli are mapped. kimi-code-ts is
deliberately NOT, for the same reason it is unmapped in the resume recipe
table — upstream detects a CLI it calls `kimi` and nobody has confirmed that
is our compiled-TypeScript one. Screen rules are more version-fragile than a
resume flag, so the bar is a real capture, not a name match. An unmapped
family gets no evaluation at all; classifying an engine with another
engine's rules is confident wrong attention, worse than today's silence.
Two porting calls that needed judgement, recorded rather than smoothed:
1. Rust-regex and RE2 are not the same dialect. 9 of the 58 vendored
patterns do not compile in Go — 5 use \uXXXX escapes, 4 use
\p{Alphabetic}, a Unicode BINARY property RE2 lacks. Editing the
vendored files is forbidden (D-1) and dropping the rules would be a
silent hole, so regex_translate.go rewrites at compile time and RECORDS
each translation, flagging the inexact one: \p{Alphabetic} becomes
[\p{L}\p{Nl}], which drops Other_Alphabetic. Every vendored use is
`\p{Alphabetic}+\w*ing\b` after a spinner glyph so the dropped set is
unreachable there — but that is a claim about the manifests as they are
today, so it is labelled, not assumed.
2. An unimplemented region is REFUSED, not emptied. Upstream resolves an
unknown region to "", which turns a typo or a newer-schema region into a
rule that silently never fires — the exact failure class this plan
exists to remove. Only the 8 region kinds the corpus uses are
implemented; the rest fail validation by name. That is the plan's risk 3
working as intended.
Same reasoning for unknown TOML keys: a manifest carrying a key this
evaluator does not implement is an error, because an unread key is a rule
that never fires.
## Q1 — generic pane input
PaneDriver serves every engine without an adapter, and it sent a multi-line
body with one `send-keys -l`, so the pane submitted at the first newline and
the rest arrived as separate turns. The per-engine adapters were fixed for
exactly this; the fallback path never was. It now takes the same
named-buffer paste with a single explicit Enter.
All four paste paths gained `-p`. Verified against tmux(1) rather than the
discussion's paraphrase: "paste bracket control codes are inserted around
the buffer if the application has requested bracketed paste mode" — so a TUI
that asked for it stops line-editing the body and one that did not is
unaffected. The generic path also waits 300 ms before Enter so the TUI has
ingested the paste. Short single-line input keeps its original two-call
shape, so no existing interaction changed.
The half-stubbed-seam guard is deliberate: a harness that stubs SendKeys but
not Tmux gets a refusal, never a fall-through to a real `tmux` exec. On a
developer box that server is the one the human is sitting in.
## Verification
Corpus is 14 of upstream's OWN test screens with upstream's OWN expected
answers (claude, codex, devin), so it is a cross-implementation parity check
rather than a self-consistency one. Building it turned up an extraction bug
worth naming: reading the nearest following assertion attributed one
screen's expected rule id to the next screen in the same test function; the
window is now bounded to the current call.
Coverage is narrower than the plan's line implies and the PR says so: the
other 16 manifests get parse + validate + compile + empty-screen fallback
only. Per-agent blocked/working screens need real captures — inventing them
from the rules under test would prove nothing.
Mutation-checked: flipping the tie-break to last-wins fails the named
tie-break test; making `contains` case-sensitive fails 10 tests including
corpus cases; dropping `-p` fails the flag test; reverting the multi-line
path to one send-keys fails the block test.
New dependency: github.com/BurntSushi/toml v1.4.0 (BSD-2, zero deps,
go 1.18 so it clears this module's 1.23 pin). D-1's byte-exact vendoring
requires parsing upstream's format.
Full `go test ./...` exit 0; 11 runnable CI lints pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ust the single-signal ones The corpus claimed to be a cross-implementation parity check, but the first cut took only upstream's single-signal tests (one screen or one OSC string) and skipped 12 eligible test functions — all the multi-signal ones: blocker-outranks-working-fallback, weak blocker, transcript-viewer freeze (state=unknown + skip), background-terminal noise, OSC-working-beats-screen and the three stale-"Working"-text screens. Those are exactly the priority/region interplay cases where a port diverges first, and the ones P3's attention will lean on. All 14 added cases pass unchanged — transcribed verbatim from herdr src/detect/manifest/tests.rs at the pinned commit, answers included. Doc counts updated (14 -> 28) and the corpus note records the extension; the plan's region-kind count now names bottom_lines(N) as implemented-but-unused-by-vendor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 8, 2026
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.
Pane-state-manifests plan, first wave — P1 (the core evaluator, pure library) and Q1 (the smallest independent).
P1 —
hub/internal/panestateidle.gois one regex plus a 90-second stall (idle.go:25,49,55), and it is the only state signal for every engine without a structured M4 adapter. codex parked on "Allow command?" raises nothing today.This makes detection declarative: an evaluator for herdr's manifest schema plus its 19 per-agent TOMLs, vendored byte-exact at
6f311498(Apache-2.0, NOTICE updated) and pinned by git blob SHA so a re-vendor diffs cleanly. Rules are data — the ADR-010 frame-profile shape — so a new engine is a TOML, not a Go adapter. No wiring (D-8, teeth before wiring).Everything of ours is in
manifests/overlay/, including the family mapping.claude-code,codex,antigravity,gemini-cliare mapped;kimi-code-tsdeliberately is not — upstream detects a CLI it callskimiand nobody has confirmed that is our compiled-TypeScript one. Screen rules are more version-fragile than a resume flag, so the bar is a real capture, not a name match. An unmapped family gets no evaluation: another engine's rules would produce confident wrong attention, which is worse than today's silence.Two porting calls that needed judgement
1. Rust-regex and RE2 are not the same dialect. 9 of the 58 vendored patterns don't compile in Go — 5 use
\uXXXXescapes, 4 use\p{Alphabetic}, a Unicode binary property RE2 lacks. Editing the vendored files is forbidden (D-1) and dropping the rules would be a silent hole, soregex_translate.gorewrites at compile time and records each translation, flagging the inexact one:\uXXXX/\u{XXXX}\x{XXXX}\p{Alphabetic}[\p{L}\p{Nl}]Every vendored use is
\p{Alphabetic}+\w*ing\bafter a spinner glyph, so the dropped set is unreachable there — but that's a claim about the manifests as they are today, so it's labelled rather than assumed, and a test fails if the inexact flag disappears.2. An unimplemented region is refused, not emptied. Upstream resolves an unknown region to
"", which turns a typo or a newer-schema region into a rule that silently never fires — the exact failure class this plan exists to remove. Only the 8 region kinds the corpus uses are implemented; the rest fail validation by name. That's the plan's risk 3 working as intended. Same reasoning for unknown TOML keys: an unread key is a rule that never fires, so it's an error.Q1 — generic pane input
PaneDriverserves every engine without an adapter, and it sent a multi-line body with onesend-keys -l— so the pane submitted at the first newline and the rest arrived as separate turns. The per-engine adapters were fixed for exactly this; the fallback path never was. It now takes the same named-buffer paste with a single explicit Enter.All four paste paths gained
-p. Verified againsttmux(1)rather than the discussion's paraphrase: "paste bracket control codes are inserted around the buffer if the application has requested bracketed paste mode" — so a TUI that asked for it stops line-editing the body, and one that didn't is unaffected. The generic path also waits 300 ms before Enter so the TUI has ingested the paste. Short single-line input keeps its original two-call shape, so no existing interaction changed.One guard worth a look: a harness that stubs
SendKeysbut notTmuxgets a refusal, never a fall-through to a realtmuxexec. On a developer box that server is the one the human is sitting in.Verification
The corpus is 14 of upstream's own test screens with upstream's own expected answers (claude, codex, devin), so it's a cross-implementation parity check rather than a self-consistency one. Building it turned up an extraction bug worth naming: reading the nearest following assertion attributed one screen's expected rule id to the next screen in the same test function — the window is now bounded to the current call.
Coverage is narrower than the plan's line implies, and I'd rather say so than imply otherwise. The other 16 manifests get parse + validate + compile + empty-screen fallback only. Per-agent blocked/working screens need real captures; inventing them from the rules under test would prove nothing. That's device-verify debt the plan already books.
Mutation-checked:
containsmade case-sensitive → 10 tests fail, including corpus cases-pdropped → flag test failssend-keys→ block test failsFull
go test ./...exit 0; 11 runnable CI lints pass;gofmt/vetclean.New dependency
github.com/BurntSushi/tomlv1.4.0 — BSD-2, zero transitive deps,go 1.18so it clears this module's 1.23 pin (the same constraint that keepsparquet-goat v0.25.0). D-1's byte-exact vendoring requires parsing upstream's format; I picked the smaller of the two candidates.🤖 Generated with Claude Code