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.claude/skills/resume-dev-team/SKILL.md
GitHubResume 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:
- Session resumed in a new Terminal tab
- CRASHED flag cleared (if it was set)
- Recommend: use
/peek $ARGUMENTSto 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