Skip to content

task-recreate-worker --agent X in a worktree that predates X's loadout launches an agent with no radio hooks, then reports Recovered: preflight the worktree's hook carrier, verify the register, warn at switch time - #309

Merged
martin-conur merged 3 commits into
mainfrom
task/recreate-stale-worktree
Oct 22, 2026

Conversation

@martin-conur

Copy link
Copy Markdown
Owner

Summary

task-recreate-worker now refuses to launch an agent into a worktree that has no radio hooks for it, and names the merge that fixes it. It also prints "Recovered" only after it has seen the new session actually register with radio. task-work --from and task-config add / use get the matching check and warning, so a worktree forked before a loadout was committed can no longer give you an agent that is silently unreachable.

Closes #304

What changed

  • lib/hook-carrier.sh (new). It checks whether a checkout or a ref carries an assistant's radio hooks: a radio register hook in .claude/settings.json (claude), or in .kiro/agents/worker.json or the global agent kiro-cli falls back to (kiro). It also lists stale worker worktrees and prints the remedy: git -C <worktree> merge <base>, or "commit it on the base first" when the base does not carry the hooks either.
  • task-recreate-worker:
    • Preflight. A hookless worktree is refused before the sidecar is touched. --no-launch skips the check.
    • Verify. After the launch it waits up to TASK_RECREATE_WORKER_REGISTER_WAIT (30s) for the session file to show a register that is newer than the launch, by the expected AGENT, with a non-empty TAB_ID matching the captured tab. Only then does it print Recovered, the agent and tab id, and the queued-mail line. Otherwise it prints a warning with the specific reason and the likely causes: a trust prompt or upgrade dialog waiting, the picker under --resume, or an agent that exited. It exits 0 in that case.
    • The --resume session-id lines now print before the check, so they appear either way.
  • task-work --from <ref> refuses a ref that lacks the hooks HEAD has. A hookless HEAD is not refused, since a merge would not help, and neither is --no-launch.
  • task-config add assistant / use warn with a list of live worker worktrees (reviewers skipped) that lack the new assistant's hooks, each with its merge command. After add the warning also says the new loadout is not committed yet. This is a warning, never a refusal.
  • Tests. Every acceptance case is covered, plus the global kiro agent, both remedies, --no-launch, polling vs. the wait bound, the exact stale-session-file state from task-recreate-worker --agent X in a worktree that predates X's loadout launches an agent with no radio hooks, then reports Recovered #304, a register by the wrong agent, and one with an empty TAB_ID. Each of the 20 mutations tried turned at least one test red. To support this, the zellij stub can run the tab payload, the claude stub can play a SessionStart register (optionally delayed), and setup_stubs sets the wait to 0.
  • Docs. README (task-recreate-worker, task-config, --from), all 8 workflow docs (edited identically, so the byte-identity test still passes), and the CHANGELOG.

Decisions worth a look

  • No --merge-base option. The issue left this to the builder. The refusal prints the exact command, and running a merge inside someone's worktree is the user's call.
  • Exit 0 when the register never lands. The tab and the agent are up, and the most common cause, a startup prompt, resolves on its own once answered. The cost: task-monitor's R surfaces only failures, so it shows "recreated" without the warning.
  • The wait blocks R. task-monitor's R runs this command synchronously, so a register that never lands now freezes the board for up to 30s. A normal launch returns as soon as the register lands. Making R async is a monitor change, out of this PR's scope.

Verification

  • ./run_tests.sh: 1584 tests, all pass
  • tools/check-drift.sh: clean
  • shellcheck -x on the changed shell files: clean
  • Everything ran in scratch repos with a scratch HOME and TASK_FORCE_HOME and stubbed zellij, claude and kiro; no real agent was launched.

🤖 Generated with Claude Code

martin-conur and others added 2 commits October 22, 2026 09:35
…t launches an agent with no radio hooks, then reports Recovered: preflight the worktree's hook carrier, verify the register, warn at switch time

A worktree holds what was committed when it was forked, so one cut before
`task-config add assistant` has no hooks for the newer assistant, and an agent
started there never registers (#304).

- lib/hook-carrier.sh: does a checkout (or a ref) carry an assistant's radio
  hooks: `.claude/settings.json` / `.kiro/agents/worker.json` (or kiro's
  global agent) with a `radio register` hook; plus the stale-worktree lister
  and the merge remedy.
- task-recreate-worker refuses a hookless worktree before touching the
  sidecar, naming `git -C <worktree> merge <base>`; after the launch it waits
  up to TASK_RECREATE_WORKER_REGISTER_WAIT (30s) for a fresh register by the
  expected AGENT with a matching TAB_ID, and prints Recovered (and the queued
  mail line) only then; otherwise it warns with the reason and likely causes.
- task-work --from refuses a ref lacking the hooks HEAD carries.
- task-config add / use warn, listing live worktrees without the new
  assistant's hooks, each with its merge.

Tests for every acceptance case, each mutation-checked; stubs can now run the
tab payload and play a SessionStart register. Docs: README, the 8 workflow
docs, CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t launches an agent with no radio hooks, then reports Recovered: silence SC2016 on the literal hook commands in seed_hook_carriers

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

Copy link
Copy Markdown
Owner Author

Spec compliance (#304)

All three Fix items and the acceptance list are delivered.

  • Preflight. lib/hook-carrier.sh checks for a radio register hook in .claude/settings.json (claude) or .kiro/agents/worker.json, plus the global kiro agent as a fallback. task-recreate-worker refuses before touching the sidecar. The refusal names git -C <worktree> merge <base>, with the base from the sidecar, or says the base lacks the hooks too. task-work --from refuses only when HEAD has the hooks and the ref doesn't. That is a sensible scoping, and it is documented.
  • --merge-base was marked optional in the issue and is not added. The CHANGELOG says so explicitly. Fine.
  • Verify. Recovered and the queued-mail line print only after the session file shows a fresh heartbeat, the expected AGENT, and a non-empty TAB_ID matching the new tab. Otherwise it prints a warning that says which check failed, and exits 0. The wait is bounded (30s, TASK_RECREATE_WORKER_REGISTER_WAIT) and covered in the tests.
  • Switch-time warning. task-config add and use list stale worker worktrees, skip reviewer worktrees, and never refuse.
  • Tests and docs. There are bats tests for every acceptance bullet, and the PR reports 20 mutation checks. README, workflow doc, all 7 steering templates and the CHANGELOG are updated, with an Upgrading note.
  • PR body. It opens with ## Summary.

Code-review findings

I read the diff and checked the pieces below against the repo. I found no blockers.

  • I confirmed that LAST_HEARTBEAT is written with date -u +%Y-%m-%dT%H:%M:%SZ, so the string comparison in _register_status is sound.
  • I confirmed that the sidecar's BASE_BRANCH= line is written by task-work.
  • I confirmed that aw_stale_worktrees builds its <repo>-worktrees path the same way task-work does.
  • I confirmed that the claude settings hook command and the kiro hook command that task-init writes both begin with radio register, so the "radio register grep matches them.
  • CI ran for the head SHA (run count is 1), and every check is green: bats on ubuntu, macOS and jq 1.6, ShellCheck, drift, and Rust.

Optional nit, not blocking:

  • The carrier test is a literal grep for "radio register. If task-init ever prefixes the hook command (an absolute path, an env assignment), every worktree would be refused. lib/hook-carrier.sh could grep for radio register without the leading quote, or a test could pin the string task-init writes. This is low risk today.

Verdict

clean-with-nits. I found nothing blocking, and the PM can decide on the one optional nit above.

…t launches an agent with no radio hooks, then reports Recovered: match radio register as words anywhere in the hook command

`_aw_carries_radio_register` grepped for a leading-quote `"radio register`,
so a hook command with a prefix (an env assignment, an absolute path to
radio, `command radio …`) was refused as carrying no hooks. It now matches
the two words anywhere, bounded so `myradio register` still does not count.
A new bats case covers both prefixed forms and the lookalike; it is red
against the old pattern and against one without the leading boundary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@martin-conur
martin-conur merged commit 10c90ed into main Oct 22, 2026
8 checks passed
@martin-conur
martin-conur deleted the task/recreate-stale-worktree branch October 22, 2026 15:48
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.

task-recreate-worker --agent X in a worktree that predates X's loadout launches an agent with no radio hooks, then reports Recovered

1 participant