# Ship

> Execute an approved plan using unattended implementation and validation with worktree isolation.

- Skill: `backspace-shmackspace/ship` (Agent Skill)
- Install (CLI): `npx skillmds@latest add backspace-shmackspace/ship`
- Raw SKILL.md: https://api.skillmd.com/api/skills/backspace-shmackspace/ship/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: backspace-shmackspace (https://skillmd.com/u/backspace-shmackspace)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/backspace-shmackspace/ship

---

# /ship Workflow

## Inputs
- Plan file: $ARGUMENTS   # Full path, or bare plan name (e.g. "cross-repo-plan-support")

## Output Rules
- **Always print full absolute paths** for all artifact references (plan files, review files, audit logs). This makes paths clickable in terminals like Warp. Use the resolved `$PLANS_DIR` value, never relative paths.

## Role
You are the **work coordinator**. You dispatch work to agents and check their results.
You do NOT write code, explore the codebase, or run tests yourself — agents do that.
Your job: read the plan once, dispatch each step, check verdicts, gate progression.

## Step 0 — Pre-flight checks

Tool: `Bash` (git status, cleanup), `Glob` (agent checks) — **Run all checks in parallel in a single message**

**Resolve devkit paths (MUST be first action in Step 0):**

Tool: `Bash`

```bash
# --- Devkit Path Resolution ---
DEVKIT_SCRIPTS="${CLAUDE_DEVKIT:-$HOME/.claude-devkit}/scripts"

# Source path resolution helper
if [ -f "$DEVKIT_SCRIPTS/resolve-project-dir.sh" ]; then
  . "$DEVKIT_SCRIPTS/resolve-project-dir.sh"
  DEVKIT_PROJECT_DIR_RESOLVED=$(resolve_devkit_project_dir) || {
    echo "Failed to resolve project directory" >&2; exit 1
  }
elif [ -n "${DEVKIT_PROJECT_DIR:-}" ]; then
  DEVKIT_PROJECT_DIR_RESOLVED="$DEVKIT_PROJECT_DIR"
else
  echo "WARNING: devkit is not installed. Using deprecated .devkit/ fallback." >&2
  DEVKIT_PROJECT_DIR_RESOLVED=".devkit"
fi

PLANS_DIR="$DEVKIT_PROJECT_DIR_RESOLVED/plans"
mkdir -p "$PLANS_DIR"
echo "Plans directory: $PLANS_DIR"
```

**Resolve plan path (MUST be second action in Step 0):**

If `$ARGUMENTS` (after removing any flags like `--security-override`) does not contain a `/` character,
treat it as a bare plan name and resolve it to `$PLANS_DIR/<name>.md`. If the name already ends with
`.md`, do not append it again. Verify the resolved file exists; if not, try without `.md` suffix removed
(in case the user passed e.g. `feature.md`). If neither exists, halt with:
"Plan not found: tried $PLANS_DIR/<name>.md"

Examples:
- `cross-repo-plan-support` → `$PLANS_DIR/cross-repo-plan-support.md`
- `cross-repo-plan-support.md` → `$PLANS_DIR/cross-repo-plan-support.md`
- `$PLANS_DIR/cross-repo-plan-support.md` → used as-is (contains `/`)
- `/absolute/path/to/plan.md` → used as-is (contains `/`)

After resolution, set `$PLAN_PATH` to the resolved absolute path for all subsequent steps.

**Parse --security-override flag (MUST be third action in Step 0):**

If `$ARGUMENTS` contains `--security-override`:
- Extract the reason string (quoted text after `--security-override`)
- Store as `$SECURITY_OVERRIDE_REASON`
- Remove the flag and reason from `$ARGUMENTS` before using it as the plan path
- After extraction, `$ARGUMENTS` contains ONLY the plan path for all subsequent steps
- Log: "Security override active. Reason: $SECURITY_OVERRIDE_REASON"

If `$ARGUMENTS` does not contain `--security-override`:
- Set `$SECURITY_OVERRIDE_REASON` to empty

**First: Generate a unique run ID for this invocation**

Tool: `Bash`

```bash
RUN_ID=$(date +%Y%m%d-%H%M%S)-$(cat /dev/urandom | LC_ALL=C tr -dc 'a-z0-9' | head -c 6)
echo "Ship run ID: $RUN_ID"
```

**Then: Clean up stale artifacts from previous runs**

Tool: `Bash`

```bash
# Prune orphaned worktrees
git worktree prune 2>/dev/null || true

# Clean up orphaned tracking files from aborted runs
for tracking_file in .ship-worktrees-*.tmp; do
  [ -f "$tracking_file" ] || continue
  ORPHANED=true
  while IFS='|' read -r wt_path _rest; do
    if git worktree list --porcelain | grep -q "^worktree $wt_path$"; then
      ORPHANED=false
      break
    fi
  done < "$tracking_file"
  if $ORPHANED; then
    rm -f "$tracking_file"
    echo "Cleaned up orphaned tracking file: $tracking_file"
  fi
done

# Clean up orphaned violation files
rm -f .ship-violations-*.tmp
```

**Then: Initialize audit logging**

Tool: `Bash`

```bash
# --- Audit Logging Setup ---
AUDIT_LOG_DIR="$PLANS_DIR/audit-logs"
mkdir -p "$AUDIT_LOG_DIR"
AUDIT_LOG="$AUDIT_LOG_DIR/ship-${RUN_ID}.jsonl"
STATE_FILE=".ship-audit-state-${RUN_ID}.json"

# L3 (audited): generate HMAC key and persist to disk for post-run chain verification
HMAC_KEY=""
if [ "$SECURITY_MATURITY" = "audited" ]; then
  HMAC_KEY=$(cat /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' | head -c 64 2>/dev/null || echo "")
  if [ -n "$HMAC_KEY" ]; then
    KEY_FILE=".ship-audit-key-${RUN_ID}"
    printf '%s' "$HMAC_KEY" > "$KEY_FILE"
    chmod 600 "$KEY_FILE"
    echo "L3 HMAC key written to $KEY_FILE (mode 0600)"
  else
    echo "Warning: Could not generate L3 HMAC key (/dev/urandom unavailable)."
  fi
fi

# Create state file for helper script
python3 -c "
import json
state = {
    'run_id': '${RUN_ID}',
    'audit_log': '${AUDIT_LOG}',
    'skill': 'ship',
    'skill_version': '3.9.0',
    'security_maturity': '${SECURITY_MATURITY}',
    'hmac_key': '${HMAC_KEY}'
}
with open('${STATE_FILE}', 'w') as f:
    json.dump(state, f)
print('Audit state file created: ${STATE_FILE}')
"

# Emit run_start event
OVERRIDE_ACTIVE="false"
[ -n "${SECURITY_OVERRIDE_REASON:-}" ] && OVERRIDE_ACTIVE="true"
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" "$STATE_FILE" \
  "{\"event_type\":\"run_start\",\"plan_file\":\"${PLAN_PATH:-${ARGUMENTS:-unknown}}\",\"security_override_active\":${OVERRIDE_ACTIVE}}"

# Emit step_start for step_0
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" "$STATE_FILE" \
  '{"event_type":"step_start","step":"step_0_preflight","step_name":"Pre-flight checks","agent_type":"coordinator"}'

echo "Audit log: $AUDIT_LOG"
```

**Then: Run validation checks in parallel:**

1. `git status --porcelain` (Bash)
2. Glob for `.claude/agents/coder*.md`
3. Glob for `.claude/agents/code-reviewer*.md`
4. Glob for `.claude/agents/qa-engineer*.md` or `.claude/agents/qa*.md`

**Fail fast if any check fails:**
- If git status is not empty: "❌ Working directory is not clean. Commit or stash changes before running /ship."
- If no coder agent found: "❌ No coder agent found. Generate one using:\n  `python3 ~/workspaces/claude-devkit/generators/generate_agents.py . --type coder`"
- If no code-reviewer agent found: "❌ No code-reviewer agent found. Generate one using:\n  `python3 ~/workspaces/claude-devkit/generators/generate_agents.py . --type code-reviewer`"
- If no qa-engineer agent found: "❌ No qa-engineer agent found. Generate one using:\n  `python3 ~/workspaces/claude-devkit/generators/generate_agents.py . --type qa-engineer`"

If **any** check fails, stop immediately and list all failures.

**Security maturity level check:**

Tool: `Bash`, `Read`

Read `.claude/settings.local.json` (if exists), then `.claude/settings.json` (if exists). Extract the `security_maturity` field. Precedence: if `.claude/settings.local.json` provides a `security_maturity` value (even if that value is `"advisory"`), the project-level setting is not consulted. The fallback to `.claude/settings.json` only occurs when the local file is absent or does not contain the `security_maturity` key.

**Note:** This block uses `python3 -c` for JSON parsing. Python 3 is available on all target platforms (macOS, Linux dev environments) and the `json` module handles edge cases (nested objects, whitespace, escaping) more reliably than regex-based alternatives. If `python3` is not available, the command silently fails and the maturity level defaults to `"advisory"` (L1) — the safe default. This is analogous to existing `/ship` pre-flight checks that use `git` and other CLI tools.

```bash
SECURITY_MATURITY="advisory"  # Default: L1
LOCAL_SET=0  # Track source, not value, to preserve precedence when local sets "advisory"

# Read local settings first (takes precedence)
if [ -f ".claude/settings.local.json" ]; then
  LOCAL_MATURITY=$(python3 -c "import json; d=json.load(open('.claude/settings.local.json')); print(d.get('security_maturity',''))" 2>/dev/null || echo "")
  if [ -n "$LOCAL_MATURITY" ]; then
    SECURITY_MATURITY="$LOCAL_MATURITY"
    LOCAL_SET=1
  fi
fi

# Only fall back to project settings if local did NOT provide a value
if [ "$LOCAL_SET" -eq 0 ] && [ -f ".claude/settings.json" ]; then
  PROJECT_MATURITY=$(python3 -c "import json; d=json.load(open('.claude/settings.json')); print(d.get('security_maturity',''))" 2>/dev/null || echo "")
  [ -n "$PROJECT_MATURITY" ] && SECURITY_MATURITY="$PROJECT_MATURITY"
fi

# Validate value
case "$SECURITY_MATURITY" in
  advisory|enforced|audited) ;;
  *) echo "Warning: Invalid security_maturity value '$SECURITY_MATURITY'. Defaulting to 'advisory'."
     SECURITY_MATURITY="advisory" ;;
esac

echo "Security maturity level: $SECURITY_MATURITY"
```

If `$SECURITY_MATURITY` is `enforced` or `audited`:

Tool: `Glob`

Check for required security skills:
- Glob `~/.claude/skills/secrets-scan/SKILL.md`
- Glob `~/.claude/skills/secure-review/SKILL.md`
- Glob `~/.claude/skills/dependency-audit/SKILL.md`

If ANY are missing, stop immediately:
"Security maturity level '$SECURITY_MATURITY' requires all security skills to be deployed.
Missing skills:
- [list missing skills]

Deploy with:
  cd ~/projects/claude-devkit && ./scripts/deploy.sh secrets-scan secure-review dependency-audit"

**Secrets scan gate (pre-flight):**

Tool: `Glob`

Glob for `~/.claude/skills/secrets-scan/SKILL.md`

**If found:**

Tool: `Task`, `subagent_type=general-purpose`, `model=claude-sonnet-4-6`

Prompt:
"You are running a pre-commit secrets scan as part of the /ship pre-flight check.

Read the secrets-scan skill definition at `~/.claude/skills/secrets-scan/SKILL.md`.
Execute it with scope `all` against the current repository working directory.

Report your verdict: PASS or BLOCKED.
If BLOCKED, list the confirmed secret types and file locations (NO actual secret values).
If PASS, report 'No secrets detected in working directory.'"

**If secrets scan returns BLOCKED:**
Secrets-scan BLOCKED blocks at ALL maturity levels (including L1). Committed secrets cannot be un-committed and require rotation.
- If `--security-override` flag is set: Log override reason. Downgrade to PASS_WITH_NOTES. Continue.
  Output: "Secrets scan BLOCKED — overridden: [reason]. Logged for audit trail."
- If `--security-override` flag is NOT set: Stop workflow.
  Output: "Secrets detected in working directory. Remove before shipping.
  If this is a false positive, re-run with: /ship $ARGUMENTS --security-override \"reason\""

**If not found:**
- If L1 (advisory): Log: "Security note: secrets-scan skill not deployed. Consider deploying for pre-commit secret detection."
- If L2/L3: Already caught by maturity level check above (will not reach here).

**Emit security_decision event for secrets scan gate:**

Tool: `Bash`

```bash
# Emit security_decision event for secrets scan result
# Replace GATE_VERDICT and ACTION with actual values from above check:
#   GATE_VERDICT: "PASS", "BLOCKED", or "not-run" (if skill not deployed)
#   ACTION: "pass", "block", "override", or "skip" (if not deployed)
#   EFFECTIVE_VERDICT: "PASS", "BLOCKED", or "PASS_WITH_NOTES" (if overridden)
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  "{\"event_type\":\"security_decision\",\"step\":\"step_0_preflight\",\"gate\":\"secrets_scan\",\"gate_verdict\":\"${SECRETS_GATE_VERDICT:-not-run}\",\"action\":\"${SECRETS_ACTION:-skip}\",\"effective_verdict\":\"${SECRETS_EFFECTIVE_VERDICT:-PASS}\"}"
```

**Emit step_end for Step 0:**

Tool: `Bash`

```bash
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_end","step":"step_0_preflight","step_name":"Pre-flight checks","agent_type":"coordinator"}'
```

## Step 1 — Coordinator reads plan

**Emit step_start for Step 1:**

Tool: `Bash`

```bash
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_start","step":"step_1_read_plan","step_name":"Coordinator reads plan","agent_type":"coordinator"}'
```

Tool: `Read` (direct — coordinator does this)

Read the plan file at `$ARGUMENTS`. Extract:
- **Files to modify/create** (from the Task Breakdown section)
- **Test command** (from the Test Plan section)
- **Acceptance criteria** (from the Acceptance Criteria section)

**Validate plan structure:** Verify the plan contains all required sections:
- Task Breakdown (required)
- Test Plan (required)
- Acceptance Criteria (required)
- `## Status: APPROVED` marker (required)

If any section is missing or plan is not approved, stop with:
"Plan at `$ARGUMENTS` is incomplete or not approved. Required: Task Breakdown, Test Plan, Acceptance Criteria, and ## Status: APPROVED marker. Run `/architect` first."

**Cross-repo plan target validation (conditional):**

Check whether the plan contains a `targets:` field in its YAML frontmatter. If so, validate that the current working directory matches one of the listed targets:

Tool: `Bash`

```bash
# Parse plan frontmatter for targets field via devkit_cli parser
TARGETS_JSON=$(PLAN_PATH_ARG="$PLAN_PATH" SCRIPTS_ARG="$DEVKIT_SCRIPTS" python3 -c "
import sys, json, os
plan_path = os.environ['PLAN_PATH_ARG']
scripts_dir = os.environ['SCRIPTS_ARG']
sys.path.insert(0, scripts_dir)
from devkit_cli import parse_plan_frontmatter
content = open(plan_path).read()
fm, err = parse_plan_frontmatter(content)
if err or 'targets' not in fm:
    sys.exit(0)
print(json.dumps(fm['targets']))
" 2>/dev/null || echo "")

if [ -n "$TARGETS_JSON" ]; then
  echo "Cross-repo plan detected."
  MATCH_RESULT=$(echo "$TARGETS_JSON" | python3 -c "
import json, os, sys
targets = json.loads(sys.stdin.read())
cwd = os.path.realpath(os.getcwd())
match_role = None
primary_name = None
for t in targets:
    path = os.path.expanduser(t.get('path', ''))
    resolved = os.path.realpath(path) if path else ''
    role = t.get('role', 'secondary')
    if role == 'primary':
        primary_name = os.path.basename(resolved)
    if resolved == cwd:
        match_role = role
if match_role is None:
    print('BLOCKED')
elif match_role == 'secondary':
    print('SECONDARY:' + (primary_name or 'unknown'))
else:
    print('PRIMARY')
" 2>/dev/null)

  case "$MATCH_RESULT" in
    BLOCKED)
      echo "ERROR: Current directory does not match any target in plan frontmatter."
      exit 1
      ;;
    SECONDARY:*)
      PRIMARY_NAME="${MATCH_RESULT#SECONDARY:}"
      CWD_NAME=$(basename "$(pwd -P)")
      echo "WARNING: This plan's primary target is $PRIMARY_NAME. Running against secondary target $CWD_NAME. Only work groups targeting this repo will be executed."
      ;;
    PRIMARY)
      echo "CWD matches primary target."
      ;;
  esac
fi
```

If CWD does not match any target: **BLOCK** the workflow with "Current directory does not match any target in plan frontmatter."

If CWD matches a secondary target: log a warning and continue. Only work groups targeting this repo will be executed (see target filtering below).

If `targets:` is not present in frontmatter (single-project plan): skip this check entirely (existing behavior unchanged).

**Security requirements validation (conditional):**

Check whether this plan contains a `## Security Requirements` section:

Tool: `Grep` (direct -- coordinator does this)

Search the plan text for the heading `## Security Requirements`.

**If `## Security Requirements` section is found:**
- Extract the section content (from `## Security Requirements` heading to the next `##` heading or end of file)
- Retain the extracted content in coordinator context for use in Step 4d
- Output: "Plan contains `## Security Requirements` section. Threat model context will be passed to /secure-review."

**If `## Security Requirements` section is NOT found:**

Check whether the plan's content contains security signals by scanning for the
same keyword categories used by /architect Step 2 Stage 1 (Identity/Auth,
Cryptography/Network, Data/Compliance, File/Process, Payment keywords) applied
against the plan body text:

**If security signals found in plan content:**
- At L1 (advisory): Output warning: "This plan appears to involve security-sensitive functionality but does not contain a `## Security Requirements` section. Consider re-running `/architect` or adding the section manually. Continuing (L1 advisory)."
- At L2/L3 (enforced/audited):
  - If `--security-override` active: Output warning (same as L1) and log override. Continue.
  - If no override: Stop workflow. Output: "This plan appears to involve security-sensitive functionality but does not contain a `## Security Requirements` section. Add the section or re-run `/architect`. To override: `/ship [plan-path] --security-override \"reason\"`"

**If no security signals found in plan content:**
- No output (plan is not security-sensitive, no check needed)

Derive `[name]` from the plan filename (e.g. `$PLANS_DIR/feature-x.md` → `feature-x`).

**Parse work groups (optional):** Look for a `## Work Groups` section inside the Task Breakdown. Format:

```markdown
### Work Group 1: [name]
- file-a.ts
- file-b.ts

### Work Group 2: [name]
- file-c.ts
- file-d.ts

### Shared Dependencies
- src/types.ts (modify — implement before work groups)
```

If no `## Work Groups` section exists, treat the entire Task Breakdown as a single group. Derive the `scoped_files` list by extracting ALL files from the Task Breakdown section:
- All files listed in the `### Files to Modify` table
- All files listed in the `### Files to Create` table

Store these as the `scoped_files` for the single implicit work group. This list is used in Step 3d (boundary validation) and Step 3e (merge).

**Cross-repo work group target filtering (conditional):**

If the plan has `targets:` frontmatter (cross-repo plan detected in the target validation above), filter work groups by the `**Target:**` annotation in each work group header:

**Matching algorithm:**
- The `**Target:** <name>` value is compared against `DEVKIT_TARGET_N_NAME` environment variables (the basename of each project directory, e.g., `cve-api`).
- Comparison is **case-insensitive** (e.g., `**Target:** CVE-API` matches `DEVKIT_TARGET_0_NAME=cve-api`).
- The current project's name is determined by matching `$CWD` against `DEVKIT_TARGET_N_PATH` values (same resolution as the target validation above).

**Edge case behavior:**
- **Unannotated work group** (no `**Target:**` line) in a multi-target plan: Treated as primary-target-only. Log a warning: "Work group N has no target annotation; treating as primary target only."
- **Mismatched target name** (`**Target:** cve-apii` -- typo): The work group does not match any known target. It is skipped with a warning: "Work group N targets unknown project 'cve-apii'; skipping."
- **No matching work groups**: If no work groups match the current project after filtering, log: "No work groups target this project (<name>). Nothing to execute." and exit with PASS (no work to do is not an error -- the user may be running `/ship` against secondary targets in sequence).

After filtering, only matching work groups are passed to Steps 3b-3f for worktree creation and dispatch.

If the plan does not have `targets:` frontmatter (single-project plan), skip this filtering entirely and process all work groups (existing behavior unchanged).

**Run codebase scanner:**

Tool: `Bash`

```bash
# Run codebase scanner (degrades gracefully if tree-sitter not installed)
SCANNER_PYTHON="${HOME}/.claude-devkit/scanner-venv/bin/python3"
SCANNER_SCRIPT="$DEVKIT_SCRIPTS/codebase-scanner.py"
if [ -x "$SCANNER_PYTHON" ]; then
  SCANNER_OUTPUT=$("$SCANNER_PYTHON" "$SCANNER_SCRIPT" --format summary --quiet 2>/dev/null || echo "")
else
  SCANNER_OUTPUT=$(python3 "$SCANNER_SCRIPT" --format summary --quiet 2>/dev/null || echo "")
fi
echo "$SCANNER_OUTPUT"

# Emit scanner_invocation audit event
if [ -n "$SCANNER_OUTPUT" ]; then
  SCANNER_HASH=$(printf '%s' "$SCANNER_OUTPUT" | python3 -c "import sys,hashlib; print(hashlib.sha256(sys.stdin.read().encode()).hexdigest())" 2>/dev/null || echo "unknown")
  SCANNER_VERSION=$(python3 "$SCANNER_SCRIPT" --version 2>/dev/null | awk '{print $NF}' || echo "unknown")
  SCANNER_FILE_COUNT=$(printf '%s' "$SCANNER_OUTPUT" | grep -oP 'Files:\s*\K[0-9]+' 2>/dev/null || echo "0")
  SCANNER_SYMBOL_COUNT=$(printf '%s' "$SCANNER_OUTPUT" | grep -oP 'Symbols:\s*\K[0-9]+' 2>/dev/null || echo "0")
  SCANNER_PARSER_MODE=$(printf '%s' "$SCANNER_OUTPUT" | grep -oP 'Parser:\s*\K\S+' 2>/dev/null || echo "unknown")
  SCANNER_TOKEN_COUNT=$(printf '%s' "$SCANNER_OUTPUT" | wc -c | awk '{printf "%.0f", $1 / 4}')
  bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
    "{\"event_type\":\"scanner_invocation\",\"scanner_version\":\"${SCANNER_VERSION}\",\"parser_mode\":\"${SCANNER_PARSER_MODE}\",\"file_count\":${SCANNER_FILE_COUNT},\"symbol_count\":${SCANNER_SYMBOL_COUNT},\"output_sha256\":\"${SCANNER_HASH}\",\"output_token_count\":${SCANNER_TOKEN_COUNT}}"
fi
```

Include `$SCANNER_OUTPUT` in coder dispatch prompts (Step 3c) under the heading `### Codebase Structure (auto-generated)`. If scanner output is empty, include "Scanner not available. Coder should discover structure via file reads."

**Emit step_end for Step 1:**

Tool: `Bash`

```bash
# SEC_REQ_PRESENT: "true" if ## Security Requirements section was found in the plan, "false" otherwise
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  "{\"event_type\":\"step_end\",\"step\":\"step_1_read_plan\",\"step_name\":\"Coordinator reads plan\",\"agent_type\":\"coordinator\",\"security_requirements_present\":${SEC_REQ_PRESENT:-false}}"
```

## Step 2 — Pattern Validation (warnings only)

**Emit step_start for Step 2:**

Tool: `Bash`

```bash
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_start","step":"step_2_pattern_validation","step_name":"Pattern validation","agent_type":"coordinator"}'
```

Validate the plan against project patterns before implementation. This step produces warnings but does NOT block the workflow.

Tool: `Read` (direct — coordinator does this)

**Read `./CLAUDE.md`** (if exists). Extract:
- Directory structure conventions
- Naming conventions (files, variables, components)
- Required test patterns
- Architecture patterns (module boundaries, dependency direction)
- Technology stack constraints

**Compare plan against patterns:**

Check each file in the plan's Task Breakdown against CLAUDE.md conventions:

1. **Directory placement:** Are new files placed in the correct directories per CLAUDE.md structure?
2. **Naming conventions:** Do new file/component names follow established patterns?
3. **Test requirements:** Does the plan include tests where CLAUDE.md requires them?
4. **Architecture alignment:** Does the plan respect module boundaries and dependency rules?
5. **Context metadata:** Does the plan contain a `<!-- Context Metadata` block? (If yes, verify `claude_md_exists` is `true` when a CLAUDE.md exists)

**Output format:**

If warnings found, output:

    Pattern validation warnings (non-blocking):

    1. [Warning description -- e.g., "New file src/utils/auth.ts -- CLAUDE.md places utilities in lib/"]
    2. [Warning description]
    ...

    These warnings are informational. The workflow will continue.
    To address these, revise the plan and re-run /ship.

If no warnings, output:

    Plan aligns with CLAUDE.md patterns.

**If CLAUDE.md does not exist:**

    No CLAUDE.md found. Skipping pattern validation.
    Consider running /sync to generate project documentation.

Continue to Step 3 regardless of warnings.

**Emit step_end for Step 2:**

Tool: `Bash`

```bash
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_end","step":"step_2_pattern_validation","step_name":"Pattern validation","agent_type":"coordinator"}'
```

## Step 3 — Implementation (with worktree isolation)

Every implementation runs in isolated git worktrees, regardless of how many work groups
the plan defines. This ensures concurrent sessions cannot interfere with the implementation.

#### Step 3a — Shared Dependencies (conditional)

**Trigger:** Plan contains `### Shared Dependencies` section. If no Shared Dependencies
section exists, skip directly to Step 3b.

**Emit step_start for Step 3a:**

Tool: `Bash`

```bash
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_start","step":"step_3a_shared_deps","step_name":"Shared dependencies","agent_type":"coder"}'
```

Tool: `Task`, `subagent_type=general-purpose`, `model=claude-sonnet-4-6`

Implement shared files in main working directory with single coder agent:

"You are implementing shared dependencies for a plan. Read the plan at `$ARGUMENTS`.
Then read the `.claude/agents/` directory to find the coder agent that matches this work.

**Your scope:** Shared Dependencies
**Your files:**
- [list files from Shared Dependencies section]

Hard rules:
- Only modify files listed in Shared Dependencies. Do not touch work group files.
- Follow the plan exactly. These files will be used by all work groups.
- If blocked, write `BLOCKED.md` in the project root and stop."

Then commit to local history (temporary commit for worktree base):

Tool: `Bash`

Command:
```bash
git add <shared-files> && git commit -m "WIP: /ship shared dependencies for ${name}

This is a temporary commit that will be squashed with the final implementation in Step 6.
Created by: /ship skill v3.9.0"
```

**Emit step_end for Step 3a:**

Tool: `Bash`

```bash
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_end","step":"step_3a_shared_deps","step_name":"Shared dependencies","agent_type":"coder"}'
```

#### Step 3b — Create Worktrees

**Emit step_start for Step 3b:**

Tool: `Bash`

```bash
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_start","step":"step_3b_create_worktrees","step_name":"Create worktrees","agent_type":"coordinator"}'
```

Tool: `Bash`

For each work group (parsed in Step 1), create isolated worktree.

**Coordinator instructions:**
- Replace `${name}` with the plan name from Step 1 (e.g., "add-user-auth")
- Replace `${wg_num}` with work group index (1, 2, 3, ...)
- Replace `${wg_name}` with work group name from plan (e.g., "Authentication")
- Replace `${scoped_files}` with space-separated file list from plan (e.g., "src/auth.ts src/middleware.ts")

```bash
# Create worktree with secure, unique path
WORKTREE_PATH=$(mktemp -d /tmp/ship-XXXXXXXXXX)

# These variables come from Step 1 plan parsing
WG_NUM="${wg_num}"   # e.g., 1, 2, 3
WG_NAME="${wg_name}" # e.g., "Authentication"
SCOPED_FILES="${scoped_files}"  # e.g., "src/auth.ts src/middleware.ts"

# Create worktree with error handling
if ! git worktree add "$WORKTREE_PATH" -b "ship-wg${WG_NUM}-${RUN_ID}" HEAD 2>/dev/null; then
  echo "❌ Failed to create worktree at $WORKTREE_PATH"
  echo "Possible causes: path exists, disk full, git locked"
  rm -f .ship-worktrees-${RUN_ID}.tmp
  exit 1
fi

# Store worktree info (pipe-delimited: path|num|name|files)
echo "$WORKTREE_PATH|$WG_NUM|$WG_NAME|$SCOPED_FILES" >> .ship-worktrees-${RUN_ID}.tmp
```

Using `mktemp -d` ensures:
- The directory is created with 0700 permissions (not world-readable)
- The path contains a random suffix, eliminating symlink/TOCTOU attacks
- Kernel-guaranteed uniqueness, no PID or timestamp collisions

**Validation:** After creating all worktrees, verify tracking file exists:

```bash
if [ ! -f .ship-worktrees-${RUN_ID}.tmp ] || [ ! -s .ship-worktrees-${RUN_ID}.tmp ]; then
  echo "❌ No worktrees were created. Check Step 3b output."
  exit 1
fi
```

Output: "✓ Created worktree for Work Group ${wg_num}: ${wg_name} at $WORKTREE_PATH"

**Emit step_end for Step 3b:**

Tool: `Bash`

```bash
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_end","step":"step_3b_create_worktrees","step_name":"Create worktrees","agent_type":"coordinator"}'
```

#### Step 3c — Dispatch Coders to Worktrees

**Emit step_start for Step 3c:**

Tool: `Bash`

```bash
# WG_COUNT = number of work groups dispatched
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  "{\"event_type\":\"step_start\",\"step\":\"step_3c_dispatch_coders\",\"step_name\":\"Dispatch coders to worktrees\",\"agent_type\":\"coder\",\"work_groups\":${WG_COUNT:-1}}"
```

Tool: `Task`, `subagent_type=general-purpose`, `model=claude-sonnet-4-6` — **dispatch one coder per work group (parallel if multiple, single Task call if one group)**

Each coder receives this prompt (scoped to its worktree):

"You are implementing part of a plan in an isolated worktree. Read the plan at `$ARGUMENTS`.

**CRITICAL: You are working in an isolated worktree at:**
`{WORKTREE_PATH}`

All file operations must use absolute paths within this worktree. The worktree is a complete copy of the repository with shared dependencies already applied.

**Your scope:** Work Group N: [group-name]
**Your files:**
- [list files from this work group only]

**File operation examples:**
- Read: Read tool with {WORKTREE_PATH}/src/components/Button.tsx
- Edit: Edit tool with {WORKTREE_PATH}/src/components/Button.tsx
- Write: Write tool with {WORKTREE_PATH}/src/utils/helpers.ts

Hard rules:
- Only modify files listed in your scope within {WORKTREE_PATH}.
- Do not access files outside your worktree.
- Follow the plan exactly. Do not expand scope.
- If blocked on something you cannot resolve, write `BLOCKED.md` at {WORKTREE_PATH}/BLOCKED.md and stop.

**Learnings (optional):**
If the file `.claude/learnings.md` exists, read the `## Coder Patterns` section before starting implementation. Apply any relevant learnings to avoid known recurring issues. Do not mention the learnings file in your output — just apply the patterns silently."

**After all coders finish:**

Tool: `Bash`

Check for BLOCKED.md in any worktree:

```bash
while IFS='|' read -r wt_path wg_num wg_name scoped_files; do
  if [ -f "$wt_path/BLOCKED.md" ]; then
    echo "Implementation blocked in Work Group $wg_num. See worktree at $wt_path"
    cat "$wt_path/BLOCKED.md"
    exit 1
  fi
done < .ship-worktrees-${RUN_ID}.tmp
```

If any worktree has BLOCKED.md, stop workflow and output: "❌ Implementation blocked. See output above."

**Emit step_end for Step 3c:**

Tool: `Bash`

```bash
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_end","step":"step_3c_dispatch_coders","step_name":"Dispatch coders to worktrees","agent_type":"coder"}'
```

#### Step 3d — File Boundary Validation

**Emit step_start for Step 3d:**

Tool: `Bash`

```bash
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_start","step":"step_3d_boundary_validation","step_name":"File boundary validation","agent_type":"coordinator"}'
```

Tool: `Bash`

For each worktree, verify agents only modified scoped files:

```bash
VIOLATIONS=""
MAIN_DIR=$(pwd)

while IFS='|' read -r wt_path wg_num wg_name scoped_files; do
  cd "$wt_path"

  # Get all modified files (working directory + index + committed)
  # This catches Edit/Write tool changes even if not staged
  MODIFIED=$(git status --porcelain | awk '{print $2}')
  # Known limitation: awk '{print $2}' does not correctly handle renamed files
  # (R old -> new captures only 'old') or file paths containing spaces.
  # The merge step (3e) is the primary safety boundary — it copies only scoped files.
  # Improving this parsing is deferred to a follow-up change.

  # If nothing modified, check against HEAD~1 (for committed changes)
  if [ -z "$MODIFIED" ] && git rev-parse HEAD~1 >/dev/null 2>&1; then
    MODIFIED=$(git diff --name-only HEAD~1 HEAD)
  fi

  # Validate each modified file is in scoped files (exact match)
  for file in $MODIFIED; do
    FOUND=0

    # Normalize paths (remove leading ./)
    normalized_file=$(echo "$file" | sed 's|^\./||')

    # Check against each scoped file (space-separated list)
    for scoped in $scoped_files; do
      normalized_scoped=$(echo "$scoped" | sed 's|^\./||')

      if [ "$normalized_file" = "$normalized_scoped" ]; then
        FOUND=1
        break
      fi
    done

    if [ $FOUND -eq 0 ]; then
      VIOLATIONS="${VIOLATIONS}Work Group $wg_num ($wg_name) modified $file (not in scope: $scoped_files)\n"
    fi
  done

  cd "$MAIN_DIR"
done < .ship-worktrees-${RUN_ID}.tmp

if [ -n "$VIOLATIONS" ]; then
  echo -e "$VIOLATIONS" > .ship-violations-${RUN_ID}.tmp
fi
```

**Verdict gate:**

Read `.ship-violations-${RUN_ID}.tmp`. If exists and non-empty:

Output:
```
❌ File boundary violations detected:

[contents of .ship-violations-${RUN_ID}.tmp]

Agents modified files outside their assigned scope. This is a critical error.
Workflow stopped. Review agent behavior and retry.
```

**STOP workflow** — do not proceed to Step 3e.

**Emit verdict event for boundary check:**

Tool: `Bash`

```bash
# BOUNDARY_VERDICT: "PASS" if no violations, "BLOCKED" if violations detected
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  "{\"event_type\":\"verdict\",\"step\":\"step_3d_boundary_validation\",\"verdict\":\"${BOUNDARY_VERDICT:-PASS}\",\"verdict_source\":\"boundary_check\",\"agent_type\":\"coordinator\"}"

bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_end","step":"step_3d_boundary_validation","step_name":"File boundary validation","agent_type":"coordinator"}'
```

If no violations, continue to Step 3e.

#### Step 3e — Merge Worktrees

**Emit step_start for Step 3e:**

Tool: `Bash`

```bash
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_start","step":"step_3e_merge","step_name":"Merge worktrees","agent_type":"coordinator"}'
```

Tool: `Bash`

For each worktree, copy scoped files to main working directory:

```bash
MAIN_DIR=$(pwd)

while IFS='|' read -r wt_path wg_num wg_name scoped_files; do
  echo "Merging Work Group $wg_num: $wg_name"

  for file in $scoped_files; do
    if [ -f "$wt_path/$file" ]; then
      mkdir -p "$MAIN_DIR/$(dirname "$file")"
      cp "$wt_path/$file" "$MAIN_DIR/$file"
      echo "  ✓ Merged $file"
    fi
  done
done < .ship-worktrees-${RUN_ID}.tmp

# Post-merge validation: verify all scoped files exist in main directory
while IFS='|' read -r wt_path wg_num wg_name scoped_files; do
  for file in $scoped_files; do
    if [ ! -f "$MAIN_DIR/$file" ]; then
      echo "WARNING: Scoped file $file was not created by coder in worktree"
    fi
  done
done < .ship-worktrees-${RUN_ID}.tmp
```

Post-merge validation emits warnings but does not block the workflow. A file may legitimately
not need creation if it already existed in the main directory before the worktree was
created (e.g., a file listed under "Files to Modify" that the coder chose not to change).
The code review in Step 4 serves as the catch for genuinely missing files.

Output: "✓ Merged N work groups (X files total)"

**Emit file_modification event per work group, then step_end for Step 3e:**

For each work group processed in the merge loop above, emit a `file_modification` event using the actual values from that iteration (`WG_NUM`, `WG_NAME`, and the list of scoped files as a JSON array):

Tool: `Bash`

```bash
# Emit one file_modification event per work group using actual loop values.
# Construct FILES_JSON as a JSON array of the scoped files for this work group.
# Example (replace WG_NUM, WG_NAME, and FILES_JSON with actual values):
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  "{\"event_type\":\"file_modification\",\"step\":\"step_3e_merge\",\"work_group\":${WG_NUM},\"work_group_name\":\"${WG_NAME}\",\"files_modified\":${FILES_JSON}}"

bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_end","step":"step_3e_merge","step_name":"Merge worktrees","agent_type":"coordinator"}'
```

#### Step 3f — Cleanup Worktrees

**Emit step_start for Step 3f:**

Tool: `Bash`

```bash
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_start","step":"step_3f_cleanup","step_name":"Cleanup worktrees","agent_type":"coordinator"}'
```

Tool: `Bash`

Remove all worktrees and temporary files:

```bash
CLEANUP_FAILURES=0

while IFS='|' read -r wt_path wg_num wg_name scoped_files; do
  if ! git worktree remove "$wt_path" --force 2>/dev/null; then
    echo "⚠️  Failed to remove worktree: $wt_path"
    CLEANUP_FAILURES=$((CLEANUP_FAILURES + 1))
  else
    echo "✓ Removed worktree for Work Group $wg_num"
  fi
done < .ship-worktrees-${RUN_ID}.tmp

# Report cleanup failures but don't block workflow
if [ $CLEANUP_FAILURES -gt 0 ]; then
  echo "⚠️  $CLEANUP_FAILURES worktree(s) failed to clean up. Manual cleanup:"
  echo "    git worktree prune"
  echo "    rm -rf /tmp/ship-*"
fi

# Clean up tracking files
rm -f .ship-worktrees-${RUN_ID}.tmp .ship-violations-${RUN_ID}.tmp
```

**Note:** Cleanup failures are logged but don't block the workflow. Orphaned worktrees can be cleaned manually with `git worktree prune` or automatically by the pre-flight check in Step 0.

**Emit step_end for Step 3f:**

Tool: `Bash`

```bash
bash "$DEVKIT_SCRIPTS/emit-audit-event.sh" ".ship-audit-state-${RUN_ID}.json" \
  '{"event_type":"step_end","step":"step_3f_cleanup","step_name":"Cleanup worktrees","agent_type":"coordinator"}'
```

## Step 4 — Parallel verification

**Pre-step: Compute import graph for blast radius (runs before parallel dispatch):**

Tool: `Bash`

```bash
# Extract import graph for files changed in this ship run, for blast radius assessment
SCANNER_PYTHON="${HOME}/.claude-devkit/scanner-venv/bin/python3"
SCANNER_SCRIPT="$DEVKIT_SCRIPTS/codebase-scanner.py"

# Get list of changed files
CHANGED_FILES=$(git diff --name-only HEAD 2>/dev/null || echo "")

IMPORT_GRAPH_DATA=""
if [ -n "$CHANGED_FILES" ] && [ -f "$SCANNER_SCRIPT" ]; then
  # Run scanner in JSON mode to get full import graph
  if [ -x "$SCANNER_PYTHON" ]; then
    SCANNER_JSON=$("$SCANNER_PYTHON" "$SCANNER_SCRIPT" --format json --quiet 2>/dev/null || echo "")
  else
    SCANNER_JSON=$(python3 "$SCANNER_SCRIPT" --format json --quiet 2>/dev/null || echo "")
  fi

  if [ -n "$SCANNER_JSON" ]; then
    # Extract import edges where source_file is one of the changed files
    IMPORT_GRAPH_DATA=$(SCANNER_JSON_VAR="$SCANNER_JSON" CHANGED_FILES_VAR="$CHANGED_FILES" \
      python3 -c "
import json, os
scanner_json = os.environ.get('SCANNER_JSON_VAR', '')
changed_files_str = os.environ.get('CHANGED_FILES_VAR', '')
try:
    data = json.loads(scanner_json)
    changed = set(f.strip() for f in changed_files_str.splitlines() if f.strip())
    imports = data.get('imports', [])
    relevant = [i for i in imports if i.get('source_file', '') in changed]
    if not relevant:
        print('No import edges found for changed files.')
    else:
        lines = ['| Source File | Imports | Kind |', '|---|---|---|']
        for imp in relevant:
            lines.append(f\"| {imp.get('source_file','')} | {imp.get('target','')} | {imp.get('kind','')} |\")
        print('\n'.join(lines))
except Exception as e:
    print(f'Import graph extraction failed: {e}')
" 2>/dev/null || echo "Import graph unavailable.")
  fi
fi

echo "Import graph data computed (${#IMPORT_GRAPH_DATA} chars)"
```

Retain `$IMPORT_GRAPH_DATA` in coordinator context for inclusion in the Step 4a code reviewer prompt.

Tool: `Task` (code review, QA), `Bash` (tests) — **Run all three checks in parallel in a single message**

Run these verification tasks in parallel (3 or 4 tasks depending on security skill deployment):

### 4a — Code review

Tool: `Task`, `subagent_type=general-purpose`, `model=claude-sonnet-4-6`

Prompt:
"You are reviewing code changes against a plan. Read the plan at `$ARGUMENTS`.
Then read the `.claude/agents/` directory to find the code-reviewer agent.
Follow that agent's r

…(truncated)
