Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 18 additions & 30 deletions plugins/eng/skills/implement/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,53 +428,37 @@ Put spec.json at `tmp/ship/spec.json`. Create `tmp/ship/` if it doesn't exist (`

If on `main` or `master`, warn before proceeding — the implement skill should normally run on a feature branch. If no branching model exists (e.g., container environment with no PR workflow), proceed with caution and ensure commits are isolated.

### Craft the implementation prompt
### Template assembly (handled by implement.sh)

**Load:** `templates/implement-prompt.template.md`
The `implement.sh` script handles all template assembly automatically. It reads `templates/implement-prompt.template.md`, selects the correct variant based on `--spec-path`, extracts `implementationContext` from spec.json, computes a fresh git diff each iteration, and fills all `{{PLACEHOLDERS}}`.

The template contains two complete prompt variants with `{{PLACEHOLDER}}` syntax. Choose ONE variant and fill all placeholders.
**Your job in Phase 2:** Ensure spec.json has a rich `implementationContext` field — this becomes the iteration agent's codebase context. Include specific patterns, shared vocabulary, abstractions in the area being modified, and repo conventions from CLAUDE.md (testing patterns, file locations, formatting). See Phase 1's implementationContext guidance for details.

**Choose variant:**
- **Variant A** — when a SPEC.md path is available (directly provided or from Phase 1)
- **Variant B** — when only spec.json is available (no SPEC.md)
**You do NOT need to:** read the template, select a variant, fill placeholders, or save a prompt file. The script does all of this.

**Conditionality lives HERE (in Phase 2 construction), NOT in the iteration prompt.** The iteration agent sees a single, unconditional workflow — never both variants, never conditional "if spec is available" logic.
### Copy artifacts to execution location

**Fill `{{CODEBASE_CONTEXT}}`:** Include the specific patterns, shared vocabulary, and abstractions in the area being modified — more actionable than generic CLAUDE.md guidance. Examples: "The API follows RESTful patterns under /api/tasks/", "Auth uses tenant-scoped middleware in auth.ts", "Data access uses the repository pattern in data-access/". Also include repo conventions from CLAUDE.md (testing patterns, file locations, formatting) that the iteration agent needs.

**Fill quality gate commands:** Use the commands from Inputs (defaults: `pnpm typecheck`, `pnpm lint`, `pnpm test --run`) — override with `--typecheck-cmd`, `--lint-cmd`, `--test-cmd` if provided.

**Fill `{{SPEC_PATH}}`** (Variant A only): Use a path relative to the working directory (e.g., `.claude/specs/my-feature/SPEC.md`). Relative paths work across execution contexts (host, Docker, worktree). Do NOT use absolute paths — they break when the prompt is executed in a different environment. Do NOT embed spec content in the prompt — the iteration agent reads it via the Read tool each iteration.

### Save the prompt

Save the crafted implementation prompt to `tmp/ship/implement-prompt.md`. This file is consumed by Phase 3 (`scripts/implement.sh`) for automated execution, or by the user for manual iteration (`claude -p`).

### Copy implement.sh to execution location

Copy the skill's canonical `scripts/implement.sh` to `tmp/ship/implement.sh` in the working directory and make it executable:
Copy the skill's canonical `scripts/implement.sh` and template to `tmp/ship/` in the working directory and make the script executable:

```bash
cp <path-to-skill>/scripts/implement.sh tmp/ship/implement.sh
cp <path-to-skill>/templates/implement-prompt.template.md tmp/ship/implement-prompt.template.md
chmod +x tmp/ship/implement.sh
```

This places the iteration loop script alongside the implementation prompt (`tmp/ship/implement-prompt.md`) as a paired execution artifact. The copy enables:
This places the iteration loop script and template as paired execution artifacts. The copy enables:
- **Manual execution** — users can run `tmp/ship/implement.sh --force` directly without knowing the skill's install path
- **Docker execution** — containers access `tmp/ship/implement.sh` via bind mount (see `references/execution.md`)

Phase 3 on host uses the skill's own `scripts/implement.sh` directly (validate-spec.ts is available next to it). The `tmp/ship/implement.sh` copy is for Docker, manual, and external execution contexts.
Phase 3 on host uses the skill's own `scripts/implement.sh` directly (template is auto-detected next to it). The `tmp/ship/` copies are for Docker, manual, and external execution contexts.

### Phase 2 checklist

- [ ] spec.json validated against SPEC.md (if available)
- [ ] spec.json schema validated (via `scripts/validate-spec.ts` if bun available)
- [ ] Stories correctly scoped, ordered, and with verifiable criteria
- [ ] Implementation prompt crafted from template with correct variant (A or B)
- [ ] All `{{PLACEHOLDERS}}` filled (spec path, quality gates, codebase context)
- [ ] Completion signal present in saved prompt (included in template — verify not accidentally removed)
- [ ] Prompt saved to `tmp/ship/implement-prompt.md`
- [ ] `implement.sh` copied to `tmp/ship/implement.sh` and made executable
- [ ] `implementationContext` is rich and actionable (architecture, constraints, key decisions, codebase patterns)
- [ ] `implement.sh` and template copied to `tmp/ship/` and made executable

---

Expand Down Expand Up @@ -566,23 +550,27 @@ Count the incomplete stories in spec.json and select parameters:

Run in background to avoid the Bash tool's 600-second timeout.

Pass `--spec-path` when a SPEC.md is available (selects Variant A of the prompt template). Pass quality gate commands if overridden from defaults.

**Host execution (default):**

```
Bash(command: "<path-to-skill>/scripts/implement.sh --max-iterations <N> --max-turns <M> --force",
Bash(command: "<path-to-skill>/scripts/implement.sh --max-iterations <N> --max-turns <M> --force --spec-path <SPEC_PATH> --typecheck-cmd '<CMD>' --lint-cmd '<CMD>' --test-cmd '<CMD>'",
run_in_background: true,
description: "Implement execution run 1")
```

**Docker execution (when `--docker` was passed — compose file resolved in Step 3):**

```
Bash(command: "docker compose -f <compose-file> exec sandbox tmp/ship/implement.sh --max-iterations <N> --max-turns <M> --force",
Bash(command: "docker compose -f <compose-file> exec sandbox tmp/ship/implement.sh --max-iterations <N> --max-turns <M> --force --template tmp/ship/implement-prompt.template.md --spec-path <SPEC_PATH> --typecheck-cmd '<CMD>' --lint-cmd '<CMD>' --test-cmd '<CMD>'",
run_in_background: true,
description: "Implement Docker execution run 1")
```

Always pass `--force` — background execution has no TTY for interactive prompts.
Always pass `--force` — background execution has no TTY for interactive prompts. In Docker, pass `--template tmp/ship/implement-prompt.template.md` since the skill directory isn't available inside the container.

Omit `--spec-path` when no SPEC.md is available (selects Variant B). Omit quality gate args to use defaults (`pnpm typecheck`, `pnpm lint`, `pnpm test --run`).

Poll for completion using `TaskOutput(block: false)` at intervals. While waiting, do lightweight work (re-read spec, review task list) but do NOT make code changes that could conflict.

Expand Down
172 changes: 163 additions & 9 deletions plugins/eng/skills/implement/scripts/implement.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,45 @@ set -e
# Implement Iteration Loop
# Spawns independent Claude Code processes to implement user stories from spec.json.
# Each iteration is a fresh process with no memory — state persists via files and git.
#
# This script also assembles the iteration prompt from the template by injecting:
# - {{SPEC_PATH}} from --spec-path argument
# - {{TYPECHECK_CMD}} from --typecheck-cmd argument (default: pnpm typecheck)
# - {{LINT_CMD}} from --lint-cmd argument (default: pnpm lint)
# - {{TEST_CMD}} from --test-cmd argument (default: pnpm test --run)
# - {{CODEBASE_CONTEXT}} from spec.json implementationContext
# - {{DIFF}} cleaned git diff (full or stat tree depending on size)

# --- Ship directory (configurable via CLAUDE_SHIP_DIR env var) ---
SHIP_DIR="${CLAUDE_SHIP_DIR:-tmp/ship}"

# --- Defaults ---
MAX_ITERATIONS=10
MAX_TURNS=75
PROMPT_FILE="$SHIP_DIR/implement-prompt.md"
TEMPLATE_FILE=""
SPEC_FILE="$SHIP_DIR/spec.json"
PROMPT_FILE="$SHIP_DIR/implement-prompt.md"
PROGRESS_FILE="$SHIP_DIR/progress.txt"
DIFF_FILE="$SHIP_DIR/implement-diff.txt"
SPEC_PATH=""
TYPECHECK_CMD="pnpm typecheck"
LINT_CMD="pnpm lint"
TEST_CMD="pnpm test --run"
FORCE=false
PROTECTED_BRANCHES="main master"

# ~5-10K tokens ≈ 30K characters
DIFF_MAX_CHARS=30000

# --- Script paths ---
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VALIDATE_SCRIPT="$SCRIPT_DIR/validate-spec.ts"

# --- Noise filters (same as ship-stop-hook.sh) ---
FILTER_LOCK='(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|\.lock$)'
FILTER_SHIP='^.. tmp/ship/'
FILTER_BUILD='(dist|\.next|build|\.turbo|node_modules)/'

# --- Colors ---
RED='\033[0;31m'
GREEN='\033[0;32m'
Expand All @@ -35,8 +57,12 @@ usage() {
echo "Options:"
echo " --max-iterations N Max iteration loops (default: 10)"
echo " --max-turns N Max agentic turns per iteration (default: 75)"
echo " --prompt FILE Prompt file path (default: tmp/ship/implement-prompt.md)"
echo " --spec FILE Spec JSON file path (default: tmp/ship/spec.json)"
echo " --template FILE Prompt template file (auto-detected from skill if omitted)"
echo " --spec FILE Spec JSON file path (default: $SHIP_DIR/spec.json)"
echo " --spec-path PATH Path to SPEC.md (for template variant A)"
echo " --typecheck-cmd CMD Typecheck command (default: pnpm typecheck)"
echo " --lint-cmd CMD Lint command (default: pnpm lint)"
echo " --test-cmd CMD Test command (default: pnpm test --run)"
echo " --force Skip uncommitted changes prompt"
echo " --create-branch, -b Create/checkout branch from spec.json branchName"
echo " -h, --help Show this help"
Expand All @@ -49,15 +75,24 @@ while [[ $# -gt 0 ]]; do
case $1 in
--max-iterations) MAX_ITERATIONS="$2"; shift 2 ;;
--max-turns) MAX_TURNS="$2"; shift 2 ;;
--prompt) PROMPT_FILE="$2"; shift 2 ;;
--template) TEMPLATE_FILE="$2"; shift 2 ;;
--spec) SPEC_FILE="$2"; shift 2 ;;
--spec-path) SPEC_PATH="$2"; shift 2 ;;
--typecheck-cmd) TYPECHECK_CMD="$2"; shift 2 ;;
--lint-cmd) LINT_CMD="$2"; shift 2 ;;
--test-cmd) TEST_CMD="$2"; shift 2 ;;
--force) FORCE=true; shift ;;
--create-branch|-b) CREATE_BRANCH=true; shift ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done

# --- Auto-detect template if not provided ---
if [[ -z "$TEMPLATE_FILE" ]]; then
TEMPLATE_FILE="$SCRIPT_DIR/../templates/implement-prompt.template.md"
fi

echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BLUE} Implement Iteration Loop${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
Expand Down Expand Up @@ -116,14 +151,13 @@ fi
echo -e "${YELLOW}Branch:${NC} $CURRENT_BRANCH"
echo -e "${YELLOW}Max iterations:${NC} $MAX_ITERATIONS"
echo -e "${YELLOW}Max turns:${NC} $MAX_TURNS"
echo -e "${YELLOW}Prompt:${NC} $PROMPT_FILE"
echo -e "${YELLOW}Template:${NC} $TEMPLATE_FILE"
echo -e "${YELLOW}Spec JSON:${NC} $SPEC_FILE"
echo ""

# --- Check required files ---
if [[ ! -f "$PROMPT_FILE" ]]; then
echo -e "${RED}Error: Prompt file not found: $PROMPT_FILE${NC}"
echo "Run /implement to generate the implementation prompt first."
if [[ ! -f "$TEMPLATE_FILE" ]]; then
echo -e "${RED}Error: Template file not found: $TEMPLATE_FILE${NC}"
exit 1
fi

Expand Down Expand Up @@ -160,6 +194,121 @@ if [[ ! -f "$PROGRESS_FILE" ]]; then
echo "" >> "$PROGRESS_FILE"
fi

# --- Compute cleaned diff (full or stat tree based on size) ---
compute_diff() {
local MERGE_BASE
MERGE_BASE=$(git merge-base main HEAD 2>/dev/null || echo "HEAD~10")

# Full cleaned diff
local FULL_DIFF
FULL_DIFF=$(git diff "$MERGE_BASE"...HEAD \
-- ':!*.lock' ':!*lock.json' ':!*lock.yaml' \
':!tmp/' ':!dist/' ':!build/' ':!.next/' ':!.turbo/' ':!node_modules/' \
2>/dev/null || echo "")

local DIFF_SIZE=${#FULL_DIFF}

if [[ "$DIFF_SIZE" -le "$DIFF_MAX_CHARS" && "$DIFF_SIZE" -gt 0 ]]; then
# Small enough — use full diff
echo "# Git diff (full — $(echo "$FULL_DIFF" | wc -l | tr -d ' ') lines)"
echo ""
echo "$FULL_DIFF"
elif [[ "$DIFF_SIZE" -gt "$DIFF_MAX_CHARS" ]]; then
# Too large — fall back to stat tree
echo "# Git diff (stat tree — full diff too large at ~$((DIFF_SIZE / 4)) tokens)"
echo ""
echo "Full diff exceeds token budget. Showing file-level summary."
echo "Read specific files with the Read tool for details."
echo ""
git diff --stat "$MERGE_BASE"...HEAD \
-- ':!*.lock' ':!*lock.json' ':!*lock.yaml' \
':!tmp/' ':!dist/' ':!build/' ':!.next/' ':!.turbo/' ':!node_modules/' \
2>/dev/null || echo "(no stat available)"
else
echo "# Git diff"
echo ""
echo "(no changes detected against main)"
fi
}

# --- Assemble prompt from template ---
assemble_prompt() {
# Extract implementationContext from spec.json
local IMPL_CONTEXT
if command -v jq &> /dev/null; then
IMPL_CONTEXT=$(jq -r '.implementationContext // ""' "$SPEC_FILE")
else
IMPL_CONTEXT=""
fi

# Compute fresh diff
local DIFF
DIFF=$(compute_diff)

# Write diff file for reference
echo "$DIFF" > "$DIFF_FILE"

# Write content to temp files for safe Python substitution
local TEMPLATE_TMP IMPL_TMP DIFF_TMP
TEMPLATE_TMP=$(mktemp)
IMPL_TMP=$(mktemp)
DIFF_TMP=$(mktemp)
cat "$TEMPLATE_FILE" > "$TEMPLATE_TMP"
echo "$IMPL_CONTEXT" > "$IMPL_TMP"
echo "$DIFF" > "$DIFF_TMP"

# Use python3 for variant selection + multi-line placeholder substitution
python3 -c "
import sys

with open('$TEMPLATE_TMP', 'r') as f:
template = f.read()
with open('$IMPL_TMP', 'r') as f:
impl_context = f.read().strip()
with open('$DIFF_TMP', 'r') as f:
diff = f.read().strip()

spec_path = '$SPEC_PATH'

# --- Variant selection ---
# Select Variant A (with SPEC.md) or Variant B (without) based on --spec-path
if spec_path:
marker_start = '## Variant A'
marker_end = '*End of Variant A*'
else:
marker_start = '## Variant B'
marker_end = '*End of Variant B*'

try:
start_idx = template.index(marker_start)
end_idx = template.index(marker_end)
section = template[start_idx:end_idx]
# Extract prompt content: between first --- and last --- in the section
first_hr = section.index('\n---\n') + 5
content = section[first_hr:].rstrip()
last_hr = content.rfind('\n---')
if last_hr > 0:
content = content[:last_hr].strip()
except (ValueError, IndexError):
# Fallback: use full template if markers not found
content = template

# --- Placeholder substitution ---
content = content.replace('{{SPEC_PATH}}', spec_path)
content = content.replace('{{TYPECHECK_CMD}}', '$TYPECHECK_CMD')
content = content.replace('{{LINT_CMD}}', '$LINT_CMD')
content = content.replace('{{TEST_CMD}}', '$TEST_CMD')
content = content.replace('{{CODEBASE_CONTEXT}}', impl_context)
content = content.replace('{{DIFF}}', diff)

with open('$PROMPT_FILE', 'w') as f:
f.write(content)
" 2>/dev/null

# Cleanup temp files
rm -f "$TEMPLATE_TMP" "$IMPL_TMP" "$DIFF_TMP"
}

# --- Main iteration loop ---
for ((i=1; i<=MAX_ITERATIONS; i++)); do
echo ""
Expand All @@ -171,6 +320,10 @@ for ((i=1; i<=MAX_ITERATIONS; i++)); do
echo "## Iteration $i - $(date)" >> "$PROGRESS_FILE"
echo "" >> "$PROGRESS_FILE"

# Assemble prompt fresh each iteration (diff changes after commits)
echo -e "${YELLOW}Assembling prompt...${NC}"
assemble_prompt

OUTPUT_FILE=$(mktemp)

# Spawn a fresh Claude Code process for this iteration.
Expand All @@ -179,12 +332,13 @@ for ((i=1; i<=MAX_ITERATIONS; i++)); do
# --dangerously-skip-permissions: no TTY for confirmation in -p mode.
# --max-turns: prevent runaway iterations.
# --output-format json: structured output for completion detection.
# < /dev/null: prevent stdin hang in nested subprocess invocations.
env -u CLAUDECODE -u CLAUDE_CODE_ENTRYPOINT claude \
-p "$(cat "$PROMPT_FILE")" \
--dangerously-skip-permissions \
--max-turns "$MAX_TURNS" \
--output-format json \
2>&1 | tee "$OUTPUT_FILE" || true
< /dev/null 2>&1 | tee "$OUTPUT_FILE" || true

# --- Post-iteration status ---
TOTAL_STORIES=0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ Impact: Without this template, the iteration prompt must be reconstructed from m

# Implementation Prompt Template

This file contains two complete prompt variants for the iteration agents. The Phase 2 agent selects **ONE** variant, fills all `{{PLACEHOLDERS}}`, and saves the result to `tmp/ship/implement-prompt.md`.
This file contains two complete prompt variants for the iteration agents. The `implement.sh` script selects the correct variant based on `--spec-path`, fills all `{{PLACEHOLDERS}}`, and saves the result to `tmp/ship/implement-prompt.md`.

**Do NOT include both variants in the saved prompt.** The iteration agent sees a single, unconditional workflow — never both variants, never conditional "if spec is available" logic.

## Placeholders

| Placeholder | Source | Used in |
|---|---|---|
| `{{SPEC_PATH}}` | Path to SPEC.md relative to working directory (e.g., `.claude/specs/my-feature/SPEC.md`) | Variant A only |
| `{{TYPECHECK_CMD}}` | `--typecheck-cmd` input or default `pnpm typecheck` | Both |
| `{{LINT_CMD}}` | `--lint-cmd` input or default `pnpm lint` | Both |
| `{{TEST_CMD}}` | `--test-cmd` input or default `pnpm test --run` | Both |
| `{{CODEBASE_CONTEXT}}` | Key patterns, shared vocabulary, and abstractions from the target codebase area — see SKILL.md Phase 2 for guidance on what to include | Both |
| `{{SPEC_PATH}}` | Path to SPEC.md — from `--spec-path` argument, injected by implement.sh | Variant A only |
| `{{TYPECHECK_CMD}}` | `--typecheck-cmd` argument or default `pnpm typecheck` — injected by implement.sh | Both |
| `{{LINT_CMD}}` | `--lint-cmd` argument or default `pnpm lint` — injected by implement.sh | Both |
| `{{TEST_CMD}}` | `--test-cmd` argument or default `pnpm test --run` — injected by implement.sh | Both |
| `{{CODEBASE_CONTEXT}}` | From spec.json `implementationContext` — injected by implement.sh | Both |
| `{{DIFF}}` | Cleaned git diff (full if small, stat tree if large) — injected by implement.sh each iteration | Both |

---

Expand Down Expand Up @@ -69,6 +70,10 @@ Implement the story. **One story per iteration** — keep changes focused.

{{CODEBASE_CONTEXT}}

**Changes under test:**

{{DIFF}}

### 5. Verify quality

Run these commands — ALL must pass:
Expand Down Expand Up @@ -169,6 +174,10 @@ Implement the story. **One story per iteration** — keep changes focused.

{{CODEBASE_CONTEXT}}

**Changes under test:**

{{DIFF}}

### 4. Verify quality

Run these commands — ALL must pass:
Expand Down
Loading