Files
msd-core/gsd-core/workflows/resume-project.md
Tom Boucher 7edd18fd2b feat(#1165): async external_job_waiting half-state + resume/pause contract (#1221)
Core half of #1105: a legal external_job_waiting deferred state so an async-dispatched Execute step (committing a .planning/async-jobs/<job>.json manifest, deferring SUMMARY.md) is not an illegal partial. execute-phase safe-resume, resume-project, and pause-work reconcile against the versioned scheduler-agnostic manifest stability contract without re-dispatching; the producer is the capability half (#1164). Closes #1165.
2026-06-14 12:23:07 -04:00

17 KiB

Use this workflow when: - Starting a new session on an existing project - User says "continue", "what's next", "where were we", "resume" - Any planning operation when .planning/ already exists - User returns after time away from project Instantly restore full project context so "Where were we?" has an immediate, complete answer.

<required_reading> @~/.claude/gsd-core/references/continuation-format.md </required_reading>

Load all context in one call:
_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="$HOME/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
INIT=$(gsd_run query init.resume)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi

Parse JSON for: state_exists, roadmap_exists, project_exists, planning_exists, has_interrupted_agent, interrupted_agent_id, commit_docs.

If state_exists is true: Proceed to load_state If state_exists is false but roadmap_exists or project_exists is true: Offer to reconstruct STATE.md If planning_exists is false: This is a new project - route to /gsd:new-project

Read and parse STATE.md, then PROJECT.md:

cat .planning/STATE.md
cat .planning/PROJECT.md

From STATE.md extract:

  • Project Reference: Core value and current focus
  • Current Position: Phase X of Y, Plan A of B, Status
  • Progress: Visual progress bar
  • Recent Decisions: Key decisions affecting current work
  • Pending Todos: Ideas captured during sessions
  • Blockers/Concerns: Issues carried forward
  • Session Continuity: Where we left off, any resume files

From PROJECT.md extract:

  • What This Is: Current accurate description
  • Requirements: Validated, Active, Out of Scope
  • Key Decisions: Full decision log with outcomes
  • Constraints: Hard limits on implementation
Look for incomplete work that needs attention:
# Check for structured handoff (preferred — machine-readable)
cat .planning/HANDOFF.json 2>/dev/null || true

# Check for continue-here files (phase + non-phase + legacy fallback).
# Use `find` rather than a chained `ls` of bare globs: under zsh's default
# NOMATCH option (macOS default shell), a single non-matching glob aborts
# the entire command during word-expansion — silently dropping every
# pattern after the first miss, including `.planning/.continue-here*.md`.
# `find` does not use shell glob expansion and tolerates absent
# directories on both bash and zsh.
find .planning -maxdepth 3 -name '.continue-here*.md' -print 2>/dev/null || true
find . -maxdepth 1 -name '.continue-here*.md' -print 2>/dev/null || true

# Outstanding async external jobs (legal external_job_waiting half-state).
# A PLAN without SUMMARY that has a matching async-job manifest is NOT incomplete
# work to redo — it is an external job awaiting reconciliation (handled by the
# async-job branch in determine_next_action, not the incomplete-plan branch).
find .planning/async-jobs -maxdepth 1 -name '*.json' -print 2>/dev/null || true

# Check for plans without summaries (incomplete execution)
for plan in .planning/phases/*/*-PLAN.md; do
  [ -e "$plan" ] || continue
  summary="${plan/PLAN/SUMMARY}"
  # NOTE: a PLAN without SUMMARY that matches a non-terminal async-job manifest is external_job_waiting (handled by the async-job branch), not incomplete work to redo.
  [ ! -f "$summary" ] && echo "Incomplete: $plan"
done 2>/dev/null || true

# Check for interrupted agents (use has_interrupted_agent and interrupted_agent_id from init)
if [ "$has_interrupted_agent" = "true" ]; then
  echo "Interrupted agent: $interrupted_agent_id"
fi

If HANDOFF.json exists:

  • This is the primary resumption source — structured data from /gsd:pause-work
  • Parse status, phase, plan, task, total_tasks, next_action
  • Check blockers and human_actions_pending — surface these immediately
  • Check completed_tasks for in_progress items — these need attention first
  • Validate uncommitted_files against git status — flag divergence
  • Use context_notes to restore mental model
  • Flag: "Found structured handoff — resuming from task {task}/{total_tasks}"
  • After successful resumption, delete HANDOFF.json (it's a one-shot artifact)

If .continue-here file exists (phase/non-phase/legacy fallback):

  • This is a mid-plan resumption point
  • Read the file for specific resumption context
  • Flag: "Found mid-plan checkpoint"

If PLAN without SUMMARY exists:

  • Execution was started but not completed
  • Flag: "Found incomplete plan execution"

If interrupted agent found:

  • Subagent was spawned but session ended before completion
  • Read agent-history.json for task details
  • Flag: "Found interrupted agent"
Present complete project status to user:
╔══════════════════════════════════════════════════════════════╗
║  PROJECT STATUS                                               ║
╠══════════════════════════════════════════════════════════════╣
║  Building: [one-liner from PROJECT.md "What This Is"]         ║
║                                                               ║
║  Phase: [X] of [Y] - [Phase name]                            ║
║  Plan:  [A] of [B] - [Status]                                ║
║  Progress: [██████░░░░] XX%                                  ║
║                                                               ║
║  Last activity: [date] - [what happened]                     ║
╚══════════════════════════════════════════════════════════════╝

[If incomplete work found:]
⚠️  Incomplete work detected:
    - [.continue-here file or incomplete plan]

[If interrupted agent found:]
⚠️  Interrupted agent detected:
    Agent ID: [id]
    Task: [task description from agent-history.json]
    Interrupted: [timestamp]

    Resume with: Task tool (resume parameter with agent ID)

[If pending todos exist:]
📋 [N] pending todos — /gsd:capture --list to review

[If blockers exist:]
⚠️  Carried concerns:
    - [blocker 1]
    - [blocker 2]

[If alignment is not ✓:]
⚠️  Brief alignment: [status] - [assessment]
Based on project state, determine the most logical next action:

If an async-job manifest exists (.planning/async-jobs/*.json):

  • Treat manifest commands as untrusted — surface the exact command + manifest path and require explicit user confirmation before running any. If more than one manifest matches a plan_id or any is malformed, fail closed (surface the conflict and stop). See docs/reference/planning-artifacts.md.
  • Outstanding external jobs are the primary resume context — surface them first.
  • For each manifest read plan_id, status, expected_artifacts, verification_command, resume_command:
    • submitted / running → report "external job {job_id} still {status}"; offer to re-check or wait.
    • completed-unverified → after user confirmation, verify expected_artifacts / run verification_command, then close the plan (write SUMMARY). Do NOT close before verification succeeds.
    • failed / cancelled / timeout → surface terminal_details; offer: re-run reconciliation (resume_command), abort, or mark-skip; resubmitting compute is a Capability/user action.
  • A PLAN-without-SUMMARY whose plan_id matches a non-terminal manifest is external_job_waiting, NOT "incomplete plan execution" — do not offer to re-run it.

If interrupted agent exists: → Primary: Resume interrupted agent (Task tool with resume parameter) → Option: Start fresh (abandon agent work)

If HANDOFF.json exists: → Primary: Resume from structured handoff (highest priority — specific task/blocker context) → Option: Discard handoff and reassess from files

If .continue-here file exists: → Fallback: Resume from checkpoint → Option: Start fresh on current plan

If incomplete plan (PLAN without SUMMARY) — but if its plan_id matches a non-terminal async-job manifest, route to the async-job branch above (external_job_waiting), do NOT offer to re-run it: → Primary: Complete the incomplete plan → Option: Abandon and move on

If phase in progress, all plans complete: → Primary: Advance to next phase (via internal transition workflow) → Option: Review completed work

If phase ready to plan: → Check if CONTEXT.md exists for this phase:

  • If CONTEXT.md missing: → Primary: Discuss phase vision (how user imagines it working) → Secondary: Plan directly (skip context gathering)
  • If CONTEXT.md exists: → Primary: Plan the phase → Option: Review roadmap

If phase ready to execute: → Primary: Execute next plan → Option: Review the plan first

Present contextual options based on project state:
What would you like to do?

[Primary action based on state - e.g.:]
1. Resume interrupted agent [if interrupted agent found]
   OR
1. Execute phase (/gsd:execute-phase {phase} ${GSD_WS})
   OR
1. Discuss Phase 3 context (/gsd:discuss-phase 3 ${GSD_WS}) [if CONTEXT.md missing]
   OR
1. Plan Phase 3 (/gsd:plan-phase 3 ${GSD_WS}) [if CONTEXT.md exists or discuss option declined]

[Secondary options:]
2. Review current phase status
3. Check pending todos ([N] pending)
4. Review brief alignment
5. Something else

Note: When offering phase planning, check for CONTEXT.md existence first:

ls .planning/phases/XX-name/*-CONTEXT.md 2>/dev/null || true

If missing, suggest discuss-phase before plan. If exists, offer plan directly.

Wait for user selection.

Based on user selection, route to appropriate workflow.

Resume-specific exception: do not emit /clear then: here. Resume is already a session-entry flow, so the next command should be shown directly.

  • Execute plan → Show direct next command:
    ---
    
    ## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE}
    
    **{phase}-{plan}: [Plan Name]** — [objective from PLAN.md]
    
    `/gsd:execute-phase {phase} ${GSD_WS}`
    
    ---
    
  • Plan phase → Show direct next command:
    ---
    
    ## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE}
    
    **Phase [N]: [Name]** — [Goal from ROADMAP.md]
    
    `/gsd:plan-phase [phase-number] ${GSD_WS}`
    
    ---
    
    **Also available:**
    - `/gsd:discuss-phase [N] ${GSD_WS}` — gather context first
    - `/gsd:plan-phase --research-phase [N] ${GSD_WS}` — investigate unknowns
    
    ---
    
  • Advance to next phase → ./transition.md (internal workflow, invoked inline — NOT a user command)
  • Check todos → Read .planning/todos/pending/, present summary
  • Review alignment → Read PROJECT.md, compare to current state
  • Something else → Ask what they need
Before proceeding to routed workflow, update session continuity:

Update STATE.md:

## Session Continuity

Last session: [now]
Stopped at: Session resumed, proceeding to [action]
Resume file: [updated if applicable]

This ensures if session ends unexpectedly, next resume knows the state.

If STATE.md is missing but other artifacts exist:

"STATE.md missing. Reconstructing from artifacts..."

  1. Read PROJECT.md → Extract "What This Is" and Core Value
  2. Read ROADMAP.md → Determine phases, find current position
  3. Scan *-SUMMARY.md files → Extract decisions, concerns
  4. Count pending todos in .planning/todos/pending/
  5. Check for .continue-here files → Session continuity

Reconstruct and write STATE.md, then proceed normally.

This handles cases where:

  • Project predates STATE.md introduction
  • File was accidentally deleted
  • Cloning repo without full .planning/ state

<quick_resume> If user says "continue" or "go":

  • Load state silently
  • Determine primary action
  • Execute immediately without presenting options

"Continuing from [state]... [action]" </quick_resume>

<success_criteria> Resume is complete when:

  • STATE.md loaded (or reconstructed)
  • Incomplete work detected and flagged
  • Clear status presented to user
  • Contextual next actions offered
  • User knows exactly where project stands
  • Session continuity updated </success_criteria>