Skip to content

Refuse a stop that leaves work in flight with nothing watching it - #44

Closed
jeremymanning wants to merge 2 commits into
mainfrom
compound/lesson-end-of-turn-inflight-check
Closed

jeremymanning wants to merge 2 commits into
mainfrom
compound/lesson-end-of-turn-inflight-check

Conversation

@jeremymanning

@jeremymanning jeremymanning commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

Adds an in-flight check at the stop, as a hook rather than a lesson.

Why a hook

The failure: a turn starts a long run, dispatches a workflow, opens pull requests, writes a status summary and ends. Nothing fails. The work finishes or stalls unseen, and the next anyone hears of it is the user asking why the monitors are gone.

This PR first proposed a lesson with a script to run by hand. That is the softer version of what works: the session that forgets the waiter forgets the lesson too. What actually stopped it was a Stop hook that refuses the stop, so that is what ships. The lesson is removed (it also failed test_docs and test_general_pool, which list everything the pool ships).

What it does

hooks/inflight-stop-check.sh refuses a stop once when any of these exist and no polling loop is alive:

  • a detached process that looks like a test suite or a launched job;
  • a workflow run dispatched by hand (cron runs are ignored: they are not the session's to watch);
  • the user's own pull request with pending checks.

It reads stop_hook_active and stays silent on the stop that follows a block, so it cannot loop. The mod's classic.Stop handler runs it and joins its reason to any owed-lesson refusal. A script that is missing, times out or prints something unreadable lets the stop through. The same file can be registered directly as a Stop hook in settings.json.

Limits

  • Waiter detection is a heuristic: a live shell loop that sleeps and polls gh, a pid or a log. A waiter wrapped in a script file is not recognised, so the stop is refused once unnecessarily.
  • The detached-process match is by pattern (pytest, a Claude scratchpad path, nohup); a long-lived unrelated process matching it would also trigger one refusal.

Tests

  • claude plugin test .: 286 pass, 4 new (refusal carries what is in flight; the script is told when a stop follows a block; nothing in flight or unreadable output lets the stop through; an owed lesson and unwatched work share one refusal).
  • ./run_tests.sh: OK, 23 files.
  • The script was pipe-tested against live state: blocked with two detached runs and a dispatched workflow in flight, allowed with a polling loop running, allowed on the follow-up stop.

🤖 Generated with Claude Code

https://claude.ai/code/session_01V7QkYvK7jaE1QVY1ZMfAt2

jeremymanning and others added 2 commits October 10, 2026 18:17
The first commit proposed a lesson with a script to run by hand. That is the
softer version of what works: the session that forgets to keep a waiter going
forgets the lesson that says to keep one too. What stopped the failure locally
was a Stop hook that refuses the stop, so that is what ships.

hooks/inflight-stop-check.sh is byte-identical to the hook registered locally.
It refuses a stop once when work is still in flight with nothing watching it: a
detached process that looks like a test suite or a launched job, a workflow run
dispatched by hand (cron runs are not the session's to watch), or the user's own
pull request with pending checks, and no polling loop alive. It reads
stop_hook_active and stays silent on the stop that follows a block.

The mod's classic.Stop handler runs it and joins its reason to any owed-lesson
refusal. A script that is missing, times out or prints something unreadable lets
the stop through. The same file can be registered directly in settings.json.

The lesson is removed from the pool: it also failed test_docs and
test_general_pool, which list everything the pool ships.

claude plugin test: 286 pass (4 new). run_tests.sh: OK, 23 files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7QkYvK7jaE1QVY1ZMfAt2
@jeremymanning jeremymanning changed the title Add lesson: end-of-turn-inflight-check Refuse a stop that leaves work in flight with nothing watching it Oct 10, 2026
@jeremymanning

Copy link
Copy Markdown
Member Author

Closing without merging: the branch is out of date, and the hook conflicts with the repository's rules.

  • The stop may wait at most 2 s for the mod's work (BUDGET in hooks/render.ts, .claude/CLAUDE.md). This hook runs hooks/inflight-stop-check.sh with a 45 s budget, and the script makes network calls at every stop: gh api user, gh run list, and one gh pr checks per open pull request.
  • The script counts as a waiter only a process whose command line holds sleep N and a poll. A background wait-for-ci.py (the waiter the ci-poll-in-background lesson ships) is not counted, so a run that is being watched is reported as unwatched.
  • The branch is 35 commits behind main, and the stop path in hooks/register.ts has changed since (9f3044c: the stop now awaits background fix judges).
  • The same script is already registered as a Stop hook directly in the maintainer's settings, so merging it would run it twice there.

The check is still available as the user lesson end-of-turn-inflight-check, with its script.

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