name: ship-sdlc
description: "Use this skill when shipping a feature end-to-end after plan acceptance: executing, committing, reviewing, fixing critical issues, versioning, and opening a PR in one flow. Dispatches every sub-skill (including execute-plan-sdlc) as an Agent for context isolation, with structured return values driving the pipeline state machine. Arguments: [--auto] [--steps ] [--quick] [--quality full|balanced|minimal] [--bump patch|minor|major|
Ship Pipeline
End-to-end feature shipping: execute plan, commit, review, fix critical issues, version, and open a PR. Chains six sub-skills sequentially with a conditional review-fix loop.
Announce at start: "I'm using ship-sdlc (sdlc v{sdlc_version})." — extract the version from the sdlc: line in the session-start system-reminder. If no version is in context, omit the parenthetical.
Step 0 — Plan Mode Check
If the system context contains "Plan mode is active":
- Locate
skill/ship.js(samefindpattern used in Step 1c below). - Invoke:
SCRIPT=$(find ~/.claude/plugins -name "ship.js" -path "*/sdlc*/scripts/skill/ship.js" 2>/dev/null | sort -V | tail -1) [ -z "$SCRIPT" ] && [ -f "plugins/sdlc-utilities/scripts/skill/ship.js" ] && SCRIPT="plugins/sdlc-utilities/scripts/skill/ship.js" [ -z "$SCRIPT" ] && { echo "ERROR: Could not locate skill/ship.js. Is the sdlc plugin installed?" >&2; exit 2; } PLAN_MODE_OUTPUT_FILE=$(node "$SCRIPT" --output-file --plan-mode-blocked $ARGUMENTS) PLAN_MODE_EXIT=$? echo "PLAN_MODE_OUTPUT_FILE=$PLAN_MODE_OUTPUT_FILE" echo "PLAN_MODE_EXIT=$PLAN_MODE_EXIT" - If
PLAN_MODE_EXITis non-zero: show any errors from the output file and stop. - Read the output JSON from
$PLAN_MODE_OUTPUT_FILE. ConfirmplanModeBlocked === true. ExtractstateFile,flags.bump,flags.steps. - Announce:
Plan mode is active. ship-sdlc requires write operations (git commit, gh pr create, git tag) and cannot run inside plan mode.
Pipeline state saved to
<stateFile>with resolved flags: bump=<flags.bump>, steps=<flags.steps>.Exit plan mode and re-invoke
/ship-sdlc(no args needed) — the existing implicit-resume mechanism will pick up the saved state and resume from the first pending step with the originally-resolved flags intact. - Run
rm -f "$PLAN_MODE_OUTPUT_FILE"to clean up the temp output file. - Stop. Do not proceed to subsequent steps.
All gates in steps 3–5 cite resolved fields from prepare output (planModeBlocked, stateFile, flags.bump, flags.steps) — never re-parse $ARGUMENTS directly.
Step 1 (CONSUME): Load Config, Parse Flags, Detect Context
1a. --init-config handler
If --init-config was passed → Read ./entry-modes.md (--init-config section) and follow it. Do not read preemptively. The handler short-circuits — no pipeline execution.
1a-gc. --gc handler (R39, issue #223)
If --gc (with optional --ttl-days <N>) was passed → Read ./entry-modes.md (--gc section) and follow it. Do not read preemptively. The handler short-circuits — the pipeline does not run.
1b. Load ship config
Hook context fast-path: If the session-start system-reminder contains a Ship config: line, note it for display. The prepare script (skill/ship.js) remains the authoritative source for config values — the hook line is a user-facing heads-up, not a data source.
Check for ship config via skill/ship.js output (reads from .sdlc/local.json → ship section, with legacy .sdlc/ship-config.json fallback). If found, read and merge. Print a single compact summary line, e.g.:
Ship config (schema v2): steps=[execute, commit, review, archive-openspec, pr], draft=false, bump=patch, reviewThreshold=high, execute.commitWaves=false
The execute.commitWaves field (Fixes #392 / R35) controls per-wave WIP commits during the execute step. Default false. When set to true in ship config, --commit-waves is appended to the execute step's invocation; the execute-plan-sdlc skill then runs git add -A && git commit -m "wip(execute): wave N — <titles>" after each wave's G9 + G11 pass. The subsequent commit step (commit-sdlc) detects the wip(execute): commits since fork-point and squashes them via soft-reset into the final feature commit, so the user-facing PR history is unchanged. Resolution is centralized in scripts/skill/ship.js (per scripts-over-llm-logic and flag-coherence-cross-skill guardrails) — SKILL.md cites step.invocation, never raw config.execute.commitWaves.
If not found: No ship config found — using built-in defaults. Run /setup-sdlc to configure.
Legacy v1 auto-migration: If the loader detects a v1 config (no top-level version, with ship.preset or ship.skip), it migrates in place to schema v2 and emits a single stderr deprecation notice. The migrated shape (ship.steps[]) is what subsequent steps consume.
1c. Prepare pipeline context
Locate and run skill/ship.js with all CLI flags to pre-compute flags, context, and step statuses:
SCRIPT=$(find ~/.claude/plugins -name "ship.js" -path "*/sdlc*/scripts/skill/ship.js" 2>/dev/null | sort -V | tail -1)
[ -z "$SCRIPT" ] && [ -f "plugins/sdlc-utilities/scripts/skill/ship.js" ] && SCRIPT="plugins/sdlc-utilities/scripts/skill/ship.js"
[ -z "$SCRIPT" ] && { echo "ERROR: Could not locate skill/ship.js. Is the sdlc plugin installed?" >&2; exit 2; }
PREPARE_OUTPUT_FILE=$(node "$SCRIPT" --output-file --has-plan --auto)
# Hook signal: if the session-start system-reminder contains a line matching
# `/^Active pipeline: ship-sdlc/`, ALSO append `--hook-active-pipeline` to the
# invocation above. When no state file is found, prepare emits
# errors[*].id === "implicitResumeNoState" (handled in Step 1e).
EXIT_CODE=$?
echo "PREPARE_OUTPUT_FILE=$PREPARE_OUTPUT_FILE"
echo "EXIT_CODE=$EXIT_CODE"
trap 'rm -f "$PREPARE_OUTPUT_FILE"' EXIT INT TERM
Parse the output JSON from $PREPARE_OUTPUT_FILE. If errors is non-empty, display them and stop. The parsed output replaces manual computation in subsequent sub-steps (1d–1g).
Context-heaviness advisory (implements R35): If the parsed output's top-level contextAdvisory field is a non-empty string, print it verbatim before continuing. The advisory recommends /compact and notes that pipeline state is preserved across compaction (PreCompact + SessionStart hooks). Sourced from $TMPDIR/sdlc-context-stats.json, written by the UserPromptSubmit hook (hooks/context-stats.js); helper at scripts/lib/context-advisory.js. When contextAdvisory is null, emit nothing.
Gitignore warning: If context.sdlcGitignored is false in the output, print:
⚠ Warning: .sdlc/ is not gitignored. Run --init-config to fix, or manually create .sdlc/.gitignore:
printf '*\n' > .sdlc/.gitignore
1d. Parse flags
Print the flags object from the skill/ship.js output, including the sources map showing where each value came from (CLI, config, or default):
Flag resolution (from skill/ship.js):
auto: true (source: cli)
steps: [execute, commit, review, archive-openspec, pr] (source: config)
preset: balanced (source: cli, legacy sugar; expanded to steps)
bump: patch (source: default)
draft: false (source: default)
1e. Resume check
Hook context fast-path: If the session-start system-reminder contains an Active pipeline: line, note the state file path and resume point. When the user does not pass --resume explicitly but the hook reported an active pipeline, the Step 1c invocation already appended --hook-active-pipeline (see comment above). The prepare script then either sets flags.implicitResume === true (state file found and fresh) or returns errors[*].id === "implicitResumeNoState" (state file missing). The LLM does NOT scan the filesystem — skill/ship.js is authoritative.
Print resume.found and resume.stateFile from the skill/ship.js output. If resume.found is true, print the state file path and resume point. If false, print that no state file was found and the pipeline will start fresh.
Implicit-resume banner (R-implicit-resume, #359): When flags.implicitResume === true in the prepare output, print the following banner verbatim BEFORE the pipeline table (Step 2). Source <nextPendingStep> from resume.nextPendingStep (provided by detectResumeState() in lib/state.js) and source the step lists from the state file at resume.stateFile:
Resuming after compaction from step <nextPendingStep>.
Completed: <comma-separated step names where status === "completed">.
Pending: <comma-separated step names where status !== "completed" && status !== "skipped">.
Note: the banner check gates on flags.implicitResume, NOT flags.resume. The prepare script auto-sets flags.resume = true when flags.implicitResume === true so the rest of the pipeline (e.g. Step 5's execute resume forwarding) sees a unified flags.resume regardless of whether the user typed --resume or the hook triggered it.
Missing-state prompt (R-implicit-resume): If the prepare output's errors array contains an entry with id === "implicitResumeNoState", use AskUserQuestion:
Active pipeline reminder found but no state file for current branch. Start fresh, or specify a state path?
Options:
- fresh — re-invoke
skill/ship.jswithout--hook-active-pipelineso the pipeline starts cleanly - path — ask the user for an explicit state file path, then re-invoke with
--state-file <path> - abort — exit cleanly without dispatching any step
Read ./state-format.md when resuming from a state file.
1f. Context detection
Print the context object values from the skill/ship.js output:
Context detection (from skill/ship.js):
Plan in context: yes
Uncommitted changes: 14 files modified
Current branch: feat/ship-sdlc
Default branch: main
gh CLI: authenticated as <user>
OpenSpec: not detected
.sdlc/ gitignored: yes
Contradictory-signal override (implements R21): After printing the context detection block, IF context.openspecAuthoritative.path is set AND the current session-start <system-reminder> contains a line matching /openspec.*not initialized|not initialized.*openspec/i, print exactly one line:
Ignoring contradictory 'not initialized' signal in session context — openspec/config.yaml exists (authoritative source: SDLC's own check via ship.js prepare output).
Then continue the flow. If the contradictory phrase is absent, emit nothing.
1g. Auto-skip logic
Print each step from the steps array in the skill/ship.js output with its status, reason, and skipSource:
Auto-skip decisions (from skill/ship.js):
execute: will_run — plan detected in context
commit: will_run — uncommitted changes detected
review: will_run — not in skip set
received-review: conditional — depends on review verdict
commit (fixes): conditional — depends on received-review changes
version: skipped (auto) — auto-skipped — tags are repo-global
verify-openspec: skipped (default) — not in steps[]
archive-openspec: conditional — openspec change ready for archive
pr: will_run — not in skip set
The parenthetical after skipped reflects the step's skipSource field:
(cli)— user passed--stepson the command line(quick)— step is canonical but absent fromship.quickunder an active--quickrun (R-quick-4);flags.sources.steps === 'quick'in the prepare output(config)— skip set loaded from.sdlc/local.json(auto)— auto-skipped bycomputeStepslogic (e.g., worktree mode)(condition)— conditional step whose condition was not met
Steps with skipSource: "none" are not skipped and show no parenthetical.
The LLM does not compute these statuses — skill/ship.js is the source of truth.
Step 2 (PLAN): Build Pipeline Plan
The pipeline table is generated from the steps array in the skill/ship.js output. Each row maps:
- Step number: array index + 1
- Skill:
step.skill - Status:
step.status - Args:
step.args - Pause:
step.pause ? 'YES' : 'no'
| Step | Skill | Status | Args | Pause |
|---|---|---|---|---|
| 1 | execute-plan-sdlc | will_run | (none, or --quality <X> if user passed --quality to ship) |
no |
| 2 | commit-sdlc | will_run | --auto |
no |
| 3 | review-sdlc | will_run | --committed |
no |
| 4 | received-review-sdlc | conditional | (if crit/high) | YES |
| 5 | commit-sdlc (fixes) | conditional | --auto |
no |
| 6 | version-sdlc | skipped | — | — |
| 7 | pr-sdlc | will_run | --auto --draft |
no |
| 7a | verify-pipeline (inline, opt-in) | conditional on 'verify-pipeline' ∈ flags.steps |
--timeout <N> --interval <N> |
YES on failure (interactive) |
| 7b | await-remote-review (inline, opt-in) | conditional on 'await-remote-review' ∈ flags.steps |
--timeout <N> --interval <N> --reviewers <csv> |
no |
| 8 | learnings-commit | will_run | (none — inline shell, see "After pr — learnings-commit" below) | no |
--auto Mode Audit
Not all sub-skills support --auto. This table is the source of truth:
| Sub-skill | --auto support | Behavior when ship runs with --auto |
|---|---|---|
| execute-plan-sdlc | No | Forwards --quality <X> only when the user explicitly passed --quality to ship; otherwise no quality flag is forwarded and execute-plan-sdlc applies its own selection logic. (Renamed from --preset in #190 to disambiguate from ship's step-selection semantics.) |
| commit-sdlc | Yes | --auto forwarded. Skips commit approval prompt. |
| review-sdlc | No | No interactive prompts to skip — runs fully automatically already. |
| received-review-sdlc | Yes | --auto forwarded. Skips Step 10 consent prompt and Step 12 reply/resolve prompt. Critique gates and verification still run. Only "will fix" items auto-implemented; threads for "will fix" items auto-resolved. |
| version-sdlc | Yes | --auto forwarded. Skips release plan approval prompt. Pre-condition checks and critique gates still run. |
| pr-sdlc | Yes | --auto forwarded. Skips PR approval prompt. |
Review verdict conditional logic
After review-sdlc completes, parse the conversation for a Verdict: line. The verdict label (CHANGES REQUESTED / APPROVED WITH NOTES / APPROVED) is display-only — it is included in the run banner but does NOT gate dispatch. Dispatch is gated exclusively by flags.reviewThreshold (resolved by scripts/skill/ship.js):
flags.reviewThreshold |
Dispatch received-review-sdlc when findings include … |
|---|---|
critical |
any critical |
high |
any critical OR high |
medium |
any critical OR high OR medium |
low |
any finding except info |
If the threshold is met → invoke received-review-sdlc (forward "--auto" when flags.auto).
Otherwise → collect findings and defer to the pipeline summary report.
Example run-banner line (display-only — do NOT control dispatch):
Review verdict: CHANGES REQUESTED (1 critical, 2 high)
In --auto mode, dispatch is automatic and received-review-sdlc --auto is forwarded — no interactive pause.
In --auto mode NEVER call AskUserQuestion at this boundary — auto-dispatch received-review-sdlc --auto on the documented default. This is enforced deterministically by the block-askuserquestion-auto.js PreToolUse hook (R71) — the mid-turn sibling of the R67/R68 turn-end continuation hooks.
Step 3 (CRITIQUE): Validate Pipeline
Print each validation check:
Pipeline validation:
[pass] gh CLI authenticated
[pass] Not on default branch (feat/ship-sdlc)
[pass] 5 of 7 steps will run
[pass] All skip values recognized
[pass] Version step supports --auto (release approval prompt skipped in auto mode)
[warn] If review finds critical/high issues, pipeline will pause for fix approval (interactive only; in --auto mode flags.auto === true → auto-dispatch the fix, no pause — R71)
Validation checks:
gh auth statussucceeds- Current branch is not the default branch (warn if it is — do not block)
- All
--stepsvalues are recognized step names:execute,commit,review,version,verify-openspec,archive-openspec,pr,learnings-commit - When
flags.sources.steps === 'quick'in the prepare output, verify thatflags.stepsis non-empty (R-quick-6 error would have fired ifship.quickwas missing — non-empty confirms the quick profile resolved correctly). Citeflags.sources.steps, NOT raw--quickor$ARGUMENTS, at all decision sites (R-quick-2, R-quick-3). --quickand--stepsare mutually exclusive — R-quick-5 error fires if both are present; surface fromerrors[]in prepare output, not re-checked independently.- At least one step will run
- Flag combinations are coherent (
--bumpwithout version step → warn).--bumpacceptsmajor|minor|patchor any pre-release label matching^[a-z][a-z0-9]*$(e.g.--bump rcships an RC release; the label is forwarded verbatim to version-sdlc).
Step 4 (DO): Present Pipeline and Confirm
Dry-run mode
If --dry-run was passed → Read ./entry-modes.md (Dry-run mode section) and follow it. Do not read preemptively. Dry-run displays the full pipeline table and stops without executing.
Auto mode
Display the pipeline table for visibility, then proceed without prompting.
Interactive mode
Display the pipeline table, then:
Use AskUserQuestion to ask:
Run this pipeline?
Options:
- yes — execute as shown
- edit — change steps, flags, or other config
- cancel — stop here
On edit: ask what to change, update flags, rebuild the pipeline table, and re-present. Loop until yes or cancel.
Step 5 (EXECUTE): Run Pipeline Steps Sequentially
Pre-step validation
Before dispatching each step, read its status from the skill/ship.js output:
"will_run"→ dispatch via Agent tool. Inline-executed steps (skill === null,dispatchMode: null) are not dispatched via a tool — they are handled directly in main context (either as Bash commands or as conditional logic such as parsing a JSON verdict, as specified per-step). This is non-negotiable."conditional"→ evaluate the runtime condition (e.g., review verdict). If condition met → dispatch via Agent tool. If not → print why with the specific condition that was not met."skipped"→ print "skipped" with thereasonandskipSourcefrom the script output.
A step with status: "will_run" MUST be dispatched per its dispatchMode. The LLM does not have authority to override dispatchMode or skip a will_run step. Printing a skip message for a "will_run" step is a pipeline violation.
Context budget — dispatch isolation
All sub-skills are Agent-dispatched for context isolation: each Agent loads its SKILL.md in its own context and returns only a structured result (5–10 lines). The ship pipeline's context receives structured data, not sub-skill definitions.
execute-plan-sdlc is the orchestrator and returns a Step-9-formatted result (waves completed, files modified, state file path) for ship's main-context loop to consume. Agent dispatch restores pipeline continuity by returning control to ship-sdlc after execute completes, enabling R37 branch migration, the staging window, and remaining steps. (Fixes #366.)
Dispatch protocol
Invocation source: Each step in the skill/ship.js output includes an invocation field containing the skill name and computed args. Use step.invocation verbatim — do not construct invocations from the examples below.
For each step that will run, apply the dispatch protocol based on step.dispatchMode:
When step.dispatchMode === 'agent' — Agent-tool dispatch (all sub-skills)
Print verbose progress header to user:
━━━ Ship Pipeline — Step 2/7: Commit ━━━ Skill: commit-sdlc Args: --auto Reason: --auto forwarded from ship --auto modeRecord step start via
state/ship.js begin-step(R70) — see the per-step transition block in "Main-thread TodoWrite orchestration" below;begin-steprecordsin_progressand renders the task-tray todos in one call.Do NOT end the response turn here (R70/#454). Once
begin-stephas marked the stepin_progress, the turn MUST continue directly into the Agent dispatch in step 3 — recording the step start and dispatching its Agent are a single uninterrupted sequence. A turn that ends afterbegin-stepbut before the Agent dispatch leaves the step strandedin_progressand requires a user message to resume (the recurrence of #452 / #454 that thestop-pipeline-continue.jsStop hook now guards against mode-independently). Immediately proceed to step 3.Dispatch Agent with: skill name, args from
step.invocation, model fromstep.model, and brief pipeline context (branch, previous step results needed for this step). Passmodel: step.modelto the Agent tool on every dispatch. Whenstep.isolationis non-null, additionally passisolation: step.isolation; whenstep.isolationis null, omit theisolationparameter entirely (the Agent tool schema does not acceptnullforisolation). The LLM must not add, remove, or change theisolationparameter from whatship.jscomputed (implements R-agent-isolation-script-driven, C15). Agent prompt template:You are executing the <skill-name> skill. Invoke `/<skill-name> <args>` using the Skill tool — this loads the SKILL.md automatically. Return a structured result: (1) status — success or failure (2) result summary — 2-3 lines (3) artifacts — commit hash, tag, PR URL, verdict, etc. (4) any warnings or issues encounteredReceive agent result. Print result to user:
[done] Step 2 complete: a1b2c3d feat(auth): add OAuth2 PKCE flow State saved to .sdlc/execution/ship-<branch>-<timestamp>.jsonRecord step completion/failure via
state/ship.js complete-step(R70) — see the per-step completion block in "Main-thread TodoWrite orchestration" below;complete-steprecordscompletedand renders the task-tray todos in one call. Failures still usestate/ship.js fail+ the ship-todos--fail-steprender.Use result to determine next step (e.g., review verdict → received-review decision). Print decision reasoning:
Review verdict: APPROVED WITH NOTES (2 medium) Decision: CONTINUING — no critical/high issues found
--autocontinuation (R67/R68/R70 — descriptive, not a competing imperative): In--automode the pipeline advances to the next step'sbegin-stepwithin the same response turn. This is reinforced by two hooks consuming the sharedpipelineAdvancingpredicate (lib/state.js): thepipeline-continue.jsPostToolUse hook (R67) emits forwardadditionalContextbetween steps, and thestop-pipeline-continue.jsStop hook (R68) returnsdecision: "block"so the turn does not end mid-pipeline. Both areflags.auto-gated for the between-steps case — interactive (non-auto) review between steps is preserved. Thebegin-step→complete-stepsequence (R70) is unchanged.
--automid-turn no-pause (R71 — descriptive, not a competing imperative; mid-turn sibling of R67/R68 — #477): In--automode (flags.auto === true) the pipeline must auto-dispatch the fix at the review→fix boundary and MUST NOT callAskUserQuestion. This is enforced deterministically by theblock-askuserquestion-auto.jsPreToolUse hook, which returnspermissionDecision: "deny"while an active ship pipeline is advancing withflags.auto === true. Where thestop-pipeline-continue.jsStop hook (R68) guards the turn-ending hole, this guards the mid-turn pause-for-input hole. Interactive (non-auto) review pauses are preserved.
Ship-sdlc retains full control of: pipeline table display, validation output, step progress headers, result formatting, state persistence messages, verdict-based flow decisions, and the final summary report. Sub-skills only execute their skill and return structured data — they do not print pipeline-level output.
Main-thread TodoWrite orchestration (R-todowrite-visibility, #427)
ship-sdlc surfaces live pipeline progress in the Claude Code task tray via main-thread TodoWrite calls. All derivation logic lives in scripts/lib/ship-todos.js (R-todowrite-visibility clause 11). The MAIN thread invokes the helper via Bash and passes the returned todos[] array to the TodoWrite tool. The helper's marker field is echoed verbatim to stdout (audit trail when the tray is hidden).
Helper resolution (run once at Step 5 entry):
SHIP_TODOS=$(find ~/.claude/plugins -name "ship-todos.js" -path "*/sdlc*/scripts/lib/ship-todos.js" 2>/dev/null | sort -V | tail -1)
[ -z "$SHIP_TODOS" ] && [ -f "plugins/sdlc-utilities/scripts/lib/ship-todos.js" ] && SHIP_TODOS="plugins/sdlc-utilities/scripts/lib/ship-todos.js"
[ -z "$SHIP_TODOS" ] && { echo "ERROR: ship-todos.js not found"; exit 2; }
STATE_SCRIPT=$(find ~/.claude/plugins -name "ship.js" -path "*/sdlc*/scripts/state/ship.js" 2>/dev/null | sort -V | tail -1)
[ -z "$STATE_SCRIPT" ] && [ -f "plugins/sdlc-utilities/scripts/state/ship.js" ] && STATE_SCRIPT="plugins/sdlc-utilities/scripts/state/ship.js"
[ -z "$STATE_SCRIPT" ] && { echo "ERROR: state/ship.js not found"; exit 2; }
Setup (one-time, BEFORE the Step 5 dispatch loop, only when flags.steps.length >= 2):
- Run:
node "$SHIP_TODOS" --state-file "$STATE_FILE" --event init(where$STATE_FILEis the resolved ship state file path from Step 1c output). - Parse JSON from stdout. Call
TodoWritewithtodosarray. - Echo
markerverbatim to stdout.
For ultra-short runs (flags.steps.length < 2), skip TodoWrite entirely.
Per-step transition + start (called at start of EACH Step 5 iteration, BEFORE the verbose progress header) — R69/R70:
begin-step atomically marks the step in_progress (the former state/ship.js start) AND renders the task-tray todos (the former ship-todos --event step) in a single call, replacing the two prior separate invocations.
- Run:
node "$STATE_SCRIPT" begin-step --step <stepName> --state-file "$STATE_FILE". - Parse JSON from stdout → call
TodoWritewith thetodosarray, echomarker.
Per-step completion (called AFTER the Agent return and result print) — R69/R70:
complete-step atomically marks the step completed (the former state/ship.js complete) AND renders the task-tray todos (the former ship-todos --event step --mark-completed) in a single call. Persisting completion and rendering happen in-process, so the ordering constraint that previously required two separate ordered calls is now internal to the subcommand.
- Run:
node "$STATE_SCRIPT" complete-step --step <stepName> --state-file "$STATE_FILE" --result "<summary>". - Parse JSON from stdout → call
TodoWritewith thetodosarray, echomarker.
Per-step failure (called when state/ship.js fail records a failure):
- Run:
node "$SHIP_TODOS" --state-file "$STATE_FILE" --event step --current-step <stepName> --fail-step <stepName>. - Parse JSON, call
TodoWrite, echomarker. - No todo lingers in_progress (helper enforces — AC4).
Resume reconstruction (called inside the existing implicit-resume banner block, BEFORE the pipeline table prints, when flags.resume === true):
- Run:
node "$SHIP_TODOS" --state-file "$STATE_FILE" --event resume --current-step <resume.nextPendingStep>. - Parse JSON, call
TodoWrite, echomarker.
flags.resume === true is the single gate (the prepare script unifies explicit --resume and flags.implicitResume; this matches the existing implicit-resume banner condition and satisfies no-opposite-logical-vectors).
Cross-skill note: execute-plan-sdlc's internal per-wave TodoWrite calls remain (Agent-context bookkeeping). They are NOT parent-visible — see execute-plan-sdlc/SKILL.md Progress signal section and R-todowrite-visibility, issue #427.
Pre-execute workspace auto-detection (R60, R37 — fixes #378, #379)
Workspace is auto-detected, not selected — there is no flag and no prompt. The prepare script (ship.js) emits the derived value as flags.workspace (R60; the context object carries no workspace field — reading context.workspace was the #451 regression):
branch— cwd is the main worktree AND HEAD is the default branch. ship-sdlc auto-creates a feature branch before dispatching execute.continue— a linked worktree, OR the main worktree already on a feature branch. ship-sdlc runs the pipeline in place; no branch is created.
The default-branch warning emitted by the prepare script is advisory (a preflight warning, not a halt): on the default branch the derive returns branch and a feature branch is auto-created, so the warning never strands a run.
Skip when resuming (flags.resume === true) — the resume block already handled the workspace.
When not resuming, consume the derived workspace and act:
SDLC_LIB=$(find ~/.claude/plugins -name "config.js" -path "*/sdlc*/scripts/lib/config.js" 2>/dev/null | sort -V | tail -1 | xargs -I {} dirname {})
[ -z "$SDLC_LIB" ] && [ -d "plugins/sdlc-utilities/scripts/lib" ] && SDLC_LIB="plugins/sdlc-utilities/scripts/lib"
[ -z "$SDLC_LIB" ] && { echo "ERROR: Could not locate scripts/lib (config.js). Is the sdlc plugin installed?" >&2; exit 2; }
# Read the derived workspace from flags.workspace (R60, #451) — NOT context.workspace (no such field).
WORKSPACE=$(F="$PREPARE_OUTPUT_FILE" node -e "const d=JSON.parse(require('fs').readFileSync(process.env.F,'utf8'));process.stdout.write((d.flags&&d.flags.workspace)||'continue')")
if [ "$WORKSPACE" = "branch" ]; then
# Step 1: Derive branch name from plan title via lib/branch-name.js (config-driven).
EXECUTE_BRANCH=$(node -e "
const {resolveBranchName}=require('$SDLC_LIB/branch-name');
const {readSection,resolveSdlcRoot}=require('$SDLC_LIB/config');
const cfg=(readSection(resolveSdlcRoot(),'workspace')||{}).branch||{};
// Logical type and slug derived from plan title (feature/bugfix/chore/docs/refactor).
// typeMap in config maps logical → branch prefix (defaults: feat/fix/chore/docs/refactor).
process.stdout.write(resolveBranchName({type:'<logical-type>',slug:'<derived-slug>',config:cfg}));
")
# Step 2: Pre-execute ship state migration (R37) — runs in the main worktree cwd,
# BEFORE branch creation, so `state/ship.js read` still resolves the OLD slug filename.
# $SCRIPT is resolved above in the workspace block (find ~/.claude/plugins … state/ship.js).
STATE_BRANCH=$(node "$SCRIPT" read 2>/dev/null | node -e "process.stdin.on('data',d=>{try{process.stdout.write(JSON.parse(d).branch||'')}catch(_){}})")
if [ -n "$STATE_BRANCH" ] && [ "$EXECUTE_BRANCH" != "$STATE_BRANCH" ]; then
FROM_SLUG=$(echo "$STATE_BRANCH" | sed 's|[^a-zA-Z0-9-]|-|g')
result=$(node "$SCRIPT" migrate --from "$FROM_SLUG" --to "$EXECUTE_BRANCH" 2>&1)
echo "State migrated: $FROM_SLUG → $EXECUTE_BRANCH"
fi
# Step 3: Create the feature branch (HEAD shared with main worktree).
git checkout -b "$EXECUTE_BRANCH"
fi
# When WORKSPACE = continue: EXECUTE_BRANCH stays unset, no migration, no checkout — run in place.
After git checkout -b the cwd is the main worktree on the new feature branch, so all subsequent Bash invocations and Agent-tool dispatches run in the current cwd trivially. There is no --branch forward to execute — by the time execute is dispatched, cwd is on the feature branch, so execute-plan-sdlc's own derive yields continue (run in place) and no value crosses the boundary. EXECUTE_BRANCH is still used by the post-version ancestry gate (see "Execute step" / version section); under continue it is unset and that gate is a no-op.
The migrate subcommand renames ship-<oldSlug>-<ts>.json → ship-<newSlug>-<ts>.json and updates data.branch. On migrated: false (e.g. no state file yet, slug already correct), warn and continue — do not abort; the orphaned file (if any) will be cleaned by the terminal cleanup step or by --gc.
Execution loop
Execute step resume: When the pipeline is resuming (gate on flags.resume === true from the prepare output — this is true whether the user typed --resume or the hook triggered implicit resume; do NOT re-parse $ARGUMENTS) and the execute step's status in the ship state file is in_progress:
- Check for
<main-worktree>/.sdlc/execution/execute-<branch>-*.json(an execute-plan-sdlc state file for the current branch). Resolve<main-worktree>viagit worktree list --porcelain(firstworktreeline). - If found, dispatch execute-plan-sdlc via the Agent tool with args from
step.invocationplus--resume(e.g."--quality <X> --resume"if the user passed--qualityto ship;"--resume"otherwise). Wave progress and gates run inside the Agent's sub-context; the structured return value drives the next step. (Implements R-implicit-resume —flags.resumeis the single resume signal regardless of source.) - If not found, dispatch via Agent tool normally using
step.invocation(execute restarts from scratch)
ship-sdlc does not manage execute-plan-sdlc's state file — execute-plan-sdlc handles its own creation, updates, and cleanup.
Worktree re-entry on resume: Check context.worktree.inLinkedWorktree from the skill/ship.js output. If true, already in the worktree — proceed normally.
If false (resuming from the main worktree but the pipeline originally ran in a worktree), find the worktree for the resume branch:
git worktree list --porcelain
Match the branch from the ship state file against worktree entries. If found and directory exists, cd <path> before continuing. If the worktree directory is gone, warn and fall back to running on the current branch.
Execute-step todo mirroring (R-todowrite-visibility clause 4):
Assign PLAN_FILE from the prepare output's context.planFile field (R-PLANFILE). This is resolved once by skill/ship.js using the priority order: CLI --plan-file → project .claude/settings.json plansDirectory → global ~/.claude/settings.json plansDirectory → default ~/.claude/plans/ (most recent *.md). Do not re-derive the path here — use context.planFile verbatim:
PLAN_FILE=$(node -e "const d=require('fs').readFileSync(process.env.F,'utf8'); process.stdout.write(JSON.parse(d).context.planFile||'')" F="$SHIP_PREPARE_OUTPUT_FILE")
Where $SHIP_PREPARE_OUTPUT_FILE is the path to the temp file holding the skill/ship.js JSON output (same file used to read flags, steps, etc.). When context.planFile is null or empty, PLAN_FILE will be empty and the ship-todos.js execute event will exit 2 with a clear error — surface that error before dispatching.
Before dispatching execute-plan-sdlc, run:
node "$SHIP_TODOS" --state-file "$STATE_FILE" --plan-file "$PLAN_FILE" --event execute --current-step execute
$PLAN_FILE is sourced from context.planFile in the prepare output (R-PLANFILE). The helper expands the execute step's placeholder substep to one substep per plan task (one ### Task N: heading per substep). Parse JSON, call TodoWrite, echo marker.
Then dispatch execute-plan-sdlc as below. On Agent return (success), run the post-execution completeness invariant before marking the step complete (R-INVARIANT-COMPLETENESS, #432):
EXECUTE_STATE_SCRIPT=$(find ~/.claude/plugins -name "execute.js" -path "*/sdlc*/scripts/state/execute.js" 2>/dev/null | sort -V | tail -1)
[ -z "$EXECUTE_STATE_SCRIPT" ] && [ -f "plugins/sdlc-utilities/scripts/state/execute.js" ] && EXECUTE_STATE_SCRIPT="plugins/sdlc-utilities/scripts/state/execute.js"
[ -z "$EXECUTE_STATE_SCRIPT" ] && { echo "ERROR: Cannot locate execute.js — completeness gate cannot run." >&2; exit 2; }
node "$EXECUTE_STATE_SCRIPT" verify-completeness
COMPLETENESS_EXIT=$?
if [ "$COMPLETENESS_EXIT" -ne 0 ]; then
echo "ERROR: execute-plan-sdlc returned but planned tasks are unaccounted. Pipeline halted." >&2
# Mark execute step failed and halt — do NOT advance to commit/review/version/pr
node "$SHIP_TODOS" --state-file "$STATE_FILE" --plan-file "$PLAN_FILE" --event execute --fail-step execute
exit "$COMPLETENESS_EXIT"
fi
If verify-completeness exits 65, the pipeline MUST halt before commit. The missing task IDs appear on stderr as JSON {missingIds, totalPlanned, totalAccounted}. Do NOT advance to the commit step.
Then run per-step-completion: --mark-completed execute. The parent does NOT receive per-task completion signals from the Agent; per-task todos all transition to completed atomically on return.
Example dispatch sequence (use step.invocation for actual args):
- Agent: execute-plan-sdlc, args: from
step.invocationverbatim. No--branchforward (R60, fixes #378, #379): when the derive wasbranch, ship already rangit checkout -bbefore this dispatch, so cwd is on the feature branch and execute-plan-sdlc's own derive yieldscontinue(run in place). When the derive wascontinue, ship never created a branch — execute also runs in place. Either way no workspace value crosses the boundary. Example:"--quality balanced --rebase auto". - Agent: commit-sdlc, args:
"--auto" - Agent: review-sdlc, args:
"--committed" - Agent: received-review-sdlc, args:
"--auto"(whenflags.auto; otherwise no args) - Agent: version-sdlc, args:
"patch" - Agent: pr-sdlc, args:
"--auto --draft"
Post-execute note (R37 migration moved pre-execute)
Branch migration (R37) now runs before the execute dispatch — inside the pre-execute workspace isolation block (see "Pre-execute workspace isolation" section above). The old post-execute migration block has been removed (fixes #379 — it ran after cwd changed, so git branch --show-current always returned the wrong value).
Subsequent state operations (start, complete, read) automatically pick up the renamed file because state/ship.js resolves by current branch.
Between execute and commit
execute-plan-sdlc does not stage files. Run git add -A -- ':!.sdlc/' with verbose output:
Staging changes from execution:
A src/middleware/auth.ts
A src/middleware/auth.test.ts
M src/routes/index.ts
Total: 14 files staged
Reason: execute-plan-sdlc creates files but does not stage them. .sdlc/ excluded to prevent committing runtime state.
Between review and received-review
Evaluate the verdict (see Step 2 conditional logic). Print the decision tree. If received-review-sdlc triggers and makes changes, check git status:
Review fixes applied: 3 files modified
M src/middleware/auth.ts
M src/routes/index.ts
M tests/auth.test.ts
→ Running commit step for review fixes
Then invoke commit-sdlc (step 5) for the fix commit.
After version — post-version ancestry HARD GATE (R-post-version-ancestry, fixes #349)
After the version step dispatches and returns, capture the new tag from the version-sdlc return value as NEW_TAG. When NEW_TAG is set (non-empty) AND EXECUTE_BRANCH is set (non-empty), run the ancestry check:
# Post-version ancestry HARD GATE
VERIFY_SCRIPT=$(find ~/.claude/plugins -name "verify-tag-ancestry.js" -path "*/sdlc*/scripts/util/verify-tag-ancestry.js" 2>/dev/null | sort -V | tail -1)
[ -z "$VERIFY_SCRIPT" ] && [ -f "plugins/sdlc-utilities/scripts/util/verify-tag-ancestry.js" ] && VERIFY_SCRIPT="plugins/sdlc-utilities/scripts/util/verify-tag-ancestry.js"
if [ -z "$VERIFY_SCRIPT" ]; then
echo "WARNING: verify-tag-ancestry.js not found — post-version ancestry check skipped." >&2
fi
if [ -n "$VERIFY_SCRIPT" ] && [ -n "$NEW_TAG" ] && [ -n "$EXECUTE_BRANCH" ]; then
node "$VERIFY_SCRIPT" --tag "$NEW_TAG" --branch "$EXECUTE_BRANCH" --remote origin
ANCESTRY_EXIT=$?
if [ "$ANCESTRY_EXIT" -ne 0 ]; then
echo "Pipeline halted: tag $NEW_TAG is not an ancestor of $EXECUTE_BRANCH." >&2
echo "Remediation: delete the tag (git push origin :refs/tags/$NEW_TAG; git tag -d $NEW_TAG) and re-run version step on the correct branch." >&2
exit 1
fi
fi
NEW_TAG is the tag string emitted by version-sdlc (e.g. v1.2.3). EXECUTE_BRANCH is the feature branch variable set during pre-execute workspace auto-detection when the derive was branch (already available in the pipeline shell context; unset when the derive was continue). This gate is a no-op when NEW_TAG is unset (version step not in flags.steps) or when EXECUTE_BRANCH is unset (continue — run in place). Version always runs when in steps[] regardless of checkout (tags are repo-global).
Between version and archive-openspec — verify-openspec (inline, opt-in)
If step.status === 'will_run' for the verify-openspec step (sourced from prepare output — NOT from $ARGUMENTS; implements R-verify-openspec-1..5):
Extract the change name from
step.argsby stripping the--changeprefix (the value after--changeis the first whitespace-delimited token following it):CHANGE_NAME="${step.args#--change }" CHANGE_NAME="${CHANGE_NAME%% *}"(
step.argsis sourced from the prepare output, e.g.--change my-featureor--change my-feature --auto.)Locate the openspec library:
OPENSPEC_LIB=$(find ~/.claude/plugins -name "openspec.js" -path "*/sdlc*/scripts/lib/openspec.js" 2>/dev/null | sort -V | tail -1) [ -z "$OPENSPEC_LIB" ] && [ -f "plugins/sdlc-utilities/scripts/lib/openspec.js" ] && OPENSPEC_LIB="plugins/sdlc-utilities/scripts/lib/openspec.js" [ -z "$OPENSPEC_LIB" ] && { echo "ERROR: Could not locate scripts/lib/openspec.js. Is the sdlc plugin installed?" >&2; exit 2; }Run structural validation (synchronous call — no
.then). Pass values via environment to avoid shell injection; keep stdout clean (no2>&1):RESULT=$(OPENSPEC_LIB="$OPENSPEC_LIB" CHANGE_NAME="$CHANGE_NAME" node -e ' const lib = require(process.env.OPENSPEC_LIB); const r = lib.validateChangeStrict(process.cwd(), process.env.CHANGE_NAME); process.stdout.write(JSON.stringify({ok:r.ok,cliAvailable:r.cliAvailable,stderr:r.stderr||""})); ') NODE_EXIT=$? [ "$NODE_EXIT" -ne 0 ] && { echo "ERROR: openspec validation script exited $NODE_EXIT" >&2; }Parse the JSON result from
$RESULTand branch on verdict:cliAvailable: false→ logopenspec CLI not available, skipping validate→ proceed to archive-openspec (non-blocking).ok: true→ logopenspec validate --strict: passed→ proceed to archive-openspec.ok: falseANDcliAvailable: true→ logstderr→ note structural issues → proceed to archive-openspec (non-blocking per KD2).
When step.status !== 'will_run' (skipped — not in steps[] or no matched change), skip this entire section.
Between version and pr — archive-openspec (conditional)
If the archive-openspec step has status: "conditional" in the pipeline plan, execute it inline (no Agent dispatch — this is a deterministic shell operation):
- Extract the change name from
step.args(--change <name>). - Call
lib/openspec.js::validateChangeStrict(projectRoot, name)via Bash:node -e " const { validateChangeStrict } = require('<LIB>/openspec.js'); const result = validateChangeStrict(process.cwd(), '<name>'); console.log(JSON.stringify(result)); " - If
ok === false: halt the pipeline. Print the validation errors (stderr) and save state for--resume. - If
ok === true: prompt the user for approval (skip prompt in--automode). - On approval, run the archive:
node -e " const { runArchive } = require('<LIB>/openspec.js'); const result = runArchive(process.cwd(), '<name>'); console.log(JSON.stringify(result)); " - If archive succeeds, commit:
git add openspec/ git commit -m "chore(openspec): archive <name>" - If
isArchived(projectRoot, name)already returns true (idempotence), skip with reason "already archived".
If the step has status: "skipped", print the skip reason from step.reason.
After pr — verify-pipeline (conditional, opt-in)
If the verify-pipeline step has status: "will_run" (gated by step membership in flags.steps — cite step.status === "will_run" from the prepare output, not $ARGUMENTS; per flag-coherence-cross-skill), execute it inline (no Agent dispatch — this is a deterministic polling script). Implements R41–R49.
Resolve the script path:
VP_SCRIPT=$(find ~/.claude/plugins -name "verify-pipeline.js" -path "*/sdlc*/scripts/skill/verify-pipeline.js" 2>/dev/null | sort -V | tail -1) [ -z "$VP_SCRIPT" ] && [ -f "plugins/sdlc-utilities/scripts/skill/verify-pipeline.js" ] && VP_SCRIPT="plugins/sdlc-utilities/scripts/skill/verify-pipeline.js" [ -z "$VP_SCRIPT" ] && { echo "ERROR: Could not locate skill/verify-pipeline.js. Is the sdlc plugin installed?" >&2; exit 2; }Run the script with the args from
step.argsplus--state-file <ship-state-path>:node "$VP_SCRIPT" $STEP_ARGS --state-file "$SHIP_STATE_PATH"Parse the single JSON line on stdout. Branch on
status:status === "green"— logverify-pipeline: CI green for PR #Nand proceed toawait-remote-review. Cites R43.status === "failed"ANDflags.auto === false— interactive (R45). UseAskUserQuestion:Wave verify-pipeline failed for PR #N.
failed checks: . Options: analyze (Recommended) | skip | abort
- analyze: dispatch
verify-pipeline-sdlcsubagent (Agent tool, model sonnet) with--pr <N>and--logs <inline-log-excerpt-from-failedChecks>. On verdictfix-applied, dispatchcommit-sdlc(Agent tool, model haiku,--auto) directly to commit and push. Then re-run verify-pipeline (loop). Iteration cap =flags.verifyPipelineMaxIterations(default 3, R47); after cap, log warning and proceed toawait-remote-review. The pre-existingcommit-fixesstep entry (already visited beforepr) is NOT involved — this dispatch is direct via the Agent tool. - skip: log warning, proceed to
await-remote-review. - abort: write
verifyPipelineExhausted: trueto the ship state file, exit pipeline 1.
status === "failed"ANDflags.auto === true— non-interactive (R46). Directly dispatchverify-pipeline-sdlcsubagent (Agent tool, model sonnet) with--pr <N> --logs <excerpt> --auto. Onfix-applied, dispatchcommit-sdlc --autodirectly. Loop with the same iteration cap (flags.verifyPipelineMaxIterations, R47). On cap exhaustion, log warning and proceed.status === "timeout"— log warningverify-pipeline: timeout after Ns. The script has already writtenverifyPipelineExhausted: trueto the state file. Proceed toawait-remote-review. Cites R48, R49.status === "skipped"(resume short-circuit) — log infoverify-pipeline: skipped (resumed from prior exhaustion). Proceed. Cites R49.status === "error"— log warningverify-pipeline: error — <reason>. Proceed.- analyze: dispatch
Do NOT replicate polling, log fetching, or fix-application logic in this prose — those live in verify-pipeline.js and the verify-pipeline-sdlc skill (per scripts-over-llm-logic).
If the step has status: "skipped", print the skip reason from step.reason and do nothing.
After verify-pipeline — await-remote-review (conditional, opt-in)
If the await-remote-review step has status: "will_run" (gated by step membership in flags.steps — cite step.status === "will_run" from the prepare output, not $ARGUMENTS), execute it inline. Implements R50–R56.
Resolve the script path:
AR_SCRIPT=$(find ~/.claude/plugins -name "await-remote-review.js" -path "*/sdlc*/scripts/skill/await-remote-review.js" 2>/dev/null | sort -V | tail -1) [ -z "$AR_SCRIPT" ] && [ -f "plugins/sdlc-utilities/scripts/skill/await-remote-review.js" ] && AR_SCRIPT="plugins/sdlc-utilities/scripts/skill/await-remote-review.js" [ -z "$AR_SCRIPT" ] && { echo "ERROR: Could not locate skill/await-remote-review.js. Is the sdlc plugin installed?" >&2; exit 2; }Run the script with the args from
step.argsplus--state-file <ship-state-path>:node "$AR_SCRIPT" $STEP_ARGS --state-file "$SHIP_STATE_PATH"Parse the single JSON line on stdout. Branch on
status:status === "actionable"— directly dispatchreceived-review-sdlc(Agent tool, model sonnet) with--pr <verdict.prNumber>(and--autowhenflags.auto === true). After the subagent completes, rungit status --porcelainin the main context; if there are working-tree changes, directly dispatchcommit-sdlc(Agent tool, model haiku,--auto) to commit and push. The pre-existingreceived-reviewandcommit-fixesstep entries (already visited beforepr) are NOT involved — these dispatches are direct via the Agent tool. Cites R52.status === "approved-clean"— logawait-remote-review: APPROVED by <reviewer>and proceed. Do NOT dispatch received-review-sdlc. Cites R53.status === "timeout"— log warningawait-remote-review: timeout after Ns waiting for <reviewers>. The script has already writtenawaitRemoteReviewExhausted: trueto the state file. Proceed. Cites R54, R55.status === "skipped"(resume short-circuit) — log info and proceed. Cites R55.status === "error"— log warning and proceed.
If the step has status: "skipped", print the skip reason from step.reason and do nothing.
After pr — learnings-commit (final step)
Pipeline-level learnings cannot land in the feature commit (issue #208) — review/version/pr/archive all run after the feature commit. The learnings-commit step exists to capture them in a trailing chore commit so post-pipeline git status is clean.
If the learnings-commit step has status: "will_run", execute it inline (no Agent dispatch — deterministic shell):
- Run the ship-level Learning Capture (see the
## Learning Capturesection below) — append any new entries to.sdlc/learnings/log.md. - Check whether anything actually changed:
git diff --quiet -- .sdlc/learnings/log.md- Exit
0(no diff) → skip the commit and reportlearnings-commit: no-op (no new learnings).
- Exit
- If there is a diff:
On push failure (offline, auth), report the error but do not halt the pipeline — the local commit still lands and a follow-upgit add .sdlc/learnings/log.md git commit -m "chore(ship-sdlc): capture pipeline learnings" git pushgit pushwill deliver it. - After the step,
git status --porcelainMUST be empty.
If the step has status: "skipped" (omitted from --steps or ship.steps[]), print the skip reason from step.reason and do not perform any of the above. The execute-plan-sdlc-level Learning Capture (R27 in docs/specs/execute-plan-sdlc.md) still runs and lands in the feature commit; only the ship-level append is conditional on this step.
Between last commit and version — rebase on default branch
After all commits are done (feature commit + optional review-fix commit + optional archive commit), rebase onto the latest default branch to ensure a clean merge:
git fetch origin <defaultBranch>
Check if rebase is needed:
git merge-base --is-ancestor origin/<defaultBranch> HEAD
If main is already an ancestor of HEAD, no rebase needed — print "Already up to date with <defaultBranch>" and skip.
Otherwise, attempt rebase:
git rebase origin/<defaultBranch>
If rebase succeeds: Print summary and continue.
Rebase: clean — <N> commits replayed on origin/<defaultBranch>
If rebase fails (conflicts): Abort and handle:
git rebase --abort
List conflicting files from the failed output. Then:
Auto mode: Stop pipeline, save state for --resume. Print:
Rebase: CONFLICTS detected with origin/<defaultBranch>
Conflicting files:
- src/foo.ts
- src/bar.ts
Pipeline paused. Resolve conflicts manually, then --resume.
Interactive mode: Use AskUserQuestion:
Rebase onto
<defaultBranch>has conflicts infiles:
src/foo.tssrc/bar.ts
- Pause pipeline — resolve manually, then
--resume- Skip rebase — create PR with conflicts (GitHub will show merge conflicts)
- Merge instead — try
git merge origin/<defaultBranch>(creates merge commit)
Option 3 fallback: run git merge origin/<defaultBranch>. If that also conflicts, abort and fall back to option 1.
Note: in a worktree, all of this is safe — main working tree is untouched.
State persistence
After each step, update pipeline state via state/ship.js. Locate the script:
SCRIPT=$(find ~/.claude/plugins -name "ship.js" -path "*/sdlc*/scripts/state/ship.js" 2>/dev/null | sort -V | tail -1)
[ -z "$SCRIPT" ] && [ -f "plugins/sdlc-utilities/scripts/state/ship.js" ] && SCRIPT="plugins/sdlc-utilities/scripts/state/ship.js"
At pipeline start (after Step 1 completes), initialize the state file:
node "$SCRIPT" init --branch <branch> --flags '<flags JSON>'
Before each step: node "$SCRIPT" start --step <name>
After each step: node "$SCRIPT" complete --step <name> --result "<summary>" (or skip --step <name> --reason "<reason>" or fail --step <name> --error "<error>")
Record decisions: node "$SCRIPT" decide --step <name> --text "<decision>"
Defer findings: node "$SCRIPT" defer --severity <s> --file <f> --title "<t>"
Terminal cleanup step (R38, issue #223)
The prepare-script output (steps[] array) ends with a synthetic step named cleanup (status: "will_run", skill: null, reserved: true). It is appended unconditionally by skill/ship.js::computeSteps and is NOT user-configurable — listing cleanup in --steps or ship.steps[] produces a validation error in Step 1c.
Dispatch the cleanup step as a direct Bash call, not as an Agent. Each cleanup step entry has an invocation object with two precomputed command variants:
{
"method": "bash",
"normal": "node \"$SCRIPT\" cleanup-pipeline",
"forced": "node \"$SCRIPT\" cleanup-pipeline --force"
}
Cleanup-step todo (R-todowrite-visibility clause 2):
Before invoking the cleanup Bash command, run:
node "$SHIP_TODOS" --state-file "$STATE_FILE" --event cleanup --current-step cleanup
Call TodoWrite, echo marker. After the cleanup command returns (success or contract violation), run per-step-completion with --mark-completed cleanup.
Selection rule: walk steps[] and check whether any prior step's recorded status (from the live state file, not the prepare snapshot) is failed. If so, dispatch with step.invocation.forced; otherwise dispatch with step.invocation.normal. $SCRIPT is the same state/ship.js path resolved in the state-persistence section above.
Behavior:
- Normal: validates pipeline contract (no
pending/in_progresssteps), deletes the current run's state file, then sweeps stale ship- and execute- state files older thanstate.gc.ttlDays(default 7 days) whose branch is no longer ingit branch --list. - Forced: preserves the current run's state file (so
--resumeworks after a failure), skips the contract check, and runs only the GC sweep.
If --ttl-days <N> was passed to ship-sdlc, append it to whichever variant you select.
The script prints a JSON report to stdout. Surface it verbatim:
Terminal cleanup:
Current run: deleted ship-<branch>-<ts>.json
GC swept: 1 ship-* file, 0 execute-* files (1 deleted, kept 1 ttl-fresh)
If currentRun.valid === false (contract violation on the normal path), print:
PIPELINE CONTRACT VIOLATION
The following steps were not resolved:
- <step>: status "<status>" (expected: completed, skipped, or failed)
State file preserved for debugging: <path>
This is a pipeline bug — all will_run steps must be dispatched.
Do NOT proceed to the success summary. The pipeline did not complete correctly.
The cleanup step ALWAYS runs, even on failure paths — orphaned state files from interrupted runs are pruned regardless of whether the current pipeline succeeded.
Step 6 (REPORT): Pipeline Summary
Ship Pipeline Complete
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step Skill Result
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1 execute-plan-sdlc [done] 8 tasks, 3 waves completed
2 commit-sdlc [done] a1b2c3d feat(auth): add OAuth2 PKCE
3 review-sdlc [done] APPROVED WITH NOTES (2 medium)
4 received-review-sdlc — not triggered (no critical/high)
5 commit-sdlc (fixes) — not triggered
6 version-sdlc — skipped (config default)
7 pr-sdlc [done] https://github.com/.../pull/42
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Decisions log:
- Steps resolved: [execute, commit, review, archive-openspec, pr] (from config default; --quality not forwarded to execute-plan-sdlc — user did not pass --quality)
- Version step skipped (from config default, bump type: patch)
- Review found 2 medium issues — below threshold, deferred
- PR created as draft (from --draft flag)
Deferred review findings (2 medium):
1. [medium] src/middleware/auth.ts:42 — Consider extracting token validation
2. [medium] src/routes/index.ts:15 — Missing rate limit on new endpoint
→ Run /received-review-sdlc to address these
State file cleaned up: .sdlc/execution/ship-<branch>-<epoch>.json deleted
If OpenSpec was detected in Step 1f and the verify-openspec step ran, append the verdict result:
→ OpenSpec verify: <satisfied|unsatisfied> — <summary>
(When unsatisfied and gaps were opened-as-finding or recorded, note: N gap(s) recorded as pipeline findings.)
If OpenSpec was detected but verify-openspec is NOT in flags.steps (step was not configured), append:
→ Run openspec validate --strict <change> to validate implementation completeness against the spec
If OpenSpec was detected in Step 1f and the archive-openspec step ran successfully, append:
→ OpenSpec change "<name>" archived and committed.
If OpenSpec was detected but archive-openspec is NOT in flags.steps, append:
→ Run openspec archive <change> --yes to archive the OpenSpec change and sync delta specs
Worktree cleanup
(removed — ship-sdlc never creates a git worktree. Workspace is auto-detected branch/continue; there is nothing to clean up. R60, fixes #378, #379.)
Post-pipeline advisory (when version was auto-skipped)
If the version step status is skipped and the reason contains "worktree", print a next-step hint after the summary table:
Note: Version step was skipped (worktree mode — tags are repo-global).
After merging this PR, run on main:
/version-sdlc <patch|minor|major>
This will tag the release and generate the changelog from all merged commits.
Reference — Error Recovery, DO NOT, Gotchas, Learning Capture
Reference material lives in ./reference.md (implements R-progressive-disclosure). Read the relevant section on its trigger; do not read preemptively:
- On any pipeline-level failure → Read
./reference.md(Error Recovery section) for the detect → diagnose → recover → escalate flow and the resume-instruction format. - Before completing the pipeline (Learning Capture) → Read
./reference.md(Learning Capture section) to append pipeline learnings to.sdlc/learnings/log.md. This is triggered by thelearnings-commitstep (see "After pr — learnings-commit" above). - When unsure about a prohibited action or an edge-case behavior → Read
./reference.md(DO NOT and Gotchas sections).
What's Next
After the pipeline completes, common follow-ups include:
/received-review-sdlc— address deferred medium/low findingsopenspec validate --strict <change>— validate implementation against OpenSpec (only suggest whenverify-openspec ∉ flags.steps; when the step ran, the result is already in REPORT)openspec archive <change> --yes— archive the OpenSpec change and sync delta specs (only suggest whenarchive-openspec ∉ flags.steps)
See Also
/execute-plan-sdlc— plan execution with wave-based dispatch/commit-sdlc— smart commit with style detection/review-sdlc— multi-dimension code review/received-review-sdlc— process and fix review findings/version-sdlc— semantic versioning and release tags/pr-sdlc— pull request creation