Skip to content

feat(hooks): stop control on every host; once-per-session raw-git nudges - #258

Merged
BrainerVirus merged 10 commits into
mainfrom
feature/stop-control
Oct 9, 2026
Merged

BrainerVirus merged 10 commits into
mainfrom
feature/stop-control

Conversation

@BrainerVirus

Copy link
Copy Markdown
Owner

Why

Host parity audit gap 3: no host blocked an agent from stopping with work provably unfinished. The user chose option A (ledger decision mv097wco): nudge once.

What

Stop control on all five hosts, from one core policy (hooks/stop.ts).

  • A stop is continued at most once per turn, and only for an obligation the ledger or local git proves:
    • (a) a commit on the session's branch that isn't on its upstream, when the endpoint is pr|green|merged;
    • (b) an open PR whose checks were last seen failing or pending, when the endpoint is green|merged;
    • (c) a delivered head with only a self verdict, or none.
  • The reason names the obligation and the workit command that clears it.
  • It is never blocked when the last message asks the user a question, in a subagent, outside a Workit workspace, or on the second stop. It fails open.
  • Native per host:
    • Claude Code and Codex: Stop → decision: block (stop_hook_active).
    • Cursor: stop → followup_message, registered with loop_limit: 1.
    • Pi: agent_end plus sendMessage({triggerTurn}).
    • OpenCode: session.idle plus session.synthetic({resume}). This is partial, as recorded in hosts.md.

Obligation (b) data: workit pr status and workit ci wait append a CLI-observed pr.status row (pr, head, checks) when the state changes. ledger list summarizes it.

Fixes from the ui-kit session audit and earlier reviews:

  • Raw-git nudges fire once per kind per session, and never on a command that already is a workit verb (ui-kit saw 63/36/27 repeats). A latent Pi/OpenCode pre-tool nudge spend is fixed.
  • The skill prompt nudge no longer fires on relayed agent messages (subagent hand-backs or notifications delivered via UserPromptSubmit).
  • Claude Code worktree-isolated subagents are told up front to use plain git, never workit git. Claude Code's isolation checks refuse that and can't be turned off. They're also pointed at workit doctor --fix for the session trailer.
  • Docs: the Host parity matrix (stop control, per-turn context, the session trailer row). design.md no longer cites the removed plugin cache. Pi nudge and untrusted-guard tests added.
  • The packed launcher test runs with a scratch HOME/CODEX_HOME.

Hook bundle budget: 675 → 682 KB (Cursor is 678,173 B).

Verification

  • workit check lint, typecheck and test pass after the rebase. bun run knip is clean.
  • bun run build && bun scripts/test.ts packaging: 245 pass.
  • Built hook entries were fed Stop fixtures in a scratch repo with a delivered, unverified PR:
    • Claude and Codex return decision:block naming the verifier; Cursor returns followup_message;
    • with stop_hook_active, or when the last message is a question, the result is {}.
  • End to end: ci wait sees pending, writes 1 pr.status row, and a stop under green blocks once naming workit ci wait --pr 12. Once pr status sees passing, the stop is allowed.

🤖 Generated with Claude Code

BrainerVirus and others added 10 commits October 9, 2026 00:45
…every host

A core stop policy (hooks/stop.ts) reads ledger and local git facts only:
an unpushed commit with endpoint pr|green|merged, an open PR whose recorded
checks are failing or pending under green|merged, and a delivered head with
no non-author verdict. Claude Code and Codex Stop render decision block,
Cursor stop renders followup_message (loop_count guard, loop_limit 1), Pi
continues from agent_end with sendMessage triggerTurn, and OpenCode resumes
from session.idle with session.synthetic. Never in a subagent, never after
a continuation, never when the last message asks a question; any failure
allows the stop.

Raw-git nudges show once per kind per session (the skill-nudge marker) and
never on a command that runs workit. Claude Code worktree-isolated
subagents are told to run git as plain commands.

Workit-Session: fb894a62-61b7-4f32-a4d0-f47c2e5e300f-w-stop-control
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n every host

Stop control tests drive the core obligations through Claude Code, Codex
and Cursor payloads, Pi's agent_end and OpenCode's idle controller. Pi
tests cover the before_agent_start skill nudge (first turn, no-change turn,
changed context) and the untrusted guard. Raw-git nudges are checked once
per kind per session and never on a command that runs workit; relayed agent
messages never trigger a skill nudge. The packed Codex launcher runs with a
scratch HOME and CODEX_HOME.

Cursor's stop stays unregistered until the installer's canonical event list
adds it; the docs record it as missing. Host parity rows for stop control
and per-turn context are updated, and design.md no longer cites the removed
CLAUDE_PLUGIN_DATA cache.

Workit-Session: fb894a62-61b7-4f32-a4d0-f47c2e5e300f-w-stop-control
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d agents at it

The Host parity matrix gains the session-trailer row from the trailer-hook
slice, and the worktree-isolated SubagentStart guidance mentions that
`workit doctor --fix` can install the commit-msg hook that adds it.

Workit-Session: fb894a62-61b7-4f32-a4d0-f47c2e5e300f-w-stop-control
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The installer's canonical Cursor event list now carries `stop` with
`loop_limit: 1`, so a local install rewrites its launcher path and the
doctor flags a stop entry that could continue a turn more than once. The
shipped hooks-cursor.json registers it and the Cursor descriptor marks stop
native.

Workit-Session: fb894a62-61b7-4f32-a4d0-f47c2e5e300f-w-stop-control
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntrol

`workit pr status` and each `workit ci wait` poll append a CLI-observed
pr.status row (pr, branch, head, checks: passing|failing|pending) when the
summary changed since the newest row for that PR, so polling never floods
the ledger. The stop policy fires the CI obligation from the newest row for
the branch's open PR at the current head, and also finds a PR it only
observed through these rows. Docs record Cursor stop control as native.

Workit-Session: fb894a62-61b7-4f32-a4d0-f47c2e5e300f-w-stop-control
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Workit-Session: fb894a62-61b7-4f32-a4d0-f47c2e5e300f-w-stop-control
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the install test

Workit-Session: fb894a62-61b7-4f32-a4d0-f47c2e5e300f
Review mv0g209t. A CLI pr.merged row for HEAD, or for a head that already
holds this session's commits, ends every obligation: a squash merge with a
deleted or pruned remote branch no longer asks for a push that would
re-create it, and an --unverified merge no longer asks for a verdict. The
push obligation counts only commits this session recorded.

Pi skips agent_end when the run was aborted (Esc) or errored, or will be
retried; OpenCode resumes only a session whose outcome is succeeded. A
question now includes offers ("want me to", "let me know"), [y/N] prompts,
the Arabic and full-width question marks and a question followed by an
option list, read from the last paragraphs. The verdict message says to
start a verifier only if none is running. Relayed-message markers match
only at the start of a prompt, and Codex's Stop hook has a 10 s timeout.

Tests kill the surviving mutants: the protected-branch, merged-PR,
stale-head, pending, failing-verdict and endpoint guards, OpenCode's child,
checkout and outcome checks with commit rows present, and recordPrStatus's
closed-PR and fail-open paths.

Workit-Session: fb894a62-61b7-4f32-a4d0-f47c2e5e300f-w-stop-control
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stop freely

Workit-Session: fb894a62-61b7-4f32-a4d0-f47c2e5e300f-w-stop-control
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review mv0gw9vt. `workit pr status` and `workit ci wait` now also record a
PR's merged or closed state as a pr.status row (once per change), and the
stop policy reads a forge-reported merge like a `workit pr merge` row. With
no merge recorded, a branch whose upstream is gone after a prune counts as
landed, so a UI merge with auto-delete never asks for a push that would
re-create the branch. A PR closed unmerged still owes its verdict, and no
longer counts as open for the checks obligation. After a branch is reused
post-merge, only commits outside every recorded merged head are owed.

Pi drops the willRetry check: agent_end never carries it. Tests kill the
mutants on the CLI-observer filter, the two-paragraph question tail and
the unclosed code fence.

Workit-Session: fb894a62-61b7-4f32-a4d0-f47c2e5e300f-w-stop-control
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@BrainerVirus
BrainerVirus merged commit 84197cf into main Oct 9, 2026
12 checks passed
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 8.8.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@BrainerVirus
BrainerVirus deleted the feature/stop-control branch October 9, 2026 05:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant