Skip to content

feat(hub): declarative pane-state detection + generic pane-input hardening (P1, Q1) - #534

Merged
physercoe merged 2 commits into
mainfrom
feat-panestate-p1-q1
Aug 8, 2026
Merged

feat(hub): declarative pane-state detection + generic pane-input hardening (P1, Q1)#534
physercoe merged 2 commits into
mainfrom
feat-panestate-p1-q1

Conversation

@physercoe

Copy link
Copy Markdown
Owner

Pane-state-manifests plan, first wave — P1 (the core evaluator, pure library) and Q1 (the smallest independent).

P1 — hub/internal/panestate

idle.go is 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-cli are mapped; kimi-code-ts deliberately is not — 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: 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 \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:

from to exact?
\uXXXX / \u{XXXX} \x{XXXX} yes
\p{Alphabetic} [\p{L}\p{Nl}] no — 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'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

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 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 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

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:

  • tie-break flipped to last-wins → named tie-break test fails
  • contains made case-sensitive → 10 tests fail, including corpus cases
  • -p dropped → flag test fails
  • multi-line path reverted to one send-keys → block test fails

Full go test ./... exit 0; 11 runnable CI lints pass; gofmt/vet clean.

New dependency

github.com/BurntSushi/toml v1.4.0 — BSD-2, zero transitive deps, go 1.18 so it clears this module's 1.23 pin (the same constraint that keeps parquet-go at 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

Ubuntu and others added 2 commits August 8, 2026 10:07
…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>
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.

2 participants