Files

237 lines
8.2 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
PHASE_NUM="${1:?Usage: phase-parallel.sh <phase-number>}"
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_ROOT"
PHASE_BRIEF="spec/phase/phase-$(printf '%02d' "$PHASE_NUM").md"
source scripts/lib/parse-phase-brief.sh "$PHASE_BRIEF"
if [[ "$TARGET_BACKEND" != "true" || "$TARGET_FRONTEND" != "true" ]]; then
echo "Phase $PHASE_NUM does not require both backend and frontend."
echo " Backend: $TARGET_BACKEND | Frontend: $TARGET_FRONTEND"
echo "Use scripts/phase-execute.sh instead."
exit 1
fi
MAIN_BRANCH=$(git branch --show-current)
# Record current HEAD before any merges — used for precise squash
PRE_MERGE_HEAD=$(git rev-parse HEAD)
BE_WT="phase${PHASE_NUM}-be"
FE_WT="phase${PHASE_NUM}-fe"
BE_BRANCH="worktree-${BE_WT}"
FE_BRANCH="worktree-${FE_WT}"
BE_DIR=".claude/worktrees/${BE_WT}"
FE_DIR=".claude/worktrees/${FE_WT}"
SHARED_STATE_PATHS=(
"PHASE_MANIFEST.txt"
"docs/reports/data/summary.json"
"docs/reports/data/rtm.json"
"docs/reports/data/tests.json"
"docs/reports/data/phases"
"docs/reports/data/history"
)
AUTO_STASH_REF=""
AUTO_STASH_CREATED=false
workspace_has_changes() {
[[ -n "$(git status --porcelain --untracked-files=all)" ]]
}
autostash_workspace_if_needed() {
if ! workspace_has_changes; then
return 0
fi
local stamp msg
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
msg="phase-parallel-${PHASE_NUM}-autostash-${stamp}"
echo "Local workspace changes detected; creating temporary stash (tracked + untracked)."
git stash push --include-untracked -m "$msg" >/dev/null
AUTO_STASH_REF="$(git stash list --format='%gd %gs' | awk -v m="$msg" '$0 ~ m {print $1; exit}')"
if [[ -n "$AUTO_STASH_REF" ]]; then
AUTO_STASH_CREATED=true
echo " Auto-stashed as $AUTO_STASH_REF"
fi
}
restore_autostash() {
if [[ "$AUTO_STASH_CREATED" != "true" || -z "$AUTO_STASH_REF" ]]; then
return 0
fi
echo "Restoring auto-stashed workspace changes ($AUTO_STASH_REF)..."
if git stash pop --index "$AUTO_STASH_REF" >/dev/null 2>&1; then
echo " Auto-stash restored."
AUTO_STASH_CREATED=false
AUTO_STASH_REF=""
else
echo "WARN: Auto-stash could not be restored cleanly."
echo " It is kept in stash list; restore manually when needed."
fi
}
prune_shared_state_changes() {
local worker="${1:-WT}"
if ! git diff --quiet -- "${SHARED_STATE_PATHS[@]}" 2>/dev/null \
|| [[ -n "$(git ls-files --others --exclude-standard -- "${SHARED_STATE_PATHS[@]}")" ]]; then
echo "[$worker] Dropping shared-state side effects (manifest/reports)."
fi
# Parallel branches must not carry manifest/report artifacts back to main.
git restore --source=HEAD --staged --worktree -- "${SHARED_STATE_PATHS[@]}" >/dev/null 2>&1 || true
git clean -fd -- "${SHARED_STATE_PATHS[@]}" >/dev/null 2>&1 || true
}
# ── Cleanup on exit ───────────────────────────────────────
cleanup() {
echo ""
echo "Cleaning up worktrees..."
git worktree remove "$BE_DIR" --force 2>/dev/null || true
git worktree remove "$FE_DIR" --force 2>/dev/null || true
git branch -D "$BE_BRANCH" 2>/dev/null || true
git branch -D "$FE_BRANCH" 2>/dev/null || true
restore_autostash
}
trap cleanup EXIT
# ── Step 0: Preflight ─────────────────────────────────────
echo "═══ Preflight ═══"
bash scripts/preflight/preflight-check.sh
autostash_workspace_if_needed
echo ""
# ── Step 1: Create isolated worktrees ─────────────────────
echo "═══ Creating worktrees ═══"
mkdir -p .claude/worktrees
git branch "$BE_BRANCH" HEAD
git branch "$FE_BRANCH" HEAD
git worktree add "$BE_DIR" "$BE_BRANCH"
git worktree add "$FE_DIR" "$FE_BRANCH"
echo " Backend worktree: $BE_DIR ($BE_BRANCH)"
echo " Frontend worktree: $FE_DIR ($FE_BRANCH)"
echo ""
# ── Step 2: Parallel generation ───────────────────────────
echo "═══ Parallel Generation — Phase $PHASE_NUM ═══"
(
cd "$BE_DIR"
echo "[BE] Starting backend generation..."
claude -p "You are backend-builder. Execute Phase $PHASE_NUM backend tasks.
Read CLAUDE.md, spec/01-constraints.md, spec/08-output-rules.md, and $PHASE_BRIEF.
Generate ONLY backend files (.java, pom.xml, .xml, .yml, .sql, .properties).
Do NOT generate any frontend files (.vue, .ts, .json for node, .css, .html).
Do NOT run gate scripts, and do NOT modify PHASE_MANIFEST.txt or docs/reports/data/*.
Follow spec/08-output-rules.md for output format." \
--agent backend-builder \
--output-format text
prune_shared_state_changes "BE"
git add -A
git commit -m "wip: phase-${PHASE_NUM} backend" --allow-empty
echo "[BE] Done."
) &
PID_BE=$!
(
cd "$FE_DIR"
echo "[FE] Starting frontend generation..."
claude -p "You are frontend-builder. Execute Phase $PHASE_NUM frontend tasks.
Read CLAUDE.md, spec/06-frontend-config.md, spec/08-output-rules.md, and $PHASE_BRIEF.
Generate ONLY frontend files (.vue, .ts, package.json, .scss, .html, tsconfig*.json).
Do NOT generate any backend files (.java, pom.xml, .sql, .yml backend configs).
Do NOT run gate scripts, and do NOT modify PHASE_MANIFEST.txt or docs/reports/data/*.
Follow spec/08-output-rules.md for output format." \
--agent frontend-builder \
--output-format text
prune_shared_state_changes "FE"
git add -A
git commit -m "wip: phase-${PHASE_NUM} frontend" --allow-empty
echo "[FE] Done."
) &
PID_FE=$!
echo "Backend PID: $PID_BE | Frontend PID: $PID_FE"
echo "Waiting for both agents to complete..."
BE_EXIT=0
FE_EXIT=0
wait $PID_BE || BE_EXIT=$?
wait $PID_FE || FE_EXIT=$?
if [[ "$BE_EXIT" -ne 0 ]]; then
echo "ERROR: Backend generation failed (exit $BE_EXIT)"
exit 1
fi
if [[ "$FE_EXIT" -ne 0 ]]; then
echo "ERROR: Frontend generation failed (exit $FE_EXIT)"
exit 1
fi
echo "Both agents completed successfully."
echo ""
# ── Step 3: Merge both branches back ─────────────────────
echo "═══ Merging worktrees → $MAIN_BRANCH ═══"
git merge --no-ff "$BE_BRANCH" -m "merge: phase-${PHASE_NUM} backend"
git merge --no-ff "$FE_BRANCH" -m "merge: phase-${PHASE_NUM} frontend"
echo ""
# ── Step 4: Gate check on merged result ───────────────────
echo "═══ Gate Check (merged) ═══"
bash scripts/gates/gate-all.sh
GATE_EXIT=$?
if [[ "$GATE_EXIT" -ne 0 ]]; then
echo "GATE FAILED — aborting."
exit 1
fi
echo ""
# ── Step 5: Diff review on merged result ──────────────────
echo "═══ Diff Review (merged) ═══"
bash scripts/review/diff-review.sh "$PHASE_NUM"
DREVIEW_EXIT=$?
if [[ "$DREVIEW_EXIT" -ne 0 ]]; then
echo "DIFF REVIEW FAILED — aborting."
exit 1
fi
echo ""
# ── Step 6: Semantic review via Agent ─────────────────────
echo "═══ Semantic Review ═══"
CHANGED_FILES=$(git diff --name-only HEAD~2)
REVIEW_RESULT=$(claude -p "You are code-reviewer. Review ONLY these changed files:
$CHANGED_FILES
Phase $PHASE_NUM scope: ${PHASE_SCOPE[*]}
Read CLAUDE.md and spec/01-constraints.md.
Check all hard rules against each changed file.
Flag any file outside scope as CRITICAL (scope-violation).
Output structured findings. End with REVIEW PASSED or REVIEW FAILED: N critical, M warnings." \
--agent code-reviewer \
--output-format text)
echo "$REVIEW_RESULT"
echo ""
if echo "$REVIEW_RESULT" | grep -q "REVIEW FAILED"; then
echo "SEMANTIC REVIEW FAILED — aborting."
exit 1
fi
# ── Step 7: Squash all merge commits into one ─────────────
# Use PRE_MERGE_HEAD (recorded before merges) for precise squash
# This avoids the fragile HEAD~N counting
echo "═══ Final Commit ═══"
git reset --soft "$PRE_MERGE_HEAD"
git commit -m "feat(phase-${PHASE_NUM}): ${PHASE_TITLE} — parallel verified"
echo ""
echo "════════════════════════════════════════"
echo "Phase $PHASE_NUM COMPLETE (parallel): $PHASE_TITLE"
echo "════════════════════════════════════════"