shipwright-run · diff
git:20260711.64f197b to git:20260714.aef19f2
81 added, 130 removed. Audit A to A.
---
name: shipwright-run
- description: "Pipeline Initializer & Phase Coordinator for the Shipwright SDLC pipeline. Writes the run spec, prints a phase launch card, then ends — each phase runs in its own external Claude session.\nTRIGGER when: user wants to run the full pipeline, start the complete SDLC process, build an entire application from scratch, or resume an interrupted pipeline. Also when user says 'shipwright run' or 'start the pipeline'.\nDO NOT TRIGGER when: user asks for a specific phase only (project, design, plan, build, test, deploy, changelog, compliance), or asks to fix/change/add something to an existing project (/shipwright-iterate). If the user wants only ONE phase, trigger that specific skill instead."
+ description: "Pipeline Initializer & Phase Coordinator for the Shipwright SDLC pipeline. Writes the run spec, then DRIVES every phase from this one conversation via a phase-runner subagent.\nTRIGGER when: user wants to run the full pipeline, start the complete SDLC process, build an entire application from scratch, or resume an interrupted pipeline. Also when user says 'shipwright run' or 'start the pipeline'.\nDO NOT TRIGGER when: user asks for a specific phase only (project, design, plan, build, test, deploy, changelog, compliance), or asks to fix/change/add something to an existing project (/shipwright-iterate). If the user wants only ONE phase, trigger that specific skill instead."
license: MIT
compatibility: Requires uv (Python 3.11+), git. Optional: JELASTIC_TOKEN for deploy.
---
# Shipwright Run — The Pipeline Coordinator
Single entry point for the entire Shipwright SDLC pipeline. The master session
- **specs** the pipeline (writes `shipwright_run_config.json`), prints a launch
- card for the first phase, and then ends. Each phase runs in its own external
- Claude CLI session — phase Stop hooks plan the next phase automatically. The
- master session is **not** a pipeline driver; it is a coordinator that writes
- the contract and steps aside.
+ **specs** the pipeline (writes `shipwright_run_config.json`), then **drives** it:
+ resolve the next phase → dispatch a phase-runner subagent → apply its result →
+ repeat, until the pipeline is terminal.
+ Because every phase runs as a subagent of *this* conversation, the pipeline
+ advances on **every surface** — CLI, VS Code extension, desktop app. There is no
+ launch card to paste and no second session to open.
+
---
## CRITICAL: First Actions
**Governing rules:** Read and follow `shared/constitution.md` (ALWAYS / ASK FIRST / NEVER boundaries).
### A. Print Intro Banner
```
================================================================================
SHIPWRIGHT-RUN: AI-Powered Software Delivery
================================================================================
From description to deployed application.
Usage:
/shipwright-run "Build a SaaS time tracker with Supabase"
/shipwright-run (interactive)
/shipwright-run @requirements.md
Pipeline: Project → Design → Plan → Build → Test → Changelog → Deploy
Security scanning is out-of-band — run /shipwright-security or activate .github/workflows/security.yml.
- Each phase runs in its own external Claude CLI session. This master session
- writes the pipeline spec, prints the first launch card, then ends.
+ This session DRIVES the pipeline: each phase runs as a phase-runner subagent
+ right here. Keep it open — closing it pauses the run (re-invoke to resume).
For ongoing changes to existing projects, use /shipwright-iterate instead.
In plain words (shared index → docs/guide.md Appendix A):
IREB-Spec: Description of what the app should do, who it's for, and what it must not do
ADR: Log of architectural decisions with rationale (why this database, why this pattern)
================================================================================
```
### B. Detect Input & Mode
**Full mode** (default):
- New project or major extension
- Continue to Step 1
If `shipwright_run_config.json` already exists at `schemaVersion: 2`, this is a
**resume** — jump to [Resume Support](#resume-support) below before continuing
the new-pipeline flow.
### C. Discover Plugin Root
The SessionStart hook injects `SHIPWRIGHT_PLUGIN_ROOT=<path>`. Use it directly.
---
## Step 1: Understand Intent
**Goal:** Figure out what the user wants to build.
**Input sources (in priority order):**
0. **Brief** (WebUI Intent Wizard): a pre-delivered file/payload with the four
wizard answers → run [brief-intake.md](references/brief-intake.md), ask ONLY what's missing.
1. **File**: `@requirements.md` → read and summarize
2. **Inline**: `"Build a SaaS time tracker..."` → use as starting context
3. **Chat**: No input → ask: "What do you want to build?"
Ask 1-3 clarifying questions if the description is vague (skip any the brief
already answered): core feature · who are the users · tech preferences.
---
## Step 2: Infer Settings
See [inference-rules.md](references/inference-rules.md). If Step 1 ran a brief,
[brief-intake.md](references/brief-intake.md) already fixed profile + deploy — reuse them; infer only fields left null.
```bash
uv run "{plugin_root}/scripts/lib/inference.py" \
--description "{user_description}"
```
The inference engine determines:
| Setting | How Inferred |
|---------|-------------|
| **Scope** | New project (no CLAUDE.md) → Full App; existing CLAUDE.md → Extension. For ongoing changes, use `/shipwright-iterate`. |
| **Profile** | "Supabase" + "Next.js" → `supabase-nextjs`; no match → ask user |
- | **Autonomy** | Default: `guided` (per-phase user prompts within each phase session); user can choose `autonomous` |
+ | **Autonomy** | Default: `guided` (phases stop at gates for you); user can choose `autonomous` |
---
## Step 3: Confirm Settings
Present inferred settings and allow override:
```
================================================================================
INFERRED SETTINGS
================================================================================
Scope: {Full Application | Extension}
Profile: {supabase-nextjs | custom}
Autonomy: {guided | autonomous}
- Guided: Phase sessions ask before destructive actions.
- Autonomous: Phase sessions run hands-off (Deploy still asks).
- Mode: {single_session (default) | multi_session (deprecated)}
- single_session = one driven conversation (runs on every surface); multi_session = each phase its own external session (deprecated, back-compat only).
+ Guided: Phases ask before destructive actions.
+ Autonomous: Phases run hands-off (Deploy still asks).
Deploy to: {Jelastic DEV | none}
Accept or modify:
================================================================================
```
```
AskUserQuestion:
question: "Settings look correct?"
options:
- "Accept — write pipeline spec"
- "Change profile"
- "Change autonomy"
- - "Change mode"
- "Skip deploy"
```
+ There is no execution-mode question: `single_session` is the sole pipeline mode.
+
---
## Step 4: Write Pipeline Spec
```bash
uv run "{plugin_root}/scripts/lib/orchestrator.py" write-config \
--scope "{scope}" \
--profile "{profile}" \
--autonomy "{autonomy}" \
- --mode "{mode}" \
--deploy-target "{target}" \
--project-root "$(pwd)"
```
- `--mode` defaults to `single_session` (the sole supported mode). `multi_session` is DEPRECATED — pass it only for a legacy/back-compat run (removal deferred). A mode-less legacy config still reads as `multi_session`; migrate it explicitly (set `mode: single_session` + resume).
-
- This writes `shipwright_run_config.json` at `schemaVersion: 2`. The orchestrator:
+ This writes `shipwright_run_config.json` at `schemaVersion: 2` with
+ `mode: single_session` (the sole mode — `--mode` exists but has exactly one
+ valid value). The orchestrator:
- Generates `runId` and freezes `runConditions`. Post-decouple, `securityEnabled` is always `false` (security is no longer an orchestrator phase). `aikidoClientIdPresent` is set from `AIKIDO_CLIENT_ID` for diagnostic purposes only — it does not gate any phase.
- - Initializes `phase_tasks[]` with the first task: `{phase: "project", status: "awaiting_launch", sessionUuid: <pre-bound uuid4>, prerequisites: []}`.
- - Subsequent phase tasks are appended by phase Stop hooks via `complete-phase-task` → `plan-next-phase`. **The master never plans phases directly.**
+ - Initializes `phase_tasks[]` with the first task: `{phase: "project", status: "awaiting_launch", prerequisites: []}`. Each task carries a pre-bound `sessionUuid` — the **CAS claim token** the loop claims it with, not a Claude session id.
+ - Subsequent phase tasks are appended by `complete-phase-task` → `plan-next-phase` as each phase finishes. **The master never plans phases directly** — it drives the loop; the lifecycle plans.
**Important — always pass `--profile`:** the WebUI Preview button keys off
`shipwright_run_config.json.profile` + `shared/profiles/{name}.json` to decide
whether Preview can launch a dev server. Omitting it leaves the field null and
Preview never appears — always include the Step 2 profile (Next.js/Supabase → `supabase-nextjs`; local-only default → `vite-hono`).
- Capture the parsed JSON output — Step 5 reads `phase_tasks[0]` from it.
-
- **Mode branch (SS3).** If `config.mode == "single_session"`, skip Step 5's
- launch-card hand-off and drive the pipeline in THIS conversation via the
- **[Single-Session Orchestrator Loop](references/single-session-loop.md)** (Step 5 + Resume Support are the `multi_session` legacy/back-compat path).
+ Capture the parsed JSON output — Step 5 reads `runId` from it.
---
## Step 4.5: Phase-Router Hook (no install step needed)
The `suggest_iterate.py` UserPromptSubmit hook is registered in
`shipwright-iterate` plugin's own `hooks/hooks.json`; no project-level
`.claude/settings.json` install is performed. Once the user enables
the `shipwright-iterate@shipwright` plugin (default in marketplace
installs), the hook fires automatically for every prompt in any project
carrying `shipwright_run_config.json`. ADRs 019/020 (carrier-shape Shape B +
quoted path + `--no-project`) survive verbatim in the plugin registration.
**Legacy adopt cleanup:** if `.claude/settings.json` carries an old
`UserPromptSubmit` entry referencing `${CLAUDE_PLUGIN_ROOT}/.../suggest_iterate.py`,
Claude Code shows a "hook is not associated with a plugin" error (that variable
only expands in plugin context). One-time fix: drop that one
`hooks.UserPromptSubmit` entry from `.claude/settings.json`; the
plugin-registered hook still fires.
---
- ## Step 5: Print Launch Card and End
-
- The master is done. Read `phase_tasks[0]` from the config you just wrote and
- render the **hand-off banner**, branched on the **launch surface**. A pipeline
- phase launches as its own `claude --session-id …` session, so the hand-off
- depends on whether this surface can start one.
-
- **Compute these values:**
-
- - `runId` → from `config.runId` (e.g. `run-a1b2c3d4`).
- - `shortRunId` → first 4 hex chars after the `run-` prefix (e.g. `a1b2`).
- - `phase` → `phase_tasks[0].phase` (always `"project"` for a fresh run).
- - `splitId` → `phase_tasks[0].splitId` (always `null` at run init).
- - `sessionUuid` → `phase_tasks[0].sessionUuid` (pre-bound uuid4).
- - `slashCommand` → `phase_tasks[0].slashCommand` (`/shipwright-project` for run init).
- - `projectRoot` → `$(pwd)` (the cwd you passed to `write-config`).
- - `pipelineLength` → `len(config.pipeline)` (always 7 post-decouple — security is no longer an orchestrator phase).
- - `nameSuffix` → `splitId ? f"{phase} / {splitId}" : phase` (here just `"project"`).
- - `surface` → read `CLAUDE_CODE_ENTRYPOINT` (POSIX `printenv CLAUDE_CODE_ENTRYPOINT`;
- PowerShell `$env:CLAUDE_CODE_ENTRYPOINT`). `cli` (a plain shell OR the WebUI's
- embedded terminal, which re-sets `cli`) → `surface = terminal`. `claude-vscode`,
- a desktop-app value, or any GUI chat surface → `surface = chat` — it CANNOT
- start a bound `claude --session-id` phase session.
-
- **Render the banner — branch on `surface`:**
+ ## Step 5: Drive the Pipeline
- **(a) `surface` is chat — VS Code extension or desktop app.** This chat surface
- can't start a bound phase session, so the pipeline can't advance here. Tell the
- truth and point at a surface that can.
+ Announce the run, then enter the loop:
```
================================================================================
- PIPELINE PLANNED — {runId}
+ PIPELINE RUNNING — {runId}
================================================================================
- Phase 1 of {pipelineLength} ({phase}) is registered — but this surface can't
- launch it. The pipeline runs each phase as its own bound session
- (claude --session-id …), which the VS Code extension / desktop chat can't start.
-
- Run it from a surface that can:
- • Terminal (CLI) — paste:
- claude --session-id {sessionUuid} --add-dir "{projectRoot}" --name 'Run-{shortRunId} / {nameSuffix}' '{slashCommand}'
- • WebUI Command Center — open the Task Board and Continue each phase.
+ Phase 1 of {pipelineLength} ({phase}) starting.
- For a single change without the full pipeline, /shipwright-iterate runs here.
+ I drive every phase from this conversation — nothing to paste, nothing to open.
+ Keep this session alive: closing it pauses the run. Re-invoke /shipwright-run to
+ resume exactly where it stopped (no phase work is lost).
================================================================================
```
- **(b) `surface` is terminal — CLI or the WebUI's embedded terminal.** Offer both
- continue paths (board or paste); `cli` covers both.
-
- ```
- ================================================================================
- PIPELINE PLANNED — {runId}
- ================================================================================
- Phase 1 of {pipelineLength} ({phase}) is registered and ready.
-
- ▸ In the WebUI Command Center? The pipeline is on the Task Board as a Run
- lane — use Continue on Phase 1 ({phase}) there. You can close this session.
-
- ▸ In a plain terminal? Open a NEW terminal and paste this command:
-
- claude --session-id {sessionUuid} --add-dir "{projectRoot}" --name 'Run-{shortRunId} / {nameSuffix}' '{slashCommand}'
-
- This master session can be closed — pipeline state lives in
- shipwright_run_config.json. Each phase plans the next on its own Stop hook.
+ Then run the **[Single-Session Orchestrator Loop](references/single-session-loop.md)**:
+ alternate `single-session-next` (resolve + claim the frontier phase) and
+ `single-session-apply` (validate + complete the phase-runner's result), with a
+ `shipwright-run:phase-runner` subagent in between, until a terminal signal
+ (`complete` / `failed` / `needs_validation`).
- When all phases complete, the final phase's Stop hook flips
- run.status = "complete" — you do NOT need to reopen this master session.
- ================================================================================
- ```
+ `pipelineLength` = `len(config.pipeline)` (always 7 post-decouple — security is
+ no longer an orchestrator phase).
- **End the turn here.** Do NOT invoke any slash command, do NOT spawn a Task,
- do NOT call orchestrator update-step. The master's job is done — phase Stop
- hooks take over from this point.
+ **Do NOT** invoke a phase slash command directly and do NOT call
+ `orchestrator update-step`. The loop's two subcommands are the only way phases
+ advance — they reuse `phase_task_lifecycle`, so there is no bespoke completion path.
- The master's own Stop hook (`master_stop_check.py`) is observational and prints
- a summary to stderr. It does not change pipeline state.
+ The master's own Stop hook (`master_stop_check.py`) is observational and prints a
+ summary to stderr. It does not change pipeline state.
---
- ## Step 6: Final Wrap-Up (only on resume)
+ ## Step 6: Final Wrap-Up
- This step runs **only if** the user re-opens the master session on an existing
- v2 config. It is informational; it does not invoke any skill.
+ Reached when the loop returns a terminal signal (or when the user re-opens the
+ master on an already-terminal config). Informational; it invokes no skill.
Read `shipwright_run_config.json`, then branch on `config.status`:
**`status == "complete"`:**
```
================================================================================
SHIPWRIGHT-RUN: COMPLETE — {runId}
================================================================================
Scope: {scope}
Profile: {profile}
Phases: {N} terminal ({done} done, {skipped} skipped)
Splits: {len(splits_frozen)} frozen
Deploy: {deploy_target}
Project artifacts:
- CLAUDE.md
- .shipwright/agent_docs/ (architecture, conventions, decision_log, session_handoff, build_dashboard)
- CHANGELOG.md
- .shipwright/compliance/ (RTM, test evidence, change history, SBOM, dashboard)
- shipwright_*_config.json files
For ongoing changes, use /shipwright-iterate.
================================================================================
```
**`status == "failed"`:**
```
================================================================================
SHIPWRIGHT-RUN: FAILED — {runId}
================================================================================
Failed phase tasks:
- {phase}{/splitId} (ptk={short}) errors:
- {error_line}
To recover, paste this in your terminal:
uv run "{plugin_root}/scripts/lib/orchestrator.py" recover-phase-task --phase-task-id {phaseTaskId}
- (add `--force-status awaiting_launch` for re-launch, or `--force-status skipped` to
- move on without that phase's output.)
+ (add `--force-status awaiting_launch` to re-run that phase, or `--force-status skipped`
+ to move on without its output.)
- Then re-invoke /shipwright-run to print a fresh launch card for the recovered phase
- (or paste the WebUI's launch-card command if the WebUI Kanban is in use).
+ Then re-invoke /shipwright-run — the loop picks up from the recovered phase.
================================================================================
```
**`status == "needs_validation"`:**
```
================================================================================
SHIPWRIGHT-RUN: COMPLETION BLOCKED — {runId}
================================================================================
Deploy completed, but other phase tasks are non-terminal:
- {phase}{/splitId} (ptk={short}) status={status}
To resolve, paste this in your terminal (one command per non-terminal task):
uv run "{plugin_root}/scripts/lib/orchestrator.py" recover-phase-task --phase-task-id {ptk} --force-status skipped
After all tasks are terminal, the next complete-phase-task call will
flip run.status to "complete".
================================================================================
```
**`status == "in_progress"`:** fall through to [Resume Support](#resume-support).
---
## Resume Support
- If `shipwright_run_config.json` exists at `schemaVersion: 2`:
+ The master conversation IS the driver, so a closed/crashed master is simply a
+ paused run. Re-invoking `/shipwright-run` on an existing `schemaVersion: 2`
+ config resumes it — see
+ [single-session-loop.md § Resumability](references/single-session-loop.md#resumability-ss5)
+ for the full protocol.
- 1. Read `config.phase_tasks[]` and `config.runId`.
- 2. Find the **next launchable** task — the first task with
- `status == "awaiting_launch"` (in `phase_tasks[]` order).
- 3. Detect **stale tasks** — entries with `status == "in_progress"` whose
- `claimAttemptedAt` is older than ~1 hour. These typically indicate a
- crashed phase session that never ran the Stop hook.
- 4. Determine `surface` — read `CLAUDE_CODE_ENTRYPOINT` (same as Step 5).
- `terminal` continues via the board's **Continue** (WebUI) or the paste card
- (CLI); `chat` (VS Code extension / desktop) can't launch a bound phase — send
- the user to a terminal or the Command Center.
+ 1. Read the resume decision (**read-only** — claims nothing, emits nothing):
+ ```bash
+ uv run "{plugin_root}/scripts/lib/orchestrator.py" single-session-resume \
+ --project-root "{project_root}"
+ ```
+ 2. Branch on `action`:
+ - `resume` → print the resume card (last-done phase, current phase, `attempt`,
+ and what resuming will do), then ask the user **Resume vs Abandon**
+ (constitution AskUserQuestion). On Resume, re-run with `--confirm` and
+ re-enter the loop at Step 5.
+ - `complete` / `failed` / `needs_validation` → the run already finished →
+ [Step 6](#step-6-final-wrap-up).
+ - `not_resumable` → single-session but nothing was ever dispatched → start the
+ loop normally (Step 5).
+ - `runid_mismatch` → the persisted loop-state belongs to a DIFFERENT run. Do
+ NOT resume; surface both run ids.
+ - `mode_unsupported` → this config is not a drivable single-session run (a
+ mode-less pre-SS1 config, or one still carrying the **removed**
+ `multi_session` mode). Print the returned `message`: the fix is to set
+ `"mode": "single_session"` in `shipwright_run_config.json` and re-invoke.
+ `phase_tasks[]` are shared and re-claim is idempotent, so no phase work is
+ lost. See `docs/migrations/multi-session-to-single-session.md`.
+ - `no_config` → not a pipeline run.
**Render a resume banner:**
```
================================================================================
RESUMING PIPELINE — {runId}
================================================================================
Status: {config.status}
Terminal: {N_terminal} / {N_total} phase tasks
Splits frozen: {len(splits_frozen)}
- {if next_launchable:}
- Next phase ready ({phase}{/splitId}):
- • Command Center — Continue it on the Task Board.
- • Terminal — paste:
- claude --session-id {sessionUuid} --add-dir "{projectRoot}" --name 'Run-{shortRunId} / {nameSuffix}' '{slashCommand}'
- {if surface == chat:} This chat (VS Code extension / desktop) can't launch it — use a terminal or the Command Center.
-
- {if stale_tasks:}
- Stale (likely crashed) phase tasks:
- - {phase}{/splitId} (ptk={short}) claimed at {claimAttemptedAt}
-
- To recover, paste this in your terminal:
- uv run "{plugin_root}/scripts/lib/orchestrator.py" recover-phase-task --phase-task-id {phaseTaskId}
+ Last completed: {loopState.lastCompletedPhaseTaskId}
+ Resuming at: {phase}{/splitId} (attempt {attempt})
- Then re-launch the relevant phase.
+ A phase left in_progress (the master died mid-phase) is re-dispatched
+ idempotently — the phase-runner was a subagent of the dead master, so no
+ orphaned worker can race this resume.
================================================================================
```
- If no `awaiting_launch` task exists and no stale tasks: pipeline is either
- in-progress (user is running a phase elsewhere — check the WebUI Kanban) or in
- one of the terminal states handled by Step 6.
-
- **Do not** invoke any slash command or modify state. Master stays a coordinator.
-
---
## Reference Documents
+ - [single-session-loop.md](references/single-session-loop.md) — the orchestrator loop (Step 5), gates, splits, resume, observability
- [inference-rules.md](references/inference-rules.md) — Scope + profile inference
- [brief-intake.md](references/brief-intake.md) — WebUI-wizard brief → profile/env, ask only what's missing (K2c)
- - [autonomy-levels.md](references/autonomy-levels.md) — Guided vs autonomous behavior (within phase sessions)
+ - [autonomy-levels.md](references/autonomy-levels.md) — Guided vs autonomous behavior (within phases)
- [scope-flows.md](references/scope-flows.md) — Full App and Extension flows
- - [single-session-loop.md](references/single-session-loop.md) — SS3 in-conversation orchestrator loop (`mode: single_session`)