Skip to content

Agent processes outlive their closed tab: task-recreate-worker and task-done should see processes running in the worktree: scan the worktree for agent processes, refuse or --kill-stale on recreate, warn on task-done (#305) - #313

Merged
martin-conur merged 2 commits into
mainfrom
task/stray-agent-procs
Oct 22, 2026

Conversation

@martin-conur

Copy link
Copy Markdown
Owner

Summary

task-recreate-worker and task-done now look for agent processes still running inside the worktree, so a kiro or claude agent whose tab closed without taking it along is no longer invisible. Recovery refuses while one runs (or stops it with --kill-stale), and task-done warns and lists it instead of leaving it behind silently.

Closes #305

What changed

  • lib/worktree-procs.sh (new): aw_worktree_agent_procs <dir> prints pid\tstart\tcommand for agent processes (claude, kiro-cli*, bun/node running kiro-cli) and their descendants whose cwd is in the worktree. Cost is one ps -A plus a cwd lookup for candidates only: /proc/<pid>/cwd on Linux, one batched lsof -a -d cwd -Fpn elsewhere. The outermost agent among the caller's ancestors is excluded with its whole tree, since task-done runs under the finishing worker's own agent. Bash 3.2 safe. TASK_FORCE_NO_PROC_SCAN=1 disables it.
  • task-recreate-worker: refuses and lists the pids, after all other refusals. --force keeps its meaning and does not get past this. --kill-stale sends TERM, then KILL after 5s, after a [y/N] on the terminal; --yes skips it, and with no TTY and no --yes it refuses without signalling. It rescans right before signalling and signals only pids still listed with the same start time (recycled pid guard). A survivor of KILL stops the recovery. Logged as stray-procs:.
  • task-done: warns, lists, prints the kill line and logs stray-procs: (its tab closes right after). It never signals. I left out a --kill-stale there: the PM decides.
  • task-monitor-snapshot: unchanged. The optional flag would cost a process scan on every refresh.

Tests

tests/worktree_procs.bats (17), with stubbed ps/lsof/signal sender and pids above both kernels' pid ceilings, so no real process can be reached. It covers: scan scope (other worktree, path prefix, non-agent, caller's own tree, non-agent children), /proc mode with a deleted cwd, the kill switch, refusal with and without --force, --kill-stale signalling exactly the worktree's pids, KILL for a survivor only, a recycled pid skipped, a process that will not die, the no-TTY refusal, no-processes leaving recovery unchanged, and task-done's warning, log line and no-warning case.

Mutation-checked: 18 deliberate breaks of the helper, recreate and task-done each turned at least one test red. Two first-round survivors were fixed: a redundant ancestor-drop line was removed, and a non-agent-child test was added. tests/setup_suite.bash sets TASK_FORCE_NO_PROC_SCAN=1 for every other suite, so the suite never reads this machine's process table. loadout_modules.bats's fakeroot now copies the new lib.

Full suite: 1612/1615 on the first run, with the 3 failures in loadout_modules (the missing fakeroot lib). After the fix that file passes 23/23. The full suite was not re-run end to end after the fix. tools/check-drift.sh and shellcheck -x are clean.

Not covered

Not verified against a live process table, by design (PM guardrail). The agent-command pattern and the ancestor exclusion are calibrated on fixtures only, not on real ps output from a running kiro or claude. The [y/N] prompt path has no pty test; the no-TTY and --yes paths are tested.

Docs: README (recreate section + troubleshooting row), the 7 steering templates, .claude/gh-workflow.md, CHANGELOG.

🤖 Generated with Claude Code

…sk-done should see processes running in the worktree: scan the worktree for agent processes, refuse or --kill-stale on recreate, warn on task-done (#305)

A new `lib/worktree-procs.sh` lists agent processes (`claude`, `kiro-cli*`,
bun/node running kiro-cli, and their children) whose cwd is inside a
worktree: one `ps` pass, then `/proc/<pid>/cwd` or one batched `lsof` for
the candidates only. The caller's own agent tree is excluded.

`task-recreate-worker` refuses while any run, listing them; `--force` does
not bypass it. `--kill-stale` sends TERM then KILL after a TTY confirmation
(or `--yes`), re-checking pid start times before signalling. `task-done`
warns, lists and logs `stray-procs:`, and never signals.

Tests stub ps, lsof and the signal sender with pids above every kernel's
pid ceiling; the suite sets TASK_FORCE_NO_PROC_SCAN=1 elsewhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@martin-conur

Copy link
Copy Markdown
Owner Author

Spec compliance (#305)

Requirement Status
Helper in lib/ listing processes whose cwd is in a worktree (pid, start, command); /proc on Linux, batched lsof elsewhere; bounded to agent processes plus their children Met: lib/worktree-procs.sh, one ps -A, cwd lookup for candidates only
task-recreate-worker refuses and lists Met
--kill-stale: TERM, then KILL, after TTY confirmation, then proceeds; --force unchanged and does not imply a kill Met. --yes and the no-TTY refusal are extras, and sensible ones
task-done --remove-worktree warns and lists, never kills Met (also logs stray-procs:)
task-monitor-snapshot (optional) Reasonably left out, with the reason given
Bats tests with a stubbed lister, mutation-checked Met: 17 tests in tests/worktree_procs.bats
Docs: README, workflow docs, steering templates, CHANGELOG Met: all 7 templates, .claude/gh-workflow.md, README (section and troubleshooting row), CHANGELOG with the upgrade note

The PR body opens with ## Summary. CI ran and passed for the head SHA.

What I verified

  • ./run_tests.sh worktree_procs, task_recreate_worker, task_done and loadout_modules are all green. tools/check-drift.sh is clean, and so is shellcheck -x on the three changed shell files.
  • Live probe of the helper on this worktree, which the PR says was not done. Default self-pid gives an empty list, so the caller's own claude tree (the bash -ic … wrapper, claude, its MCP helpers) is excluded. With AW_PROC_SELF_PID=1 those same processes are listed. The macOS ps -o lstart= output parses into the 5-word start field. The ancestor exclusion works on real macOS ps output, not just fixtures.

Findings

No blockers. Nits, worth fixing in this PR since each is small:

  1. kill -0 cannot tell "gone" from "not permitted" (lib/worktree-procs.sh, aw_kill_worktree_procs). The liveness checks are _aw_proc_signal -0 "$pid" 2>/dev/null && alive+=…. If TERM or KILL fails with EPERM (a process owned by another user), kill -0 also fails. The pid is then counted as dead and the function returns 0, so recovery proceeds beside a live process. This is unlikely, since the scan is for the user's own agents. Still, the function's contract is "fails when anything survived the KILL". Either record a failed TERM/KILL send as survived, or re-scan after KILL and compare against the scan instead of kill -0.
  2. The agent pattern matches any argv word ending in claude or kiro-cli*. _aw_proc_candidates matches (^|[ \/])claude( |$) against the whole command line. A non-agent process whose cwd is in the worktree and whose command line merely ends in such a word, such as less docs/claude, qualifies. Under --kill-stale it would be TERMed after the [y/N], and the listing at least shows it. Matching on argv[0] and the script argument of a node/bun launcher would be tighter. Alternatively, say in the helper comment that the match is intentionally loose. Not blocking, since the user confirms against a printed list.
  3. task-done run by someone other than the worker will list the live worker. The exclusion is the caller's own agent tree. If the PM runs task-done --remove-worktree from a main-checkout tab while the worker's agent is still up, that worker is reported as a stray. This is consistent with the issue ("PM or user decides") and the warning never kills. A half-sentence in the README or workflow-doc paragraph would save someone a double-take: "run from the worker's own tab, the worker itself is not listed".
  4. Untested prompt path (acknowledged in the PR). The interactive [y/N] branch has no test. The read and case logic is simple, but the "no" answer and the EOF-on-read-then-abort path are the safety property of --kill-stale. Consider a pty test with the existing pty_helper for n/empty -> "Aborted; nothing was signalled", with the signal stub asserting zero calls.

Verdict

clean-with-nits. The spec is fully delivered, the tests are green, and the real-ps probe confirms the self-exclusion that fixtures alone could not. Nits 1 and 4 are the ones worth taking in this PR. The rest are comment or doc tweaks.

@martin-conur

Copy link
Copy Markdown
Owner Author

PM: one round before merge

CI is green. Please take all four of the reviewer's nits in this PR (tight-PR norm), plus one test gap from my mutation check:

  1. EPERM counted as dead (reviewer 1): make aw_kill_worktree_procs fail when a process survives or can't be signalled. Either treat a failed TERM/KILL send as survived, or decide by re-scanning the worktree after KILL instead of kill -0. Add a test whose signal stub fails -KILL and -0 for one pid: recovery must refuse.
  2. Loose agent pattern (reviewer 2): tighten it to argv[0] (basename claude / kiro-cli*), or the script argument of a bun/node launcher, and keep the bash -ic "… claude …" wrapper matched, since the self-exclusion depends on it (see 5). If tightening breaks the wrapper case, keep it loose and say why in the helper comment. Either way, add a fixture line like less docs/claude that is not listed, or document that it is.
  3. Doc note (reviewer 3): in the README / workflow-doc paragraph, add that run from the worker's own tab, the worker itself is not listed, while run from elsewhere (the PM's tab), a live worker shows up and is not necessarily stray.
  4. pty test for the [y/N] prompt (reviewer 4): use the existing pty helper. An n answer and an empty/EOF answer both print Aborted; nothing was signalled, and the signal stub records zero calls.
  5. Outermost vs innermost self-exclusion (PM): I changed if (p in agent) root = p to keep the first (innermost) agent ancestor and every test stayed green. In real use the innermost is claude and the outermost is the bash -ic "TASK_FORCE_ROLE=… claude …" wrapper with its cwd in the worktree, so the mutant makes task-done list its own launch wrapper as stray. Add that wrapper as a parent of claude in the "leaves out the agent the caller runs under" fixture so the mutation goes red.

Mutation-check 1, 4 and 5 yourself before re-review-requested. Still no signals to real processes: stubs only.

…sk-done should see processes running in the worktree: review round on #313

- Liveness after TERM/KILL is a re-scan of the worktree, not `kill -0`,
  which fails alike for a gone process and one we may not signal (EPERM).
  A process that refuses the signal now stops the recovery.
- An agent is judged by what runs: the `claude` / `kiro-cli*` binary, a
  bun or node launcher whose script is one, or a shell `-c` wrapper that
  runs one. `less docs/claude` no longer counts.
- Self-exclusion fixture now has task-work's `bash -ic` wrapper above
  claude, so keeping the innermost agent as root goes red.
- pty tests for the `[y/N]` prompt: y signals, n and EOF signal nothing.
- Docs: from the worker's own tab the worker is not listed; from the PM's
  tab a live worker is, and is not necessarily stray.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@martin-conur
martin-conur merged commit 32bc729 into main Oct 22, 2026
8 checks passed
@martin-conur
martin-conur deleted the task/stray-agent-procs branch October 22, 2026 22:12
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.

Agent processes outlive their closed tab: task-recreate-worker and task-done should see processes running in the worktree

1 participant