Skip to content

PM handoff across assistants: board digest at PM start, optional handoff note, refuse a second live PM (#301) - #319

Merged
martin-conur merged 2 commits into
mainfrom
task/pm-handoff
Oct 23, 2026
Merged

martin-conur merged 2 commits into
mainfrom
task/pm-handoff

Conversation

@martin-conur

Copy link
Copy Markdown
Owner

Summary

A PM that starts fresh now opens with a short board digest (in flight, queue, recent merges, attention), built from disk with no forge call, plus the previous PM's handoff note when one was left. That means handing the PM from claude to kiro, or back, no longer begins with a blind tracker re-read. task-pm also refuses to start a second PM while this repo's PM is live in another tab, so two assistants never split one inbox.

Closes #301

What changed

1. Board digest at a fresh PM start

  • task-monitor-snapshot --brief: a new output mode. It prints the first repo as text: attention first, then in flight (ref, title, PR, review state), the queue with its notes, and the last 24h of merges with each PR summary's first sentence. The text comes from the new lib/monitor-digest.jq. tm_board_digest in lib/monitor-text.sh caps it at 40 lines, and the last line says how many were cut. Titles are clipped at 90 characters and sentences at 160.
  • radio register runs task-monitor-snapshot --local --brief for a pm-* role under the same gate as the offline-backlog summary (fresh=1, not an alias). So it never prints on a /compact or /clear re-register, and never for a worker. A failure is logged, never fatal. TASK_FORCE_NO_PM_DIGEST=1 turns it off. Claude's SessionStart and kiro's agentSpawn both call radio register, so one path serves both, and the installed hook commands are unchanged.

2. Handoff note

  • task-monitor-queue handoff write|show|clear. write reads the note on stdin and saves it to $TASK_FORCE_HOME/monitor/<repo>/handoff.md, with written_at, the PM role and its recorded agent in frontmatter. It refuses an empty note. I made this a subcommand rather than a new binary, so no install.sh changes are needed.
  • The snapshot gains a handoff field, {text, written_at, role, agent} or null (documented in docs/task-monitor-schema.md, with an example).
  • The digest prints the note under a [handoff] line with its agent, role and age, capped at 20 lines. The note stays until the next write replaces it.
  • task-monitor: the note is the repo view's last row, collapsed to its first line with "from the kiro PM · 3h ago · 3 lines". ⏎ expands it in the usual detail view. o has nothing to open, and t goes to the PM tab.
  • PM prompts on all seven loadouts, plus this repo's installed .claude/commands/pm.md, gain a short "Handing off" paragraph.

3. One live PM per repo

  • task-pm refuses when pm-<repo> has a session file that radio orphans does not list (heartbeat under 1h) and whose TAB_ID and ZELLIJ_SESSION are not the active tab's. The refusal names the agent from <role>.agent and says to close that PM first. --force overrides.
  • A session that is live in this tab goes through, because that is the handoff itself: kiro has no exit hook, so a kiro PM quit a minute ago still looks live.
  • One addition beyond the spec: whenever task-pm proceeds over an earlier session file, it retires that file with radio unregister --manual. Without this, a claude PM started right after a kiro PM sees the leftover file, takes the fresh=0 path, and never prints the backlog or the digest. That is exactly the handoff this ticket is for. The inbox and the sidecars are left alone.

Tests

  • tests/pm_handoff.bats (new, 10 tests):
    • the digest prints on a fresh register and not on a re-register;
    • it is for PMs only, and the opt-out works;
    • it is capped at 40 lines and reports what was cut;
    • it makes no gh call, under a gh loadout with a queued ref;
    • the note round-trips with its age (written 2h ago), stays until overwritten, then is replaced;
    • the note records its agent, an empty write is refused, clear removes it;
    • task-pm refuses a live kiro PM in another tab, with the agent read from the sidecar;
    • --force launches and retires the old session;
    • the same-tab and stale cases go through;
    • a same-numbered tab in another zellij session is refused.
  • Rust test the_handoff_note_renders_collapsed_in_the_repo_view_and_expands.
  • Every new assertion was mutation-checked: 18 shell mutations and 5 Rust mutations, each turned red. The first pass found one vacuous assertion: the agent name in the refusal also came from the session file's AGENT=, so the sidecar read was never tested. The test now removes that field.
  • tests/task_pm.bats: the Flip radio wake defaults: workers auto-submit and stay in the background; the PM box is never auto-submitted #254 auto-submit takeover test now passes --force, because it takes over a live PM, which this change refuses on purpose.
  • The shared snapshot fixture was regenerated. Its only change is handoff: null, plus timestamps.
  • Also run locally against a jq 1.6 binary, since CI's jq-1.6 job is the floor.

Upgrading

Re-run task-init <loadout> on every loadout: the copied commands/pm.md / agents/pm.json and the workflow doc changed. The hooks did not. Re-run install.sh to rebuild task-monitor and get the handoff row.

🤖 Generated with Claude Code

martin-conur and others added 2 commits October 23, 2026 12:06
…off note, refuse a second live PM: digest from task-monitor-snapshot --local --brief on a fresh PM register, task-monitor-queue handoff, task-pm live-PM refusal with --force (#301)

- `task-monitor-snapshot --brief` renders the first repo as at most 40
  text lines (`lib/monitor-digest.jq`, capped by `tm_board_digest`), then
  the handoff note with its age. `radio register` prints it on a fresh,
  non-alias `pm-*` register only, from `--local`, so there is no forge call.
  `TASK_FORCE_NO_PM_DIGEST=1` turns it off.
- `task-monitor-queue handoff write|show|clear` owns
  `monitor/<repo>/handoff.md`. The snapshot gains a `handoff` field, and
  task-monitor shows it as a collapsed last row of the repo view that
  expands on Enter.
- `task-pm` refuses while `pm-<repo>` is live (not in `radio orphans`) in a
  tab other than the active one, naming the agent from `<role>.agent`.
  `--force` overrides. When it proceeds over an earlier session file it
  retires that file, so the new register is fresh and prints the backlog
  and the digest.
- Every PM prompt gains a "Handing off" paragraph. Docs: README, schema
  doc and example, the workflow docs, CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heck SC2119)

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

Copy link
Copy Markdown
Owner Author

Spec compliance (#301)

All three proposal items and the acceptance list are delivered.

  • Board digest. A fresh pm-* register prints task-monitor-snapshot --local --brief under the same fresh=1 / non-alias gate as the backlog summary, so /compact and /clear never re-print it.
    • It makes no forge call and is capped at 40 lines, with a "N more" footer.
    • Attention comes first and Done last, so the cap cuts the oldest news rather than the signal.
    • In flight shows ref, title, PR and review state. The queue shows its notes. Done shows the PR summary's first sentence.
    • TASK_FORCE_NO_PM_DIGEST=1 is an opt-out beyond the spec.
  • Handoff note. task-monitor-queue handoff write|show|clear, with frontmatter (written_at, role, agent) and an empty-note refusal. The next fresh start prints it with its age and keeps it until overwritten. The snapshot has a new handoff field, and the repo view has a collapsed row (Rust test included). The "Handing off" paragraph is on all 7 loadouts' PM prompts.
  • One live PM. task-pm refuses a fresh-heartbeat PM whose tab isn't the current one, names the agent from <role>.agent, and accepts --force. The same-tab case goes through, which fits kiro having no exit hook. The same-numbered-tab-in-another-zellij-session case is handled, and --also aliases are skipped.
  • Docs and changelog. README, schema doc, workflow docs, the steering templates and the CHANGELOG with the task-init upgrade note are all updated.

Code-review findings

I found no blockers and no correctness bugs.

  • CI. CI ran on head 253662d and every job passed: bats on ubuntu, macos and jq 1.6, plus drift, shellcheck and the Rust jobs including MSRV.
  • Local run. I ran pm_handoff.bats (10/10) and task_pm.bats against an isolated TASK_FORCE_HOME, and tools/check-drift.sh is clean.
  • Role names. Both task-pm and task-monitor-queue build the role from aw_main_repo_name, which is sanitized. A dotted repo name therefore cannot make the handoff note's role or the orphans match diverge.
  • Failure isolation. A failed digest only logs, never fails register.

Notes that need no change:

  • task-pm --force over a genuinely live PM retires its session file via radio unregister --manual. That PM stays unwakeable until it re-registers. This is what the spec and docs describe, so I'm only recording it.
  • I did not mutation-check the new assertions myself. I'm relying on the PR's statement that it did.

Verdict

clean. The PR body opens with ## Summary. PM owns the merge decision.

@martin-conur
martin-conur merged commit faae155 into main Oct 23, 2026
8 checks passed
@martin-conur
martin-conur deleted the task/pm-handoff branch October 23, 2026 17:09
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.

PM handoff across assistants: board digest at PM start, optional handoff note, refuse a second live PM

1 participant