Session Lifecycle

workflow no always-loaded rules -- loads on demand updated 2026-06-14

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

  • session
  • shutdown
  • handoff
  • workflow

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

  • /sds command: runs a fixed sequence of wrap-up phases without prompting unless something genuinely cannot proceed
  • sds-broadcast.sh helper: 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:

  1. Background work check — enumerate active background tasks; report and wait/kill as appropriate
  2. Working tree — commit dirty changes (WIP commit + push on feature branches; ask on main)
  3. Issue updates — comment on referenced issues with session summary; auto-close only when commit body has closes/fixes #N AND PR merged
  4. Reflect — invoke the /reflect workflow inline to capture non-obvious learnings to the JSONL store
  5. Handoff — write a structured handoff via handoff.py write to ~/.claude/handoffs/{repo}/, where auto-startup.py will auto-inject it into the next session
  6. Sibling broadcast — append a session-ended event to ~/.claude/sessions/{repo}/events.jsonl and surface dirty-sibling state
  7. Final summary — one-screen report of what each phase did
  8. Exitkill -TERM $PPID terminates the parent claude process, equivalent to typing /exit. SessionEnd hooks still fire. Skipped if --no-exit or --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 /reflect and 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-ended row 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 #N or fixes #N AND 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 /exit to the model. Phase 8 uses kill -TERM $PPID because $PPID from inside the Bash tool resolves to the parent claude process. SIGTERM lets it flush transcripts and run SessionEnd hooks 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.

Manual, per file

Full control -- copy exactly the files you want from the sections below.

Files

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