feat(detect): agent-agnostic turn-end status contract (MAT-125) - #39
Merged
Conversation
Surface the closing block as three independent channels -- blocking gates, running agents, idle -- instead of collapsing them into one screen-scraped status. Screen scraping sees Claude's prompt box the instant a turn ends and calls the pane idle (manifests/claude.toml live_prompt_box, priority 950). That is right about the harness and wrong about the work: agents may still be running, or a Gate may be waiting on a human. Adds the closing-block sources to full_lifecycle_hook_authority so the Stop hook outranks the prompt box, with a regression test asserting the authority does not outlive the agent process. Demo harness under demo/.
Replace prose-shaped reporting with a structured payload any coding agent can
emit from its own turn-end hook:
{"v":1,"agent":"claude","blocking":2,"agents":3,"gates":[...]}
blocking (items only a human can clear) and agents (still working) are
independent, so "nothing for you, but 3 agents are still running" becomes
visible for the first time.
The hook is the trigger, not the model: a turn-end hook fires deterministically,
a tool call the model must remember does not. Transport is the existing herdr
socket, with an atomic JSON mirror under XDG_STATE_HOME as durable fallback.
is_closing_block_source admits any herdr:<agent>-closing-block whose suffix
names the agent it speaks for, so adopting the contract needs no herdr release.
hook_authority_is_effective still requires that agent's process to be present.
Adapters for Claude (Stop) and Codex (notify), plus a dependency-free CLI.
The turn-end status contract was accepted at the RPC edge and then dropped
inside TerminalState, while the caller still saw {"type":"ok"} because
handle_pane_report_agent discards the mutation.
Two gates rejected it, both keyed on session identity:
- route_full_lifecycle_hook_report requires an AgentSessionRef, but
agent_resume::session_ref_from_report mints one only for sources on the
is_official_agent_source allowlist. A closing-block source reports what the
agent just said, not where to resume it, so its ref is always None and every
admitting path was unreachable.
- set_hook_authority_at rejected the report as an owner conflict on any pane
where the real agent had already announced itself under herdr:<agent> -- that
is, every pane that matters.
Accept on process presence instead, which is the only guarantee this source
claims; accept_hook_report still enforces per-source sequence monotonicity.
The existing test passed a session ref the RPC can never produce, so it stayed
green while the live path dropped every report. It now reports with None
against a pane whose session is owned by herdr:claude, and fails without the
fix.
Verified end-to-end against a live claude process: blocked survives the visible
prompt box, repeated detection ticks, and focusing the tab.
Sidebar labels are now "gate" and plain "working". One gate or three, the action is the same; running agents just mean "not done", which the working dot already says. The counts still ride along in the closing_* tokens. Adds INSTALL.md with the wiring verified live for both agents, including the two traps: --settings never loads hooks, and project-scoped hooks are silently skipped until the folder is trusted.
Codex sets its terminal title to the cwd and its rollout files carry no title, so a codex pane has nothing to name it by. The notify handler now derives one from the first turn's user message -- the same thing codex's own resume picker previews -- and pins it in the mirror so later turns do not rename the pane. Rides along as a `session_title` metadata token.
A full-lifecycle source that stays silent leaves its last report standing forever, so a pane that reported a gate kept showing it with nothing able to clear it.
Use recent PTY output as a bounded fallback when Codex has no positive idle or blocker evidence. Codex's non-empty OSC cwd title still resolves Idle but no longer sets visible_idle, so pending-idle confirmation and blocker arbitration can observe the difference. Claude's live prompt box and blocker rules remain unchanged.
Verified against live `agent.read` captures of a codex 0.147 pane: a real turn prints no block marker, so the block-marker region emptied the rule and the only working evidence codex has stopped firing.
source commit: 8605133
A live codex 0.147 turn goes silent for 3.6s between erasing its progress line and the first streamed token; a two-second window showed that gap as `done` mid-turn.
Owner
Author
|
Pull request: #39 Terminal contract readyThis change has no web preview. Run the named repository-owned nonvisual journeys; each emits a head-bound local assertion artifact. All five repository-owned nonvisual journeys passed head-bound automated assertions (cargo nextest suites) at this exact head. |
Owner
Author
|
No description provided. |
This was referenced Aug 9, 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.
Summary
Stopand Codexnotifyadapters through the agent-agnosticherdr:<agent>-closing-blocksource contractblockingandagentschannels and writes the durable atomic mirror at~/.local/state/herdr/agent-status/<pane-id-with-colons-replaced-by-underscores>.jsonAgentStatus::Donefor non-Codex agents by limiting the measured recent-output fallback to CodexSplit from #31
The sidebar grouping/pinning, dock/editor, compact worklist-row, mouse, and unrelated input churn is excluded from this PR. All 15 UI-assigned source nodes are preserved in draft PR #38 (
mat125/ui-churn-split). PR #31 remains open and untouched for planner cleanup.Acceptance criteria
blocking/agentschannelswork_contextmicrobenchmark, which reproduces alone at 1.04–1.79s and is excluded from the final local pass; exact-head CI is requiredMAT-125/[no-ticket]Local verification
python3 -m pytest -q demo/test_closing_block.py— 7 passedAgentStatus::Donetests — greenjust check-parallelwith Zig 0.15.2 — contract failure found and fixed; repeated runs exposed only unchanged baseline timing/load flakeswork_context::tests::ac25_preview_url_extraction_rejects_large_whitespace_free_input_quickly— lint, 3,487 Rust tests, Windows lint, maintenance, integrations, marketplace green