| name | gaia-sprint-plan |
| description | Plan a sprint by selecting stories from the backlog, applying sizing and priority rules via the sm subagent (Nate), and committing the sprint atomically to sprint-status.yaml via sprint-state.sh. GAIA-native replacement for the legacy XML engine workflow. |
| argument-hint | [sprint-scope] |
| allowed-tools | ["Read","Write","Edit","Grep","Glob","Bash","Skill"] |
| orchestration_class | heavy-procedural |
Orchestration Mode
SESSION_MODE=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/detect-orchestration-mode.sh")
WARNING_OUTPUT=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/orchestration-warning.sh" --skill-class heavy-procedural --mode "$SESSION_MODE")
if printf '%s' "$WARNING_OUTPUT" | grep -q '^SURFACE-WARNING: '; then
SENTINEL_PATH=$(printf '%s' "$WARNING_OUTPUT" | sed -n 's/^SURFACE-WARNING: //p' | head -n1)
cat "$SENTINEL_PATH"
fi
Surface contract. When the prelude cats a sentinel file — which happens once per session under Mode A (subagent dispatch) — you MUST mirror that cat'd warning text VERBATIM as the FIRST user-visible text of your response, before any skill-phase output. Claude Code auto-collapses Bash tool-call output, so the warning is invisible to users unless re-emitted as LLM turn text. Skip this step only when the prelude produced no sentinel output (Mode B, repeat invocation in same session, or out-of-scope skill class).
Setup
!${CLAUDE_PLUGIN_ROOT}/skills/gaia-sprint-plan/scripts/setup.sh
Mission
You are planning a sprint using the Nate (Scrum Master) persona. This skill reads the backlog from .gaia/artifacts/planning-artifacts/epics-and-stories.md, classifies stories by readiness, applies sizing and priority rules, and commits the finalized sprint atomically to sprint-status.yaml via sprint-state.sh. The skill MUST NOT write to sprint-status.yaml directly -- all state mutations go through sprint-state.sh.
This skill is the native Claude Code conversion of the legacy XML engine workflow. It delegates planning reasoning to the sm subagent and uses sprint-state.sh for atomic state updates.
How It Works
Sprint planning scans the backlog and builds a candidate set for user selection. Stories with priority_flag: "next-sprint" in their frontmatter are auto-included in the candidate set before user interaction begins. These pre-filled stories are annotated with [priority_flag: next-sprint] so the user can see why they were included. The user may deselect any auto-included story -- deselection preserves the flag for the next planning run. After sprint finalization, the flag is cleared (set to null) on all included stories; deselected flagged stories retain their flag.
The priority_flag field is set only by humans (via frontmatter edit in triage, correct-course, or add-feature). This skill only reads and clears the flag -- it never writes "next-sprint".
Critical Rules
- NEVER write to
sprint-status.yaml directly. All writes MUST go through sprint-state.sh. This is the sanctioned-writer contract.
- Backlog selection from epics-and-stories.md. Stories are selectable directly from the BACKLOG — the epics-and-stories.md roster columns — WITHOUT requiring a pre-materialized
ready-for-dev story file. This inverts the prior "ready-for-dev + existing file" precondition that forced a planner to run /gaia-create-story 40 times before sprint-plan would touch the stories. The dependency lint and capacity assessment read from the roster columns (Depends on / Blocks / Size / Points / Risk), not story files. JIT materialization of the selected stories happens after planning (/gaia-create-story --for-sprint). When a project DOES have materialized ready-for-dev files, they remain selectable too (backward-compat): resolve-story-file.sh resolution is still reachable for the files-present path.
- Column-sourced dependency lint. Run
${CLAUDE_PLUGIN_ROOT}/scripts/backlog-select-lint.sh --epics <epics-and-stories.md> --candidates "<co-selected keys>" --done "<done keys>" to validate the candidate set's dependencies from the roster Depends on column. A candidate HARD-BLOCKS when a hard-dep target is neither done nor co-selected in this sprint (cross-sprint dependency hard-block); soft-deps (; soft on …) and parenthetical annotations never block. The caller derives --done from closed-sprint history (sprint-archive yamls / epic-block status) and --candidates from the selection set — the lint is pure and reads only the columns. Capacity uses the agent-native check (sm-capacity-check.sh: depth + coherence + telemetry-gated wall-clock), NOT the points-vs-velocity heuristic.
- Commit as
planned. A finalized selection is committed as a status: planned sprint via sprint-state.sh (init seeds planned; a later layer adds the planned→active readiness gate) — NOT active. The legacy total_points <= velocity gate is no longer the capacity criterion.
- Sprint commitments respect the velocity estimate from the
sizing_map config key, resolved via !scripts/resolve-config.sh sizing_map.
- Use the sm subagent (Nate) persona for planning reasoning -- do not re-implement planning logic inline.
- NEVER auto-set
priority_flag: "next-sprint" on any story. Only humans set this flag. The skill reads and clears it only.
- Story status MUST only be changed via
transition-story-status.sh. Direct edits to status: fields in story frontmatter, sprint-status.yaml, epics-and-stories.md, story-index.yaml, or per-epic shards under .gaia/artifacts/planning-artifacts/epics/ are FORBIDDEN.
Steps
Step 0 -- Prior-close guard
Before sprint scoping begins, verify the previous sprint has been closed via /gaia-sprint-close. This prevents planning a new sprint while the prior sprint's sprint-status.yaml still shows status: active — which would orphan in-flight work and bypass the close ceremony.
- Resolve the previous sprint's yaml: search
.gaia/artifacts/implementation-artifacts/sprint-archive/ for the most recent *-closed-*.yaml, OR check the current .gaia/state/sprint-status.yaml for status: active (= prior sprint not yet closed).
- If
status: active (or status: field absent on a non-fresh tree), refuse with error: previous sprint {id} not closed; run /gaia-sprint-close first and exit non-zero.
- If user passes
--allow-stale-prior, skip the guard with a warning: warning: proceeding despite prior sprint {id} not closed (--allow-stale-prior).
- If
status: closed, proceed to Step 1.
- Backward-compat: if no previous sprint yaml exists (first sprint ever — fresh project), skip the guard silently.
Reference shell idiom (the SKILL.md should invoke this check via a small helper or inline grep; both are acceptable):
SS_YAML=".gaia/state/sprint-status.yaml"
if [ -r "$SS_YAML" ]; then
prior_status="$(grep '^status:' "$SS_YAML" | head -1 | sed 's/^status:[[:space:]]*//' | tr -d '"' || true)"
if [ "$prior_status" != "closed" ]; then
prior_id="$(grep '^sprint_id:' "$SS_YAML" | head -1 | sed 's/^sprint_id:[[:space:]]*//' | tr -d '"')"
if [ "${1:-}" != "--allow-stale-prior" ]; then
printf 'error: previous sprint %s not closed; run /gaia-sprint-close first\n' "$prior_id" >&2
exit 1
fi
printf 'warning: proceeding despite prior sprint %s not closed (--allow-stale-prior)\n' "$prior_id" >&2
fi
fi
Ground-truth staleness entry gate (BLOCKING). AFTER the prior-close guard and BEFORE any story selection, run the ground-truth staleness gate. Sprint-plan fires this gate at Step 0 ENTRY (placement asymmetry) so planning decisions operate on FRESH ground-truth. The gate sources the shared staleness predicate (never re-inline it) and BLOCKS on STALE:
bash ${CLAUDE_PLUGIN_ROOT}/skills/gaia-sprint-plan/scripts/ground-truth-gate.sh
- On STALE the gate exits non-zero and prints a diagnostic naming the stale validator-sidecar ground-truth plus the operator's next action: run
/gaia-refresh-ground-truth --incremental. HALT planning — do not proceed to story selection. After the operator runs the incremental refresh (a no-op when nothing changed) and the gate passes, planning resumes. This blocking gate is a hard precondition and is NOT bypassed by yolo or any best-effort flag.
- The auto-trigger instructs the INCREMENTAL refresh only — never
--agent all (the deferred manual-only full refresh).
- On FRESH the gate is silent and exits 0; proceed to Step 1.
Step 1 -- Load Epics, Stories, and Previous Retro
- Back-fill per-epic
story-index.yaml. Before parsing stories, run ${CLAUDE_PLUGIN_ROOT}/scripts/backfill-story-index.sh once. The helper scans every epic-*/ dir and replays transition-story-status.sh --reconcile-only for any story whose epic does not yet have a story-index.yaml (the symptom: bulk-authored stories existed under epic-*/{key}-{slug}/story.md but the per-epic index was never written, so downstream consumers that walk via the index missed them). The helper is idempotent and a no-op when every epic is already indexed; safe to call unconditionally.
- Read
.gaia/artifacts/planning-artifacts/epics-and-stories.md.
- Parse all stories with their priorities, sizes, and dependencies.
- Resolve individual story files via the shared
${CLAUDE_PLUGIN_ROOT}/scripts/resolve-story-file.sh helper. For each story key parsed from epics-and-stories.md, call the helper to get the resolved path, capturing both stdout (path) and stderr (WARNINGs). The helper centralizes the nested-over-flat precedence rule: it walks .gaia/artifacts/implementation-artifacts/epic-*/stories/{key}-*.md recursively as the canonical layer (the recursive idiom find "${IMPLEMENTATION_ARTIFACTS}" -path '*/stories/*.md' -type f -print0 is encapsulated inside the helper — bash globs do NOT recurse, so any non-recursive for f in .gaia/artifacts/implementation-artifacts/*.md loop would silently miss every nested story), and falls back to the legacy-flat layout .gaia/artifacts/implementation-artifacts/{key}-*.md with stderr WARNING: legacy-flat path — {flat_path}. Precedence rule: if a story file exists at BOTH layers for the same {key}, the nested file wins and the flat sibling is logged as WARNING: legacy-flat shadow ignored — {flat_path} (deterministic — no glob-ordering dependence). Helper exit codes: 0 = resolved (single hit), 1 = zero matches (the story file is missing — classify as NOT SELECTABLE), 2 = multi-match ambiguity (operator must resolve). For each resolved file, read its frontmatter status field.
- Classify stories into selectable and non-selectable. Backlog selection: a backlog story (a row in epics-and-stories.md, status
backlog, with NO individual file yet) is SELECTABLE directly from the roster; it does NOT require pre-materialization. JIT materialization (/gaia-create-story --for-sprint) runs AFTER planning. The resolve-story-file.sh resolution above still applies for the files-present (already-materialized) path — both are selectable:
- SELECTABLE (backlog, JIT): a roster row in epics-and-stories.md with no file yet — materialized after planning. No
/gaia-create-story precondition.
- SELECTABLE (materialized): stories with individual files AND
status: ready-for-dev (backward-compat, files-present path).
- NOT SELECTABLE (wrong status): a materialized story in a non-
ready-for-dev/non-backlog status (e.g. in-progress/review/done) is not re-selectable: "Story {key} is in '{status}' status -- not selectable."
- Dependency selectability is enforced by the column-sourced lint (
backlog-select-lint.sh, see Critical Rules) — a candidate whose hard dep is neither done nor co-selected is HARD-BLOCKED.
- Display the classification: selectable stories table (
Key | Title | Priority | Size | Risk | Status) and non-selectable stories with reasons.
- Priority-flag pre-scan: run
pflag_scan_backlog from ${CLAUDE_PLUGIN_ROOT}/scripts/priority-flag.sh against .gaia/artifacts/implementation-artifacts/. This returns all story keys whose frontmatter has status: backlog AND priority_flag: "next-sprint". Display these as a separate section: "Auto-included by priority_flag: [list of keys]". These stories are pre-filled into the candidate set in Step 3 before user selection. If no flagged stories are found, display "priority_flag: no flagged backlog stories found" and proceed normally.
- Hotfix active-sprint inject: run
pflag_scan_active_hotfix from ${CLAUDE_PLUGIN_ROOT}/scripts/priority-flag.sh against .gaia/artifacts/implementation-artifacts/. This returns all story keys with priority_flag: "hotfix" regardless of current status (backlog | in-progress | ready-for-dev). For each match, invoke bash ${CLAUDE_PLUGIN_ROOT}/scripts/sprint-state.sh inject --story <key> (sanctioned boundary writer) to add the story to the ACTIVE sprint. Emit one WARNING log line per injection: WARNING: hotfix story <key> injected into active sprint via priority_flag: "hotfix". Hotfix stories MUST still pass the full /gaia-run-all-reviews including wire-verification — a hotfix is faster to PLAN, NOT faster to TEST. sprint-state.sh inject is idempotent under lock — re-runs are no-ops via the existing yaml_has_story_key check. The capacity-exceeded case does NOT block hotfix injection — hotfix is a sanctioned bypass of normal sprint capacity.
- Load most recent
retro-{sprint_id}.md from .gaia/artifacts/implementation-artifacts/ if available. If retro found: extract open action items and present them as sprint constraints.
Step 1.5 -- Action-Item Escalation Halt
Before proceeding to sprint scoping, halt if any HIGH-priority action item has been open for two or more sprints (escalation_count >= 2). This forces systemic issues to resolution -- or conscious override -- before new sprint commitments are made.
Invocation:
!${CLAUDE_PLUGIN_ROOT}/scripts/escalation-halt.sh
AI_PATH="$(${CLAUDE_PLUGIN_ROOT}/scripts/lib/resolve-artifact-path.sh action_items \
--project-root "${CLAUDE_PROJECT_ROOT}")"
bash -c "source ${CLAUDE_PLUGIN_ROOT}/scripts/escalation-halt.sh && \
esch_check_blocking \
'${AI_PATH}' \
'${CLAUDE_PROJECT_ROOT}/.gaia/state/sprint-status.yaml'"
Contract:
- Reads
.gaia/state/action-items.yaml (canonical), resolved via resolve-artifact-path.sh action_items with a read-compat fallback to .gaia/artifacts/planning-artifacts/action-items.yaml.
- Filter predicate:
priority == "HIGH" AND escalation_count >= 2 AND status == "open" (case-sensitive).
- Exit 0 (proceed): no matching items, OR all matching items have a recorded override in the current
sprint-status.yaml.
- Exit 1 (halt): one or more matching items with no recorded override. Halt message on stdout lists each blocking item (
id, title, escalation_count, priority: HIGH) followed by exit guidance pointing to /gaia-action-items or the explicit override flag. No sprint-status.yaml mutation and no story-selection prompt occur when the halt fires.
Missing-file fallback (AC4): If action-items.yaml is absent, empty, or contains zero action items, emit a single-line stderr warning (NOTE: action-items.yaml not found at ... — escalation halt skipped) and proceed. The file is NOT created here -- creation is owned by upstream writers.
Override path (AC3): If the user re-invokes /gaia-sprint-plan with the explicit override, record it via sprint-state.sh and proceed:
${CLAUDE_PLUGIN_ROOT}/scripts/sprint-state.sh record-escalation-override \
--item-ids "AI-42,AI-77" \
--user "$(git config user.name || printf alice)" \
--reason "Acknowledged during sprint planning — owner committed to resolution this sprint"
Override metadata schema (appended under overrides: in sprint-status.yaml):
overrides:
- date: "2026-04-22"
user: "alice"
override_type: escalation_halt
overridden_item_ids:
- "AI-42"
- "AI-77"
reason: "Acknowledged during sprint planning"
The override is idempotent on the dedup key (sprint_id, sorted-unique(overridden_item_ids), override_type) -- re-running with the same still-open items and a prior recorded override does NOT re-halt and does NOT append a duplicate entry.
Rollback toggle: set GAIA_ESCALATION_HALT=off in the environment to bypass the halt entirely (for emergency rollout if a schema regression in action-items.yaml appears). Default: enabled.
Cross-refs:
- This step implements the halt gate.
- Upstream writers of
action-items.yaml (/gaia-retro, /gaia-correct-course, /gaia-triage-findings).
- All
sprint-status.yaml writes (including override recording) go through sprint-state.sh; this skill never writes yaml inline.
- When reconciliation has landed, it runs before this halt; if not, the halt still functions because it reads
action-items.yaml, not sprint-status.yaml.
Step 2 -- Sprint Scoping
- Ask: Sprint duration (1 week / 2 weeks / custom)?
- Ask: Team velocity estimate (story points)?
- Ask: Sprint number (for multi-sprint tracking)?
- Resolve the
sizing_map key via !scripts/resolve-config.sh sizing_map (the resolver transparently merges the team-shared and machine-local layers, applying the "local overrides shared" precedence). Display the canonical point values (S/M/L/XL) before selection.
Step 3 -- Story Selection
- Select stories for this sprint based on priority ordering (P0 > P1 > P2) and dependency topology -- only from stories classified as SELECTABLE in Step 1.
- Priority-flag pre-fill: any story keys returned by the priority-flag pre-scan in Step 1 are pre-selected in the candidate set before user interaction. Annotate each auto-included entry with
[priority_flag: next-sprint] so the user sees why it was pre-filled. The user may deselect any pre-filled story -- deselection preserves the flag for the next planning run.
- Agent-native capacity check. The "is this sprint too big" gate is evaluated on three agent-native measures — NOT the human points-per-duration heuristic (which false-flagged a large points-heavy sweep). Run
${CLAUDE_PLUGIN_ROOT}/scripts/sm-capacity-check.sh --stories-file <candidate-set> (one KEY|DEPS|POINTS line per candidate) and read its verdict: (1) dependency critical-path depth (longest serial chain over depends_on), (2) context-coherence ceiling (distinct story count the agent can carry before quality degrades / forced compaction), and (3) telemetry-gated measured agent wall-clock (median minutes/story × story count) vs a configured agent-session budget. A sprint is "too big" only when one of these three measures is exceeded. Cold start (no closed-sprint telemetry) uses depth + coherence only, with no fabricated constant. Story points are RETAINED as the relative complexity/risk signal (review rigor, Val scrutiny, sizing display) — but total_points <= velocity is NO LONGER the capacity gate.
- Dependency blocking: for each candidate, check its
depends_on list. If any dependency is NOT done, the story CANNOT be included. Display: "BLOCKED: Story {key} depends on {dep_key} (status: {dep_status})."
- Priority surfacing: after selection, check for P0 stories that are
ready-for-dev but NOT selected. If any found, warn: "WARNING: P0 stories ready but not selected:" and ask user to confirm the exclusion.
- Resolve the test-plan via the strategy-fallback rule: try
.gaia/artifacts/test-artifacts/test-plan.md (flat); fall back to .gaia/artifacts/test-artifacts/strategy/test-plan.md (strategy/ placement). If the resolved file exists: apply risk levels -- buffer 20% for high-risk stories.
- ATDD check (high-risk only): for each high-risk story, check if an ATDD file exists at the resolver-returned path (
bash ${CLAUDE_PLUGIN_ROOT}/scripts/lib/resolve-test-artifact-per-story.sh atdd {story_key} --existing-only — accepts the new per-story test-artifacts/epic-{epic_slug}/stories/{key}-{slug}/atdd.md home and the legacy flat test-artifacts/atdd-{story_key}.md fallback). If the resolver exits 1 (no rung exists): "HIGH-RISK story {key} has no ATDD file -- run /gaia-atdd {key} before development."
- Manual-verification surfacing: for each selected story, read its
manual_verification frontmatter flag and annotate the candidate row with [manual_verification] when the flag is true — so the manual-test gate is a visible, bound part of the sprint, not a silent flag discovered only at review. Use the read-only helper ${CLAUDE_PLUGIN_ROOT}/scripts/manual-verification-scan.sh scan-keys <implementation_artifacts> <selected keys...> to list the flagged keys (or … annotate <story_file> per row). This is a display annotation, not a new state field — symmetric with the [priority_flag: next-sprint] annotation above. The flag is set at authoring time (/gaia-create-story's surface-aware offer); sprint-plan only reads it.
- Present the candidate sprint to the user and capture confirmation.
Step 4 -- Update Story Files
-
For each selected story with an individual file, set the sprint_id field to sprint-{N} via one of the two sanctioned helpers (do NOT hand-edit the frontmatter):
${CLAUDE_PLUGIN_ROOT}/scripts/set-story-sprint.sh {story_key} --sprint sprint-{N}
${CLAUDE_PLUGIN_ROOT}/scripts/sprint-state.sh set-story-sprint --story {story_key} --sprint sprint-{N}
Both writers rewrite only the sprint_id: scalar under a per-story flock (atomic tmp+mv; insert when absent). They are functionally interchangeable — the sprint-state.sh set-story-sprint form is the canonical surface for new callers because it keeps every sprint-binding mutation inside the same script that owns inject, while the standalone set-story-sprint.sh remains as a stable lower-level primitive. Either form is the field that sprint-state.sh inject's drift guard reads, so it MUST be set before the inject/commit step. Clear it with --sprint null (standalone) or use sprint-state.sh rollover --from sprint-{N} --keys {story_key} to move between sprints.
-
This step sets ONLY the sprint_id binding. Story status is reconciled in
Step 4a below: a backlog-selected story is Val-validated and transitioned to
ready-for-dev there; an already-ready-for-dev materialized story is left
unchanged. Do NOT hand-edit the status: field here. /gaia-dev-story
transitions a ready-for-dev story to in-progress when work begins.
Step 4a -- Val-gated activation (backlog → ready-for-dev)
Backlog selection (Step 1) lets a status: backlog story be selected directly
from the roster, but /gaia-dev-story HALTs unless the story frontmatter is
ready-for-dev or in-progress. This step closes that gap: every selected
story is validated and, on PASS, transitioned to ready-for-dev so the sprint
is actually startable. Activation is single-path — sprint-plan owns
backlog → ready-for-dev; /gaia-dev-story FRESH mode owns
ready-for-dev → in-progress (the backlog → in-progress edge is reserved for
pure backlog devs with sprint_id: null).
For each selected story whose materialized status is backlog (skip stories
already ready-for-dev — they are activation-complete):
- Materialize if roster-only. A backlog story selected as a roster row with
no file yet MUST first be materialized via
/gaia-create-story {story_key} --for-sprint sprint-{N} so there is a story file to validate. (Already-filed
backlog stories skip this.)
- Validate via
/gaia-validate-story {story_key}. This is the sanctioned
full Val gate (main-turn Agent-tool dispatch + envelope assert + 3-attempt
cap). It records its verdict via review-gate.sh. Do NOT inline-judge.
- On FAIL — SM fix loop (≤3 attempts). Dispatch the SM (Nate) to fix the
story file against Val's findings, then re-validate. Repeat up to 3
attempts total per story (matching the
/gaia-validate-story cap). Each
attempt's findings drive the next fix; do NOT pre-load prior findings into a
re-dispatch prompt.
- On PASS (within ≤3 attempts) — transition. Run
${CLAUDE_PLUGIN_ROOT}/scripts/transition-story-status.sh {story_key} --to ready-for-dev. This sanctioned single-writer atomically updates all five
surfaces (story frontmatter, sprint-status.yaml, epics-and-stories.md,
the per-epic shard under planning-artifacts/epics/, and story-index.yaml)
— keeping the yaml mirror and the story frontmatter in agreement.
- On still-FAIL after 3 attempts — leave at
backlog. Record the story key
and its outstanding Val findings.
Commit partition + can't-start-with-backlog HALT. Stories that reach
ready-for-dev are committed into the sprint normally (Step 6). If ANY selected
story remains backlog after this step, the sprint cannot start: surface
each still-backlog story with its Val findings and HALT, asking the user to
decide per story — remove it from the sprint, or fix it and re-validate
manually (/gaia-fix-story {key} then re-run planning). Do NOT auto-drop or
auto-defer. The passing subset has already been transitioned; only the
unresolved stories block the sprint start.
Step 5 -- Sprint Plan Generation
- Create the sprint plan with story assignments and execution order, ordered by dependency resolution + priority.
- Generate a Sprint Burndown Estimate table:
Day | Points Remaining | Stories Completing.
- Include: sprint goals, selected stories (ordered), velocity target, risk assessment, and a Testing Readiness section listing ONLY high-risk stories with their ATDD file status.
Step 6 -- Commit Sprint via sprint-state.sh
- Generate
sprint-status.yaml content with the standardized schema:
sprint_id: "sprint-{N}"
duration: "{duration}"
velocity_capacity: {velocity}
total_points: {sum}
start_date: "{start_date YYYY-MM-DD}"
end_date: "{end_date YYYY-MM-DD}"
started: "{date}"
stories:
- key: "{story-key}"
title: "{title}"
status: "ready-for-dev"
points: {points}
risk_level: "{risk}"
assignee: null
blocked_by: null
updated: "{date}"
- Write
sprint-status.yaml to .gaia/state/sprint-status.yaml EXCLUSIVELY via sprint-state.sh:
SPRINT_YAML=".gaia/state/sprint-status.yaml"
[ -e "$SPRINT_YAML" ] || \
${CLAUDE_PLUGIN_ROOT}/scripts/sprint-state.sh init --sprint-id "{sprint_id}" --start-date "{start_date YYYY-MM-DD}" --end-date "{end_date YYYY-MM-DD}"
${CLAUDE_PLUGIN_ROOT}/scripts/sprint-state.sh inject \
--story "{story_key}" [--sprint-id "{sprint_id}"]
Invoke init once (guarded on yaml absence), then inject once per selected story to register it in the sprint. The init subcommand seeds the canonical shape (sprint_id/state: active/total_points: 0/goals: []/items: []). The inject subcommand appends the story's metadata mirrored from the story file's frontmatter — the four required fields (sprint_id, status, points, risk) MUST be present in the story file before the call. inject is idempotent — re-running on an already-registered key is a no-op. Use transition only for in-sprint status changes after the entry exists. If sprint-state.sh exits non-zero, abort cleanly and surface the error to the user. Do NOT fall back to direct YAML writes.
Step 6a -- Sprint Goal Routing
After Step 6 commits the sprint to sprint-status.yaml, present the 3-lane goal router to the user so the sprint carries an explicit goals[] list (consumed downstream by /gaia-sprint-review Track A Val rubric scoring). The auto-suggested goals are the union of selected stories' acceptance-criteria headlines.
This step is gated on the sprint having at least one selected story — if zero stories were selected at Step 3, skip the goal router silently (preserves the backward-compat invariant).
Goal consolidation. When the auto-suggested goals map 1:1 to single stories — one goal per story — collapse them into fewer outcome-level goals (target ≤2 where the stories share an intent) BEFORE persisting. A goal should read as an outcome the sprint delivers, not as a restatement of a single story title; a long list of one-story goals trips the velocity-distribution rubric on mechanics rather than intent. Group stories that advance the same outcome under one goal; persist the consolidated set via set-goals. Consolidation is judgement-led — if the selected stories genuinely advance unrelated outcomes, leave them as distinct goals (the shape detector below then handles the sweep case).
The router presents AskUserQuestion at main-turn with the canonical 3-lane menu (main-turn-only invariant):
user-direct — the user edits the suggested goals inline via a follow-up AskUserQuestion (free-form) and the result persists via sprint-state.sh set-goals --sprint <id> --goals "<g1|g2|...>" (boundary writer; never direct yq -i). --goals is PIPE-DELIMITED, not JSON — cmd_set_goals parses it with IFS='|'. Pass "Goal one|Goal two|Goal three", not a JSON array.
pm-route — dispatch Val via the main-turn Agent tool with the AI-1 sprint-review rubric at gaia-framework/plugins/gaia/rubrics/base/sprint-review.json to score the suggested goals against the selected stories' ACs. Display Val's verdict + findings inline, then present a follow-up AskUserQuestion directed at the USER (NOT the PM) for the final accept — this preserves the PM-cannot-self-approve invariant. On user accept: sprint-state.sh set-goals. The PM may DRAFT but the USER ratifies.
yolo — dispatch Val identically to the pm-route lane. On Val PASSED: auto-accept and persist via sprint-state.sh set-goals. On Val FAILED: HALT with the findings list — YOLO MUST NOT bypass a FAILED verdict.
Step 6c -- Sprint-Shape Detection
After the goal set is finalized (Step 6a), detect whether the committed sprint is sweep-shaped (goals map 1:1 to single stories) or facet-decomposed (one epic split into serial facets). These shapes were never multi-story outcome sprints, so the review-time incidental-goal / velocity-distribution floor — built for multi-story outcome goals — would otherwise fire spuriously against them. Detecting the shape at planning time and stamping it lets /gaia-sprint-review relax the floor for the shape instead of FAILing on mechanics.
The detector is pure, read-only, and cold-start safe — it resolves the shape from the committed story selection and the final goal count ALONE (epic membership comes from the story key), with NO dependency on closed-sprint telemetry. Run it against the committed selection and the finalized goal count:
if SHAPE="$(${CLAUDE_PLUGIN_ROOT}/scripts/detect-sweep-shape.sh \
--stories "{committed_keys_csv}" --goals "{n_goals}")"; then
${CLAUDE_PLUGIN_ROOT}/scripts/sprint-state.sh set-shape \
--sprint "{sprint_id}" --shape "$SHAPE"
fi
- On a positive verdict (exit 0), stamp the shape via the sanctioned
sprint-state.sh set-shape boundary writer — NEVER write sprint_shape: to sprint-status.yaml directly. set-shape is the single sanctioned writer for that field.
- On the no-stamp default (exit 10), leave the shape unset — the sprint keeps the default (thrust) treatment and the review floor applies normally. The predicate is deliberately conservative: a false negative (not stamping a genuine sweep) is safer than a false positive (suppressing a real multi-outcome sprint's floor).
- The detector reads only the keys and the goal count, so it works on the first-ever sprint of a fresh project with no history.
Step 6b -- Dependency Inversion Lint
Step 6c -- Priority-Flag Clear
- After sprint finalization (sprint-status.yaml committed), iterate the set of stories that landed in the sprint.
- For each included story, use
pflag_read from ${CLAUDE_PLUGIN_ROOT}/scripts/priority-flag.sh to check if priority_flag is "next-sprint".
- For each included story with
priority_flag: "next-sprint", call pflag_clear to rewrite the frontmatter to priority_flag: null. This is a line-targeted rewrite that preserves all other frontmatter fields byte-for-byte.
- Deselection preservation (AC2): stories that were flagged but deselected (excluded from the sprint) are NOT cleared. Their
priority_flag: "next-sprint" persists so the next planning run auto-includes them again.
- Failure isolation: if
pflag_clear fails on one story (permission error, malformed frontmatter), log a warning and continue clearing the remaining stories. Do NOT abort the sprint-plan run.
- After all clears, call
pflag_record_cleared from the same script to append a priority_flag_cleared: block to sprint-status.yaml listing the cleared story keys. If no stories were cleared, record an empty array.
- Emit a summary line:
"priority_flag cleared on {N} included stories; {M} deselected flagged stories retained their flag."
Step 7 -- Save Sprint Plan Document
- Run
mkdir -p .gaia/artifacts/implementation-artifacts/sprint-plan/ so the nested directory exists on first run.
- Write the sprint plan document to
.gaia/artifacts/implementation-artifacts/sprint-plan/{sprint_id}-plan.md.
- The document includes all sections from Step 5.
Step 8 -- Sprint-plan consistency check
Per-story factual validation is performed in Step 4a (the blocking Val gate
that activates each story backlog → ready-for-dev). This step is the
plan-level consistency pass over the committed sprint — it does NOT re-validate
individual stories:
- Every committed story is
ready-for-dev (Step 4a guarantees this — any story
that could not reach ready-for-dev blocked the sprint start and is therefore
absent from the committed set). Assert the invariant holds.
- Dependency ordering is correct.
- Points sum is recorded (relative-complexity signal) and the agent-native
capacity check (
sm-capacity-check.sh: depth + coherence + telemetry-gated
wall-clock) did not flag the batch — the legacy total <= velocity points-gate
is NOT the capacity criterion.
- No duplicate story keys.
If the consistency check surfaces a committed story that is NOT ready-for-dev,
that is a Step 4a contract violation — HALT and re-run Step 4a; do not log-and-
continue. (The per-story Val blocking happens in Step 4a; this plan-level pass
is the backstop invariant, not an optional re-check.)
Step 9 -- Token Footprint Measurement
- Record the skill's token footprint for tracking. This measurement becomes input to the aggregate reporting.
- Log: skill name, step count, approximate token usage vs. the legacy XML engine invocation.
Step 10 -- Report
- Display the finalized sprint summary: sprint ID, duration, velocity, stories selected, total points, capacity utilization.
- Suggest next step:
/gaia-dev-story {first_story_key} to begin the first story.
Step 11 — Persist to Val Sidecar
Final step. Delegates Val-decision persistence to the shared Val sidecar writer helper (val-sidecar-write.sh). Placing this last satisfies the atomicity requirement — any upstream failure (sprint-state transition, dependency-inversion lint error, Val validation failure in Step 8) short-circuits before the helper runs, so no partial sidecar entry can appear.
Read sprint_id via the shared sprint-state.sh foundation script — never parse sprint-status.yaml directly (project hard rule):
sprint_id="$(${CLAUDE_PLUGIN_ROOT}/scripts/sprint-state.sh current-sprint --field sprint_id 2>/dev/null || echo 'N/A')"
Build the decision payload as {verdict, findings[], artifact_path} using the Step 8 Val verdict (or verdict: "skipped" if Val was unavailable) and the sprint-plan artifact path from Step 7.
Invoke the helper:
${CLAUDE_PLUGIN_ROOT}/scripts/val-sidecar-write.sh \
--command-name "/gaia-sprint-plan" \
--input-id "${sprint_id}" \
--sprint-id "${sprint_id}" \
--decision-payload "$(jq -cn \
--arg verdict "${verdict:-skipped}" \
--arg artifact_path ".gaia/artifacts/implementation-artifacts/sprint-plan/${sprint_id}-plan.md" \
--argjson findings "${findings_json:-[]}" \
'{verdict: $verdict, findings: $findings, artifact_path: $artifact_path}')"
The helper enforces the two-file allowlist and idempotency by composite (command_name, input_id, decision_hash) key — re-runs with identical payload yield status=skipped_duplicate and must be treated as success.
Failure posture: if the helper rejects or errors, log a warning and continue — memory persistence is best-effort and MUST NOT fail the skill.
Mode B Readiness
Driving teammate turns (MANDATORY under team orchestration). Declaring
readiness above sets up the spawn / relay / shutdown bookkeeping seams — it does
NOT by itself drive a teammate. When SESSION_MODE == team, the orchestrator
MUST drive each teammate turn per the canonical Mode B teammate round-trip
contract at knowledge/mode-b-round-trip-contract.md: emit a real
SendMessage(to: <handle>) whose message ends with the reply-routing reminder,
let the teammate reply via SendMessage(to: team-lead) (one-shot re-prompt on
idle-without-reply; never fabricate the reply), then relay the received body to
the transcript / artifact. The bridge functions named above are bookkeeping
only; the round-trip itself is an orchestrator-driven, main-turn loop.
No discretionary Mode A fall-through. The team-mode round-trip is mandatory
when the session resolves to team orchestration — "it is a small / focused /
quick step" is NOT a license to fall back to one-shot Mode A, and a slow reply
is the cross-turn-boundary case (wait or re-prompt once), not a fallback
trigger. The ONLY legitimate fall-through is a real MODE_B_FALLBACK token
emitted by the bridge at spawn time (substrate genuinely unavailable).
This skill is Mode B-ready. Under the team-orchestration mode, the sm-subagent work that the prose above describes as inline subagent dispatch is instead routed through the shared execution bridge library at ${CLAUDE_PLUGIN_ROOT}/scripts/lib/execution-mode-b-bridge.sh, which itself layers on the shared dispatch library ${CLAUDE_PLUGIN_ROOT}/scripts/lib/dispatch-teammate.sh.
- Spawn seam. The sm subagent (Nate) performs sizing and priority selection. The orchestration calls
execution_spawn_subagent gaia:sm "gaia-sprint-plan" to obtain a persistent teammate handle. The clean-room gate in the shared library refuses any reviewer persona before a teammate is created.
- Relay seam. Each planning turn is relayed verbatim to the team lead via
execution_relay_turn <handle> <payload>. The committed sprint-status.yaml is written identically to Mode A — only the dispatch seam differs, never the committed output.
- Shutdown seam. At skill exit the orchestration calls
execution_shutdown, which delegates to shutdown_all so no teammate pane is left orphaned.
- Honest fallback. Live Mode B is not exercisable in every Claude Code context. When the substrate is absent the bridge degrades to the existing Mode A foreground dispatch and emits a single
MODE_B_FALLBACK token to stderr; the Mode A behaviour documented above remains the source of truth.
Finalize
!${CLAUDE_PLUGIN_ROOT}/skills/gaia-sprint-plan/scripts/finalize.sh