- N1 (planner-reviewer-claude-01) worktree_remove.sh: when `git status` fails in the worktree (e.g. its directory was deleted), judge_tree returned before METHOD was set and the summary line died under set -u with exit 1. METHOD now has its default before the function; the refusal is exit 3 with "nothing was changed", plus a `git worktree prune` hint when the directory is gone. Same for --dry-run. - R4 (planner-reviewer-codex-01) worktree_prepare.sh: a submodule path that is not initialised yet but already holds files made the clone fail, and the rollback's `deinit -f` then deleted those files. --submodules now refuses such a path up front (new exit 3, nothing changed), which also makes the rollback's premise true: after a failed attempt, everything under a not-yet-initialised path was created by that attempt. - R5 (planner-reviewer-codex-01) lib_py/worktree.py: `submodule foreach` visits initialised checkouts only, but a deinit'd submodule keeps its repository - stash and unpushed commits included - under the worktree's modules/, which the forced removal deletes. Every repository under modules/ (nested ones included) is now checked directly (`--git-dir`): commits on no remote (HEAD included) and refs/stash with its reflog. A directory that looks like a partial repository cannot be checked and refuses. - Also: the rollback loops are C-style (`seq 0 -1` counts down on BSD), and prepare warns that --base is ignored for an existing branch. Tests W6i (deleted worktree directory, with and without --dry-run), W6j and W6j2 (stash / unpushed commit in a deinit'd submodule's repository) and W6k (existing files in an uninitialised submodule path). Skill docs and the design doc describe the new refusal and the wider repository check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
10 KiB
name, description, version, author, license, platforms, environments, metadata
| name | description | version | author | license | platforms | environments | metadata | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| multi-agent-mux-stop | Stop an agent herdr session (claude, antigravity/agy) and update .mam/agent-sessions.yaml. Default stops gracefully and marks status=stopped with conversation preserved for resume. Does NOT delete on-disk conversation artifacts (jsonl/db) — those are preserved unless --purge-conversation is passed. Use when ending a work session, switching to a different one, or cleaning up before a fresh start. | 4.6.0 | godopu | MIT |
|
|
|
Multi-Agent Stop — Stop an Agent herdr Session
Companion skills:
multi-agent-mux-create(start),multi-agent-mux-resume(re-attach),multi-agent-mux-monitor(live status). Herdr Isolation:stop명령은 YAML의herdr_session필드를 자동으로 파싱하여 해당 격리 서버의 세션을 안전하게 종료(kill)하므로,HERDR_SESSION_NAME환경변수를 수동으로 지정할 필요가 없습니다. (--herdr-workspace는 CLI 대칭성을 위해 파서에서 허용되지만 소켓 라우팅에는 영향을 주지 않습니다.) Single source of truth:./.mam/agent-sessions.yaml.
What this skill does
Stop an agent's herdr session gracefully, resolve and store the conversation ID, and mark the YAML entry (status=stopped). Preserves:
- The herdr session's recorded
pane.pid / cmd / cwd / mcp_attachmentsfor audit - The agent's on-disk conversation (claude
*.jsonl, agyconversations/*.db) — so the user canmulti-agent-mux-resumelater - The
start_commandso a futuremulti-agent-mux-create --session <name>reproduces the same herdr spec
The stop command is always graceful by default:
- Sends exit keys to the agent TUI (
/exitfor Claude,/exitfor Agy) and waits 3 seconds. - If still alive, issues
herdr kill-session(SIGTERM) and waits 5 seconds. - If still alive, kills the pane PID via SIGKILL (
kill -9) as a last resort. - Auto-captures the conversation ID into the row (
claude_session_id_own/agy_conversation_id_own) before killing, ensuring the next resume uses a race-free tier-1 lookup.
Pre-flight
SESSION_NAME=<workspace>-creator-<agent> # convention
AGENT=claude # claude | agy | hermes | grok | opencode | codex — always pass it
AGENT_SESSIONS_YAML=.mam/agent-sessions.yaml
# 1) Session is registered?
python3 -c "
import yaml
d = yaml.safe_load(open('$AGENT_SESSIONS_YAML'))
names = [s['name'] for s in d.get('herdr_sessions', [])]
if '$SESSION_NAME' not in names:
print('NOT in YAML — refusing to stop (no audit trail). Use multi-agent-mux-create first, or pass --force-no-yaml.')
raise SystemExit(1)
"
# 2) Already stopped?
ALREADY=$(python3 -c "
import yaml
d = yaml.safe_load(open('$AGENT_SESSIONS_YAML'))
s = [x for x in d['herdr_sessions'] if x['name']=='$SESSION_NAME'][0]
print(s.get('status', 'unknown'))
")
if [ "$ALREADY" = "stopped" ]; then
echo "Already stopped."
fi
Workflow
# 1. Stop gracefully (default — captures ID, shuts down safely, status=stopped)
bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
--session "$SESSION_NAME" --agent "$AGENT"
# 2. Stop gracefully + record a custom stop reason
bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
--session "$SESSION_NAME" --agent "$AGENT" --reason api_error
# 3. Stop gracefully + clean up on-disk conversation (DANGEROUS)
# — this prevents any future resume (status=terminated, resumable=false).
bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
--session "$SESSION_NAME" --agent "$AGENT" --purge-conversation
Idempotency: if the row is already status: stopped, the script prints already stopped (...) and exits 0 — re-running is a safe no-op.
--agent is the standard. Pass it on every invocation. If omitted, the script
resolves the agent from the registry record — the row's agent field, then the
session-name suffix, then pane.cmd — and exits 2 if none of the three resolve.
The fallback exists for recovery, not as the normal calling convention: a session
whose name carries no agent suffix (e.g. agy-creator-01) is only resolvable
while its registry row survives.
State machine
running ──(stop default / --reason)────────► stopped (resumable:true, conv preserved)
running ──(stop --purge-conversation --yes)► terminated (resumable:false, conv deleted)
stopped ──(stop default … again)───────────► stopped (idempotent no-op)
Fields written in STOP mode: status: stopped, stopped_at, stopped_at_epoch, stop_reason, termination_mode: graceful, claude_session_id_own/agy_conversation_id_own and resumable: true.
If --purge-conversation is used: status: terminated, terminated_at, terminated_at_epoch, termination_mode: purge and resumable: false.
The script:
- Verifies the session is in agent-sessions.yaml
- If
delegate_job_idis set, automatically publishes aprogress --detail "terminating"event to the multi-agent-mux-delegate-job registry - Captures the
last_visible_statusfromherdr capture-pane(so we have a final TUI snapshot for audit) - Attempts graceful exit keys → SIGTERM kill-session → SIGKILL fallback
- For
purge-conversation: deletes~/.claude/projects/.../jsonl(claude) or, for agy,~/.gemini/antigravity-cli/conversations/<uuid>.dbwith its-wal/-shm/-journalsidecars,brain/<uuid>/,annotations/<uuid>.pbtxtandpresence/<uuid>.lock(the lock only once no live process references the conversation — Issue #10) - Updates the YAML entry and SQLite database atomically
- If
delegate_job_idis set, publishes acompletedevent to the multi-agent-mux-delegate-job registry
Removing an issue worktree: worktree_remove.sh (Issue #21 item 4)
bash .agents/skills/multi-agent-mux-stop/scripts/worktree_remove.sh --path <WT> [--stop] [--dry-run]
Every check runs first and changes nothing; a refusal exits 3:
- a linked worktree of the control root, not locked (
git worktree unlockfirst - git refuses even--forceon a locked one); - running sessions under it only with
--stop(stopped normally, conversations kept); - no open job of those sessions - never cancelled automatically, they may belong to a running loop (the delegate job of a session
--stopstops is cancelled by stop itself); - a clean tree, submodules included (
status --porcelain --ignore-submodules=none); - inside submodules: no modification, no commit that is on no remote (
HEADincluded - a submodule sits on a detached HEAD) and no stash (refs/stashis reachable from no branch and leaves the status clean) - checked in every repository under the worktree'smodules/, including deinitialised submodules (their repository stays there) and nested ones; a repository that cannot be checked refuses.
Then it stops the sessions, re-checks the jobs, runs checks 4 and 5 again (a session that is being stopped can still write, commit or stash - any change refuses with exit 3, sessions already stopped) and removes the worktree (the branch is kept): plain, or - when the worktree holds submodule state, which git refuses to remove otherwise - a single --force justified by checks 4 and 5. Do not run git worktree remove --force by hand on such a worktree: it deletes the worktree's own submodule repositories, unpushed commits included.
Exit codes: 0 removed, 1 stop/git failure, 2 not a worktree of the control root, 3 refused (nothing changed). Sessions created in a removed worktree cannot be resumed until a worktree exists at that path again.
Pitfalls
- Don't delete on-disk artifacts by default — the agent's
*.jsonl/conversations/*.dbis the data thatmulti-agent-mux-resumeneeds.--purge-conversationis for when the user is genuinely done with the conversation and wants zero recovery chance. - YAML is append-only until you write a stop — if a previous run left the entry as
runningbut herdr is actually dead (crash, host reboot), the YAML is stale. Runningmulti-agent-mux-stopwill detect "herdr already dead, just update YAML" and proceed. - Don't delete the
claude_session_id_own: nullplaceholder — when the user creates a fresh session withmulti-agent-mux-createand never sent a message, the entry hasclaude_session_id_own: null. Stopping must preserve that field. - Monitor skill may still be tracking — if
multi-agent-mux-monitoris running a heartbeat loop, stopping a session while it watches will trigger itsherdr ls != yamlreconciliation. That's expected — let the monitor run, it will mark the entry asterminatedon its own.
Verification
# 1. herdr gone (real native command — `herdr has-session` is a lib.sh
# tmux-compat pseudo-command and needs `source .agents/skills/lib.sh` first)
herdr agent get "$SESSION_NAME" >/dev/null 2>&1 && echo "STILL ALIVE" || echo "OK: herdr gone"
# 2. YAML has stopped entry
python3 -c "
import yaml
d = yaml.safe_load(open('$AGENT_SESSIONS_YAML'))
s = [x for x in d['herdr_sessions'] if x['name']=='$SESSION_NAME'][0]
assert s['status'] == 'stopped', f'expected stopped, got {s[\"status\"]}'
assert s.get('stopped_at'), 'missing stopped_at'
print(f'OK: stopped at {s[\"stopped_at\"]}')
print(f' preserved: pane.pid={s[\"pane\"][\"pid\"]}, cmd={s[\"pane\"][\"cmd\"]}, cwd={s[\"pane\"][\"cwd\"]}')
"
# 3. (if --purge-conversation) disk artifacts gone
[ -f "${CLAUDE_PROJECT_DIR:-$HOME/.claude/projects}/<projkey>/<uuid>.jsonl" ] && echo "WARN: jsonl still exists" || echo "OK: jsonl purged"
When NOT to use this skill
- Just detaching → there's no
herdr detachCLI command; press the herdr detach keybinding inside the pane, or just close the terminal. The herdr session keeps running. - Stopping the agent inside but keeping herdr → send
Ctrl-Cor/exit(claude) /Ctrl-D(agy) viaherdr agent send <target> <text>(real native command;herdr send-keysis a lib.sh tmux-compat pseudo-command that needssource .agents/skills/lib.shfirst). The herdr session stays but the agent process is gone. - Replacing an existing session with a new one →
multi-agent-mux-stopfirst, thenmulti-agent-mux-create.