/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_DIRvalue, 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
# --- 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/.md"
Examples:
cross-repo-plan-support→$PLANS_DIR/cross-repo-plan-support.mdcross-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
$ARGUMENTSbefore using it as the plan path - After extraction,
$ARGUMENTScontains 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_REASONto empty
First: Generate a unique run ID for this invocation
Tool: 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
# 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
# --- 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:
git status --porcelain(Bash)- Glob for
.claude/agents/coder*.md - Glob for
.claude/agents/code-reviewer*.md - Glob for
.claude/agents/qa-engineer*.mdor.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.
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-overrideflag is set: Log override reason. Downgrade to PASS_WITH_NOTES. Continue. Output: "Secrets scan BLOCKED — overridden: [reason]. Logged for audit trail." - If
--security-overrideflag 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
# 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 "$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 "$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: APPROVEDmarker (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
# 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 Requirementsheading to the next##heading or end of file) - Retain the extracted content in coordinator context for use in Step 4d
- Output: "Plan contains
## Security Requirementssection. 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 Requirementssection. Consider re-running/architector adding the section manually. Continuing (L1 advisory)." - At L2/L3 (enforced/audited):
- If
--security-overrideactive: 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 Requirementssection. Add the section or re-run/architect. To override:/ship [plan-path] --security-override \"reason\""
- If
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:
### 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 Modifytable - All files listed in the
### Files to Createtable
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 againstDEVKIT_TARGET_N_NAMEenvironment variables (the basename of each project directory, e.g.,cve-api). - Comparison is case-insensitive (e.g.,
**Target:** CVE-APImatchesDEVKIT_TARGET_0_NAME=cve-api). - The current project's name is determined by matching
$CWDagainstDEVKIT_TARGET_N_PATHvalues (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 (). Nothing to execute." and exit with PASS (no work to do is not an error -- the user may be running
/shipagainst 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
# 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
# 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 "$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:
- Directory placement: Are new files placed in the correct directories per CLAUDE.md structure?
- Naming conventions: Do new file/component names follow established patterns?
- Test requirements: Does the plan include tests where CLAUDE.md requires them?
- Architecture alignment: Does the plan respect module boundaries and dependency rules?
- Context metadata: Does the plan contain a
<!-- Context Metadatablock? (If yes, verifyclaude_md_existsistruewhen 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 "$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 "$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.mdin the project root and stop."
Then commit to local history (temporary commit for worktree base):
Tool: Bash
Command:
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 "$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 "$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")
# 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:
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 "$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
# 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.mdat {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:
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 "$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 "$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:
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
# 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 "$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:
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
# 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 "$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:
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 "$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
# 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)