Session Lifecycle
Structured session shutdown via /sds. Autonomous wrap-up sweep: commit dirty work, update referenced issues, run /reflect, write a handoff, broadcast to sibling clones. Complements /startup at the other end of the session.
Tags
README
session-lifecycle
/sds (Shutdown Sequence) — autonomous end-of-session wrap-up. Commits dirty work, updates referenced issues, runs /reflect, writes a handoff, broadcasts to sibling clones, then terminates the Claude Code session.
Pairs with /startup at the other end of the session: where /startup orients you on session entry, /sds captures and disseminates context on session exit, then closes the door behind you.
What This Module Does
/sdscommand: runs a fixed sequence of wrap-up phases without prompting unless something genuinely cannot proceedsds-broadcast.shhelper: deterministic sibling detection + tracking.csv signal writer (kept out of the LLM)
The Sequence
/sds runs these phases in order. Each is a step the agent would otherwise have to remember to do separately:
- Background work check — enumerate active background tasks; report and wait/kill as appropriate
- Working tree — commit dirty changes (WIP commit + push on feature branches; ask on main)
- Issue updates — comment on referenced issues with session summary; auto-close only when commit body has
closes/fixes #NAND PR merged - Reflect — invoke the
/reflectworkflow inline to capture non-obvious learnings to the JSONL store - Handoff — write a structured handoff via
handoff.py writeto~/.claude/handoffs/{repo}/, whereauto-startup.pywill auto-inject it into the next session - Sibling broadcast — append a
session-endedevent to~/.claude/sessions/{repo}/events.jsonland surface dirty-sibling state - Final summary — one-screen report of what each phase did
- Exit —
kill -TERM $PPIDterminates the parentclaudeprocess, equivalent to typing/exit. SessionEnd hooks still fire. Skipped if--no-exitor--dry-run.
Files
| File | Type | Description |
|---|---|---|
commands/sds.md |
command | /sds shutdown sequence command |
lib/sds-broadcast.sh |
lib | Detects sibling clones from tracking.csv and writes the session-ended marker |
Dependencies
- multi-agent: provides
~/.claude/lib/handoff.py(used in phase 5) - self-improving: provides
/reflectand the learnings store (used in phase 4) - git-workflow: provides commit/PR conventions used in phases 2-3
Manual Installation
# Copy the command
mkdir -p ~/.claude/commands
cp commands/sds.md ~/.claude/commands/sds.md
# Copy the helper
mkdir -p ~/.claude/lib
cp lib/sds-broadcast.sh ~/.claude/lib/sds-broadcast.sh
chmod +x ~/.claude/lib/sds-broadcast.sh
Usage
/sds Run the full shutdown sequence and exit the session
/sds --no-exit Run the wrap-up but leave the session open (for testing or chaining)
/sds --dry-run Show what would happen without doing anything (implies --no-exit)
Design Notes
- Autonomous by default. Asking five questions during shutdown defeats the point. The agent only prompts when it cannot proceed (e.g., merge conflict on the WIP commit).
- Composes existing primitives. This module does not duplicate the handoff, reflection, or tracking infrastructure — it orchestrates them.
- Passive sibling coordination. Sibling clones are not directly invoked; they see a
session-endedrow in tracking.csv on their next interaction. Active push (e.g.,tmux send-keys) is intentionally deferred. - Conservative issue closing. Only auto-closes issues when the commit body explicitly says
closes #Norfixes #NAND the PR has merged. GitHub already does this on merge; the phase is a safety net. - Process exit, not a tool call. Claude Code does not expose a programmatic
/exitto the model. Phase 8 useskill -TERM $PPIDbecause$PPIDfrom inside the Bash tool resolves to the parentclaudeprocess. SIGTERM lets it flush transcripts and runSessionEndhooks before dying. If you don't want this behavior, pass--no-exit.
Will install
| Path | Action | Target | Type |
|---|---|---|---|
commands/sds.md | → | commands/sds.md | command |
lib/sds-broadcast.sh | → | lib/sds-broadcast.sh | lib |
Dependencies
Required by
No other module depends on this one.
Included in presets
Install this module
Agent prompt
Recommended for agent users -- hands the whole install off to your assistant.
Fetch https://cd23a9be.ccgm-site.pages.dev/modules/session-lifecycle.md and install this module into my Claude Code setup.
Native plugin marketplace
One command via the native plugin marketplace -- additive, does not merge settings.json.
claude plugin install session-lifecycle@ccgm
The marketplace path is additive, not a replacement: it installs commands, agents, and skills as native plugin components, but it does not perform the bash installer's deep settings.json merge, and it does not write the always-loaded global CLAUDE.md context. Rules are only injected via an opt-in SessionStart hook rather than being auto-loaded. Use the bash installer when those pieces matter to you.
Files
command (1)
commands/sds.md
--- description: Shutdown Sequence - autonomous end-of-session wrap-up (commit, issues, reflect, handoff, broadcast, exit) --- # /sds — Shutdown Sequence Autonomous end-of-session wrap-up. Commits dirty work, updates referenced issues, runs `/reflect`, writes a handoff, broadcasts to sibling clones, and **terminates the Claude Code session**. The bookend to `/startup`. ## Usage ``` /sds Run the full shutdown sequence and exit the session /sds --no-exit Run the wrap-up but leave the session open /sds --dry-run Show what would happen without doing anything (implies --no-exit) ``` ## Principles - **Autonomous by default.** Do not prompt unless you genuinely cannot proceed (e.g., merge conflict on the WIP commit, ambiguous issue reference). - **Compose, do not duplicate.** Use existing tools: `handoff.py`, `/reflect`, `ccgm-learnings-log`, `agent_tracking.py`, `gh` CLI. - **Conservative on writes that touch other people's view.** Closing an issue is bigger than commenting on one. - **One screen summary at the end.** The user should see exactly what changed without scrolling. ## The Sequence Execute these phases in order. Report a brief status line per phase as you go. Do not narrate intermediate reasoning — terse status updates only. ### Phase 0 — Detect mode and identity ```bash bash ~/.claude/lib/sds-broadcast.sh --siblings-only ``` This JSON gives you: agent id, repo, workspace, sibling list (with dirty state), handoff dir, event log path. Use these throughout. If `--dry-run` was passed to `/sds`, pass it through to all write operations in later phases. ### Phase 1 — Background work check Use TaskList to see active background tasks the agent owns. For each running task: - If it's near completion (last status update < 60s ago and progressing), wait briefly. - If it's stalled or long-running, report it and ask once: "Background task `<id>` is still running. Wait, kill, or leave it?" - If nothing is running, say so in one line and move on. Sub-agents spawned via the Agent tool that have already returned are out of scope — they don't need shutdown. Only `run_in_background` work and persistent background shells matter here. ### Phase 2 — Working tree commit ```bash git status --porcelain git branch --show-current ``` Decision tree: - **Clean working tree**: report "clean" and skip to Phase 3. - **Dirty on a feature branch**: stage all, commit as WIP, push: ```bash git add -A git commit -m "WIP: <one-line summary of what was in progress>" git push --set-upstream origin "$(git branch --show-current)" 2>/dev/null || git push ``` The WIP message should be specific (file or feature name), not generic. - **Dirty on main**: do NOT auto-commit. Report the dirty files and ask: "Dirty changes on main. Commit, stash to a branch, or leave?" If the commit fails (pre-commit hook, conflict), report the error and stop the sequence — don't proceed to issue updates with dirty state. ### Phase 3 — Issue updates Find issue references from three sources: 1. Branch name leading digits: `^(\d+)[-/]` (e.g., `423-deepresearch-followups` → 423) 2. Recent commits since branching from main: `git log --pretty=%B origin/main..HEAD` — extract `#\d+` and `closes/fixes/resolves #\d+` 3. The open PR for this branch (if any): `gh pr view --json number,body` For each unique issue number: ```bash # Check state gh issue view <N> --json state,title,number ``` - **If issue is open**: post a comment summarizing what was done this session for it. Use commit titles + PR link as evidence. Keep it under ~150 words. Format: ``` Session update from agent-<id>: - <commit title 1> - <commit title 2> PR: #<N> (status: <merged|open>) Next: <one line on what's left, or "issue work complete"> ``` - **If issue is closed**: skip (no comment, no action). - **Auto-close ONLY when**: issue is still open AND a merged PR's commit body contains `closes #<N>` or `fixes #<N>` or `resolves #<N>`. GitHub usually handles this automatically; this phase is a safety net for cases where the squash-merge stripped the keyword. Use `gh issue close <N> --comment "Closed by merged PR #<P>"`. - **Never close**: issues where the work isn't actually complete. Err on the side of leaving open. Report per issue: `#423: commented` / `#423: closed (closes-#N + PR #X merged)` / `#423: already closed, skipped`. ### Phase 4 — Reflect Invoke the `/reflect` workflow inline (do NOT spawn a subagent — reflection needs in-session context). Follow the abbreviated reflection pass: 1. What was the task this session? 2. What surprised you or took longer than expected? 3. Is there a reusable pattern, common mistake, user preference, or tool gotcha worth capturing? For each candidate learning: ```bash # Check for duplicates first ccgm-learnings-search --query "<keyword>" --max 3 # If new, log it ccgm-learnings-log --type <pattern|pitfall|preference|tool|architecture|operational> \ --content "<one-paragraph rule>" \ --tag <kebab-tag> \ --confidence <1-10> # If existing, reinforce ccgm-learnings-log verify <id> ``` If nothing notable, say "no learnings captured" and move on. Do not force. ### Phase 5 — Handoff Build a handoff body (markdown, ~200 words max). Use the template: ```markdown # Handoff — <one-line description> ## What I did <one paragraph: what shipped, files/modules touched, PR link> ## What's next <bullet list of immediate follow-ups, ideally concrete enough that the next agent acts without re-deriving context> ## Blockers / context <known landmines, decisions made and why, anything that would surprise the next agent. Skip if nothing applies.> ``` Write it via the existing handoff lib (auto-detects repo, branch, agent, PR, issue): ```bash python3 ~/.claude/lib/handoff.py write --body "$(cat <<'EOF' <your handoff body here> EOF )" ``` The file lands at `~/.claude/handoffs/<repo>/<timestamp>-<agent>.md` where `auto-startup.py` will auto-inject it into the next session for this clone AND for every sibling clone. ### Phase 6 — Sibling broadcast + event log ```bash bash ~/.claude/lib/sds-broadcast.sh ``` This appends a `session-ended` event to `~/.claude/sessions/<repo>/events.jsonl` and re-prints the sibling status JSON. For each sibling with `dirty: true` on a feature branch, note it in the final summary so the user can decide whether to switch clones and run `/sds` there as well. Do NOT directly invoke `/sds` in sibling clones — they may be mid-task. ### Phase 7 — Final summary Print a one-screen report. **The summary must be plain text in your response, NOT inside a Bash heredoc**, so the user sees it before Phase 8 terminates the process. Format: ``` /sds complete — agent-<id> on <branch> Phase 1 — Background: <none | killed N | waited on N> Phase 2 — Commit: <clean | WIP committed: <sha> | <skipped, reason>> Phase 3 — Issues: #<N> commented, #<N> closed, ... Phase 4 — Reflect: <N> learning(s) captured | nothing notable Phase 5 — Handoff: <path> Phase 6 — Broadcast: <N> sibling(s) notified via event log Sibling state: - agent-w0-c1 on <branch> (clean | dirty) - ... Next: <one line — "exiting session now" or "consider running /sds in dirty sibling X"> ``` The status lines stop at Phase 6 by design. Do NOT add a "Phase 7 — Summary:" or "Phase 8 — Exit:" line — Phase 7 IS this summary text, and Phase 8 IS the kill tool call that follows it. They are not items inside the report; they are the report and what happens after it. ### Phase 8 — Exit the session This is the terminal action of `/sds`. Skip entirely if `--no-exit` or `--dry-run` was passed. ```bash kill -TERM $PPID ``` `$PPID` from inside the Bash tool resolves to the parent `claude` process. SIGTERM gives it a chance to flush transcripts and run `SessionEnd` hooks before dying — equivalent to the user typing `/exit`. **No narration between Phase 7 and the kill.** After the Phase 7 summary's "Next:" line, the next thing in your output MUST be the Bash tool call running `kill -TERM $PPID`. Do not print a "Phase 8 — Exit:" header, do not print "Exiting now" or any farewell, do not bundle the summary into a heredoc inside the kill command. The Phase 7 summary is the last text the user sees; the kill is the last tool call. If you find yourself about to type anything after the "Next:" line, stop and issue the kill instead. If the kill bash call returns at all (race condition), do not print anything further — the session is going down regardless. If `--no-exit` was passed, replace the kill with a one-line text reminder: `Run /exit when ready.` ## Failure handling - Any phase that fails should report the failure and stop the sequence. Do not continue past a broken phase silently. - **Crucially: if any earlier phase failed, DO NOT execute Phase 8 (exit).** Leave the session open so the user can address the failure. - The only phase where partial completion is acceptable is Phase 3 (Issues) — a failed comment on one issue doesn't prevent commenting on another. Report per-issue. - If the git push in Phase 2 fails (auth, branch protection), commit locally and report the push failure — don't roll back the commit, and skip Phase 8. ## When NOT to use - Mid-task. `/sds` is for end-of-session, not pausing. Use `/checkpoint save` to pause. - Right after a fresh `/startup`. Nothing to wrap up. - When you've been in read-only mode (no commits, no issues touched). The phases will be no-ops; you can run it but a one-line "nothing to wrap up" is the right report. ## Cross-reference - `/startup` — the other bookend; reads handoffs `/sds` writes - `/handoff` — invoked by Phase 5 under the hood - `/reflect` — invoked by Phase 4 inline - `/checkpoint save` — different concern (pause mid-task vs. end session) - `/cpm` — for the "ship the PR and merge" flow before /sds is appropriate
lib (1)
lib/sds-broadcast.sh
#!/usr/bin/env bash
# sds-broadcast.sh — deterministic sibling detection + session-event log writer
#
# Used by the /sds command to:
# 1. Detect sibling clones in the same workspace (workspace model only).
# 2. Append a session-event entry to ~/.claude/sessions/{repo}/events.jsonl
# so future tools can query when each agent last cleanly shut down.
#
# This script does NOT directly notify siblings — the handoff file at
# ~/.claude/handoffs/{repo}/ does that via auto-startup.py on their next
# session. This script just gathers info and persists a structured event.
#
# Usage:
# sds-broadcast.sh Detect + log
# sds-broadcast.sh --dry-run Detect only, no writes
# sds-broadcast.sh --siblings-only Print sibling JSON only, no event log
#
# Output (stdout, JSON):
# {
# "agent": "agent-w0-c0",
# "repo": "ccgm",
# "workspace": "ccgm-w0",
# "clone_path": "$HOME/code/.../ccgm-w0-c0",
# "siblings": [
# { "agent": "agent-w0-c1", "path": "...", "branch": "...", "dirty": true }
# ],
# "event_path": "$HOME/.claude/sessions/ccgm/events.jsonl",
# "handoff_dir": "$HOME/.claude/handoffs/ccgm"
# }
set -uo pipefail
DRY_RUN=0
SIBLINGS_ONLY=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
--siblings-only) SIBLINGS_ONLY=1 ;;
*) echo "warn: unknown arg: $arg" >&2 ;;
esac
done
# -----------------------------------------------------------------------------
# Identity
# -----------------------------------------------------------------------------
CWD="$(pwd)"
CLONE_PATH="$CWD"
# Read agent_id from .env.clone if present, else derive from directory name
AGENT_ID=""
if [ -f "$CWD/.env.clone" ]; then
AGENT_ID="$(grep -E '^AGENT_ID=' "$CWD/.env.clone" 2>/dev/null | head -1 | cut -d= -f2)"
fi
if [ -z "$AGENT_ID" ]; then
base="$(basename "$CWD")"
if [[ "$base" =~ w([0-9]+)-c([0-9]+)$ ]]; then
AGENT_ID="agent-w${BASH_REMATCH[1]}-c${BASH_REMATCH[2]}"
elif [[ "$base" =~ -([0-9]+)$ ]]; then
AGENT_ID="agent-${BASH_REMATCH[1]}"
else
AGENT_ID="agent-0"
fi
fi
# Repo from git remote
REPO=""
if git -C "$CWD" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
url="$(git -C "$CWD" remote get-url origin 2>/dev/null || true)"
if [ -n "$url" ]; then
REPO="$(basename "$url")"
REPO="${REPO%.git}"
fi
fi
[ -z "$REPO" ] && REPO="$(basename "$CWD")"
# Workspace = parent dir if cwd basename matches the workspace pattern (w0-cN)
WORKSPACE=""
if [[ "$(basename "$CWD")" =~ w[0-9]+-c[0-9]+$ ]]; then
WORKSPACE="$(basename "$(dirname "$CWD")")"
fi
# -----------------------------------------------------------------------------
# Sibling detection (workspace model only)
# -----------------------------------------------------------------------------
SIBLINGS_JSON="[]"
if [ -n "$WORKSPACE" ]; then
ws_dir="$(dirname "$CWD")"
parts=()
while IFS= read -r -d '' sib; do
[ "$sib" = "$CWD" ] && continue
sib_base="$(basename "$sib")"
if [[ "$sib_base" =~ w([0-9]+)-c([0-9]+)$ ]]; then
sib_agent="agent-w${BASH_REMATCH[1]}-c${BASH_REMATCH[2]}"
else
continue
fi
sib_branch="$(git -C "$sib" branch --show-current 2>/dev/null || echo '')"
sib_dirty="false"
if [ -n "$(git -C "$sib" status --porcelain 2>/dev/null)" ]; then
sib_dirty="true"
fi
sib_path_escaped="${sib//\"/\\\"}"
sib_branch_escaped="${sib_branch//\"/\\\"}"
parts+=("{\"agent\":\"$sib_agent\",\"path\":\"$sib_path_escaped\",\"branch\":\"$sib_branch_escaped\",\"dirty\":$sib_dirty}")
done < <(find "$ws_dir" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null)
if [ ${#parts[@]} -gt 0 ]; then
SIBLINGS_JSON="[$(IFS=,; echo "${parts[*]}")]"
fi
fi
# -----------------------------------------------------------------------------
# Paths (handoff dir + session event log)
# -----------------------------------------------------------------------------
# Slugify repo name (filesystem-safe)
REPO_SLUG="$(echo "$REPO" | sed -E 's/[^A-Za-z0-9._-]+/-/g; s/^-+|-+$//g')"
[ -z "$REPO_SLUG" ] && REPO_SLUG="unknown"
HANDOFF_DIR="${HOME}/.claude/handoffs/${REPO_SLUG}"
SESSIONS_DIR="${HOME}/.claude/sessions/${REPO_SLUG}"
EVENT_PATH="${SESSIONS_DIR}/events.jsonl"
# -----------------------------------------------------------------------------
# Event log write (unless dry-run or siblings-only)
# -----------------------------------------------------------------------------
if [ "$DRY_RUN" -eq 0 ] && [ "$SIBLINGS_ONLY" -eq 0 ]; then
mkdir -p "$SESSIONS_DIR"
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
branch="$(git -C "$CWD" branch --show-current 2>/dev/null || echo '')"
branch_escaped="${branch//\"/\\\"}"
printf '{"ts":"%s","agent":"%s","repo":"%s","branch":"%s","event":"session-ended"}\n' \
"$ts" "$AGENT_ID" "$REPO_SLUG" "$branch_escaped" >> "$EVENT_PATH"
fi
# -----------------------------------------------------------------------------
# Output
# -----------------------------------------------------------------------------
cat <<EOF
{
"agent": "$AGENT_ID",
"repo": "$REPO_SLUG",
"workspace": "$WORKSPACE",
"clone_path": "$CLONE_PATH",
"siblings": $SIBLINGS_JSON,
"event_path": "$EVENT_PATH",
"handoff_dir": "$HANDOFF_DIR",
"dry_run": $([ "$DRY_RUN" -eq 1 ] && echo true || echo false)
}
EOF