resume-dev-team

Recover a crashed per-repo Phase 2 session by reading its current-session.id and spawning a new Terminal tab running `claude --resume <session-id>`. Use when /sprint-status shows CRASHED for a repo or

SKILL.md

GitHub

Resume Dev Team: $ARGUMENTS

set -uo pipefail
CTO_HOME_REQUIRED=1
# >>> CTO-HOME ANCHOR (canonical — sync with: bash scripts/cto/lint-skills.sh --apply) >>>
# Fleet state lives in the CTO home, and neither obvious anchor is reliable alone:
# `git rev-parse` returns whatever repo the SHELL sits in, so one lingering cd into a dev
# repo silently retargets the skill; and $CLAUDE_PROJECT_DIR was observed EMPTY in skill
# shells (2026-08-13) — /handoff then resolved the registry against a dev repo and refused
# with an error that blamed the registry rather than the anchoring. So: try every anchor,
# VALIDATE each candidate before accepting it, and when none holds either fail naming the
# real cause or fall back explicitly and say so.
# Skills that read the fleet registry set CTO_HOME_REQUIRED=1 before this block.
#
# NO POSITIONAL PARAMETERS IN THIS BLOCK — no dollar-1, no dollar-2, not even awk's dollar-1.
# (Spelled out rather than written literally, because the rewrite described next hits comments
#  too: a literal example here would itself be replaced with argument text.)
#
# This block is embedded in SKILL.md, and the skill runtime rewrites every dollar-followed-by-
# digits token in the FILE with the invocation's arguments before the shell ever sees it. The
# rewrite is document-wide: no awareness of code fences, no escape (a backslash before it does
# not protect it — it renders empty), and it applies inside comments and string literals alike.
# A shell function's own positional parameter is therefore replaced by argument text, and when
# the skill is invoked with no arguments it is replaced by nothing at all.
#
# The first version of this block used positional parameters and was consequently broken from
# the day it shipped: _cto_is_home tested a garbage path on every candidate, so the anchor
# NEVER matched, and every skill carrying it silently fell back to the cwd repo — precisely the
# confident wrong answer it was written to prevent. Found 2026-09-01, six weeks after it
# shipped, because the fallback usually landed on the right repo by luck.
#
# Candidates are passed in named variables instead. Brace-wrapped positionals happen to survive
# the rewrite; do not use them either — the next person to write the bare form reintroduces the
# bug, and it fails silently. Guarded by lint CHECK E.
_CTO_REJECTED=""
_CTO_CAND=""
_cto_is_home() {          # in: _CTO_CAND · appends to _CTO_REJECTED
  [ -n "$_CTO_CAND" ] && [ -f "$_CTO_CAND/.cto/projects.yaml" ] || return 1
  # The PUBLIC template ships a placeholder registry (/Users/yourname/repos/my-project).
  # Accepting it resolves cleanly and then reports a fleet of repos that do not exist —
  # a confident wrong answer, which is worse than the error it replaced. Observed live:
  # four dev repos still point .cto-path at the template, so this is the real path.
  if grep -q '/Users/yourname/' "$_CTO_CAND/.cto/projects.yaml" 2>/dev/null; then
    _CTO_REJECTED="$_CTO_REJECTED  $_CTO_CAND (placeholder registry — the unconfigured template)"
    return 1
  fi
  return 0
}
# Sets _CTO_FOUND rather than printing: a $(…) capture runs in a SUBSHELL, so the rejected-
# candidate diagnostics collected by _cto_is_home would be discarded exactly when they are
# needed — on the failure path.
_CTO_FOUND=""
_cto_walk() {             # in: _CTO_START · out: _CTO_FOUND
  _d="$_CTO_START"
  while [ -n "$_d" ] && [ "$_d" != "/" ]; do
    _CTO_CAND="$_d"; _cto_is_home && { _CTO_FOUND="$_d"; return 0; }
    if [ -f "$_d/.cto-path" ]; then
      _p="$(tr -d '[:space:]' < "$_d/.cto-path")"
      _CTO_CAND="$_p"; _cto_is_home && { _CTO_FOUND="$_p"; return 0; }
    fi
    # `--` because a mis-rendered candidate can begin with a dash, and `dirname --repos=x`
    # exits with "illegal option" instead of walking. That message was the only outward sign
    # of the rewrite bug above for two weeks, and it was read as cosmetic noise.
    _d="$(dirname -- "$_d")"
  done
  return 1
}
ROOT=""
for _c in "${CTO_HOME:-}" "${CTO_REPO:-}" "${CLAUDE_PROJECT_DIR:-}"; do
  [ -n "$_c" ] || continue
  _CTO_CAND="$_c"; _cto_is_home && { ROOT="$_c"; break; }
done
if [ -z "$ROOT" ]; then _CTO_START="$PWD"; if _cto_walk; then ROOT="$_CTO_FOUND"; fi; fi
if [ -z "$ROOT" ] && [ -f "$HOME/.cto/home" ]; then
  _p="$(tr -d '[:space:]' < "$HOME/.cto/home")"
  _CTO_CAND="$_p"; _cto_is_home && ROOT="$_p"
fi
if [ -z "$ROOT" ]; then
  if [ "${CTO_HOME_REQUIRED:-0}" = "1" ]; then
    echo "ERROR: no CTO home found — no directory containing .cto/projects.yaml." >&2
    echo "  \$CTO_HOME='${CTO_HOME:-}'  \$CTO_REPO='${CTO_REPO:-}'  \$CLAUDE_PROJECT_DIR='${CLAUDE_PROJECT_DIR:-}'" >&2
    echo "  walked up from: $PWD" >&2
    # Report the permanent anchor's CONTENT, not just that the remedy exists. When the anchor
    # is set correctly and the skill still fails, the error must not keep recommending it —
    # that loop cost a full diagnosis cycle on 2026-09-01.
    if [ -f "$HOME/.cto/home" ]; then
      echo "  ~/.cto/home is SET and was tested: '$(tr -d '[:space:]' < "$HOME/.cto/home")'" >&2
      echo "  It did not validate, so the remedy below is already applied and is NOT the fix." >&2
      echo "  Check that path holds .cto/projects.yaml, then suspect the skill rendering itself." >&2
    else
      echo "  This is an ANCHORING failure, not a missing registry. Run the skill from the CTO" >&2
      echo "  home, or fix it permanently:  echo /path/to/<project>-cto > ~/.cto/home" >&2
    fi
    [ -n "$_CTO_REJECTED" ] && { echo "  rejected candidates:" >&2; printf '%s\n' "$_CTO_REJECTED" >&2; }
    exit 1
  fi
  ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
  echo "NOTE: no CTO home found; using the current repo ($ROOT)." >&2
fi
CTO_REGISTRY="$ROOT/.cto/projects.yaml"
cd "$ROOT" || { echo "ERROR: cannot enter $ROOT" >&2; exit 1; }
# <<< CTO-HOME ANCHOR <<<
if [ ! -f "$CTO_REGISTRY" ]; then
  echo "ERROR: fleet registry not found at $CTO_REGISTRY — run this from the CTO home."
  exit 1
fi
REPO_NAME="$ARGUMENTS"

if [ -z "$REPO_NAME" ]; then
  echo "Usage: /resume-dev-team <repo-name>"
  exit 1
fi

# Resolve repo path
REPO_PATH=$(python3 - <<PYEOF
import re
with open('$CTO_REGISTRY') as f:
    text = f.read()
for b in re.split(r'(?=- name:)', text):
    m = re.search(r'- name:\s*(\S+)', b)
    p = re.search(r'path:\s*(\S+)', b)
    if m and m.group(1) == "$REPO_NAME" and p:
        print(p.group(1))
        break
PYEOF
)

if [ -z "$REPO_PATH" ]; then
  echo "ERROR: repo '$REPO_NAME' not in .cto/projects.yaml"
  exit 1
fi

SESS_ID_FILE="$REPO_PATH/.claude/current-session.id"
if [ ! -f "$SESS_ID_FILE" ]; then
  echo "ERROR: no recorded session ID at $SESS_ID_FILE"
  echo "Either /handoff was never run for this repo, or the SessionStart hook didn't record the ID."
  echo "Recovery option: re-run /handoff <sprint-XX> --repos $REPO_NAME"
  exit 1
fi

SESSION_ID=$(cat "$SESS_ID_FILE" | tr -d '[:space:]')

# Verify the JSONL exists (sanity check before trying to resume)
JSONL=$(find "$HOME/.claude/projects" -name "${SESSION_ID}.jsonl" -type f 2>/dev/null | head -1)
if [ -z "$JSONL" ]; then
  echo "ERROR: no JSONL found for session $SESSION_ID"
  echo "Session may have been deleted. Recovery option: re-run /handoff with --repos $REPO_NAME"
  exit 1
fi

echo "Resuming session $SESSION_ID for $REPO_NAME"
echo "JSONL: $JSONL"
echo "Last activity: $(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$JSONL")"
echo ""

# Open new Terminal tab running claude --resume
# CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=0: no grey composer suggestions in an
# ORCHESTRATED window. Window peeks read plain text with colour stripped, so a
# generated suggestion is byte-identical to a human draft and a watcher can submit
# the session its own advice as if the CEO typed it (2026-08-27). Off at the source.
# MODEL PIN (RCA 2026-09-03) — same resolution as /handoff. Without it a crash
# recovery silently reverts the repo to the machine-global default: the sprint
# starts pinned, then finishes on whatever /model last saved in some other window.
DEV_MODEL="$(bash "$ROOT/scripts/cto/resolve-devteam-model.sh" "$REPO_NAME" 2>/dev/null)"
if [ -n "$DEV_MODEL" ]; then
  MODEL_FLAG="--model '$DEV_MODEL' "
  echo "Model pin: $DEV_MODEL"
else
  MODEL_FLAG=""
  echo "Model pin: (inherit — tab takes the machine-global default)"
fi

echo "Opening Terminal tab with claude --resume..."
osascript <<APPLESCRIPT 2>&1 | tail -1
tell application "Terminal"
    activate
    do script "cd $REPO_PATH && export CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=0 && echo '[resume] resuming session $SESSION_ID for $REPO_NAME...' && claude $MODEL_FLAG--resume $SESSION_ID"
end tell
APPLESCRIPT

# Clear CRASHED flag if it exists
if [ -f "$REPO_PATH/.claude/CRASHED" ]; then
  rm "$REPO_PATH/.claude/CRASHED"
  echo "Cleared CRASHED flag."
fi
echo ""
echo "Resume tab launched. Watch the new Terminal window for the session to come back online."

Your task as CTO

Confirm to the CEO:

  1. Session resumed in a new Terminal tab
  2. CRASHED flag cleared (if it was set)
  3. Recommend: use /peek $ARGUMENTS to verify the resumed session is making progress

If the resume fails (e.g., session ID stale, JSONL missing), surface the error to the CEO and suggest re-running /handoff for that specific repo.

Sign as: — CTO