From 85d4989e8ee5068d2b251e9cbc3615e5bcd28d45 Mon Sep 17 00:00:00 2001 From: ashanuoc Date: Tue, 3 Mar 2026 17:44:41 -0500 Subject: [PATCH] feat: add /gsd:review cross-AI peer review command Add a new command that invokes external AI CLIs (Gemini, Claude, Codex) to independently review phase plans and produces a REVIEWS.md document that can be fed back into planning via --reviews flag. Co-Authored-By: Claude Opus 4.6 --- commands/gsd/plan-phase.md | 3 +- commands/gsd/review.md | 36 ++++ get-shit-done/bin/gsd-tools.cjs | 31 ++- get-shit-done/bin/lib/commands.cjs | 182 +++++++++++++++++ get-shit-done/bin/lib/core.cjs | 2 + get-shit-done/bin/lib/init.cjs | 78 ++++++++ get-shit-done/workflows/plan-phase.md | 5 +- get-shit-done/workflows/review.md | 159 +++++++++++++++ tests/review.test.cjs | 276 ++++++++++++++++++++++++++ 9 files changed, 768 insertions(+), 4 deletions(-) create mode 100644 commands/gsd/review.md create mode 100644 get-shit-done/workflows/review.md create mode 100644 tests/review.test.cjs diff --git a/commands/gsd/plan-phase.md b/commands/gsd/plan-phase.md index bb377a897a..7365699255 100644 --- a/commands/gsd/plan-phase.md +++ b/commands/gsd/plan-phase.md @@ -1,7 +1,7 @@ --- name: gsd:plan-phase description: Create detailed phase plan (PLAN.md) with verification loop -argument-hint: "[phase] [--auto] [--research] [--skip-research] [--gaps] [--skip-verify] [--prd ]" +argument-hint: "[phase] [--auto] [--research] [--skip-research] [--gaps] [--skip-verify] [--prd ] [--reviews]" agent: gsd-planner allowed-tools: - Read @@ -35,6 +35,7 @@ Phase number: $ARGUMENTS (optional — auto-detects next unplanned phase if omit - `--gaps` — Gap closure mode (reads VERIFICATION.md, skips research) - `--skip-verify` — Skip verification loop - `--prd ` — Use a PRD/acceptance criteria file instead of discuss-phase. Parses requirements into CONTEXT.md automatically. Skips discuss-phase entirely. +- `--reviews` — Incorporate cross-AI review feedback from REVIEWS.md into planning (run /gsd:review first) Normalize phase input in step 2 before any directory lookups. diff --git a/commands/gsd/review.md b/commands/gsd/review.md new file mode 100644 index 0000000000..19b0e40074 --- /dev/null +++ b/commands/gsd/review.md @@ -0,0 +1,36 @@ +--- +name: gsd:review +description: Request cross-AI peer review of phase plans from external AI CLIs +argument-hint: "--phase N [--gemini] [--claude] [--codex] [--all]" +agent: gsd-planner +allowed-tools: + - Read + - Write + - Bash + - Glob + - Grep +--- + +Invoke external AI CLIs (Gemini, Claude, Codex) to independently review phase plans. Produces a REVIEWS.md document with structured feedback from each reviewer that can be fed back into planning. + +**Flow:** Init → Check CLIs → Build Prompt → Invoke CLIs → Write REVIEWS.md → Commit → Present Results + + + +@~/.claude/get-shit-done/workflows/review.md +@~/.claude/get-shit-done/references/ui-brand.md + + + +Phase number: extracted from $ARGUMENTS (required) + +**Flags:** +- `--gemini` — Include Gemini CLI review +- `--claude` — Include Claude CLI review +- `--codex` — Include Codex CLI review +- `--all` — Include all available CLIs + + + +Execute the review workflow from @~/.claude/get-shit-done/workflows/review.md end-to-end. + diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 48cb9cf563..844438f138 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -488,6 +488,32 @@ async function main() { break; } + case 'review': { + const subcommand = args[1]; + if (subcommand === 'check-cli') { + commands.cmdReviewCheckCli(cwd, raw); + } else if (subcommand === 'build-prompt') { + const phaseIdx = args.indexOf('--phase'); + commands.cmdReviewBuildPrompt(cwd, phaseIdx !== -1 ? args[phaseIdx + 1] : null, raw); + } else if (subcommand === 'write-reviews') { + const phaseIdx = args.indexOf('--phase'); + const reviews = {}; + const geminiIdx = args.indexOf('--gemini-file'); + const claudeIdx = args.indexOf('--claude-file'); + const codexIdx = args.indexOf('--codex-file'); + if (geminiIdx !== -1) reviews.gemini = args[geminiIdx + 1]; + if (claudeIdx !== -1) reviews.claude = args[claudeIdx + 1]; + if (codexIdx !== -1) reviews.codex = args[codexIdx + 1]; + commands.cmdReviewWriteReviews(cwd, { + phase: phaseIdx !== -1 ? args[phaseIdx + 1] : null, + reviews, + }, raw); + } else { + error('Unknown review subcommand. Available: check-cli, build-prompt, write-reviews'); + } + break; + } + case 'todo': { const subcommand = args[1]; if (subcommand === 'complete') { @@ -549,8 +575,11 @@ async function main() { case 'progress': init.cmdInitProgress(cwd, raw); break; + case 'review': + init.cmdInitReview(cwd, args[2], raw); + break; default: - error(`Unknown init workflow: ${workflow}\nAvailable: execute-phase, plan-phase, new-project, new-milestone, quick, resume, verify-work, phase-op, todos, milestone-op, map-codebase, progress`); + error(`Unknown init workflow: ${workflow}\nAvailable: execute-phase, plan-phase, new-project, new-milestone, quick, resume, verify-work, phase-op, todos, milestone-op, map-codebase, progress, review`); } break; } diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index e42034946f..e0461af76f 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -532,6 +532,185 @@ function cmdScaffold(cwd, type, options, raw) { output({ created: true, path: relPath }, raw, relPath); } +// ─── Review commands ────────────────────────────────────────────────────────── + +function cmdReviewCheckCli(cwd, raw) { + const { execFileSync } = require('child_process'); + const result = {}; + for (const cli of ['gemini', 'claude', 'codex']) { + try { + execFileSync('which', [cli], { stdio: 'pipe' }); + result[cli] = true; + } catch { + result[cli] = false; + } + } + output(result, raw); +} + +function cmdReviewBuildPrompt(cwd, phase, raw) { + if (!phase) { + error('phase required for review build-prompt'); + } + + const phaseInfo = findPhaseInternal(cwd, phase); + if (!phaseInfo) { + error(`Phase ${phase} not found`); + } + + const padded = normalizePhaseName(phase); + const phaseDirFull = path.join(cwd, phaseInfo.directory); + + // Gather PROJECT.md (first 50 lines) + const projectContent = safeReadFile(path.join(cwd, '.planning', 'PROJECT.md')); + const projectSnippet = projectContent ? projectContent.split('\n').slice(0, 50).join('\n') : ''; + + // Gather ROADMAP phase section + const { getRoadmapPhaseInternal } = require('./core.cjs'); + const roadmapPhase = getRoadmapPhaseInternal(cwd, phase); + const roadmapSection = roadmapPhase?.section || ''; + + // Gather REQUIREMENTS.md + const requirementsContent = safeReadFile(path.join(cwd, '.planning', 'REQUIREMENTS.md')) || ''; + + // Gather all PLANs + const planFiles = phaseInfo.plans || []; + const planContents = planFiles.map(f => { + const content = safeReadFile(path.join(phaseDirFull, f)); + return content ? `## ${f}\n\n${content}` : ''; + }).filter(Boolean); + + // Optional: CONTEXT.md + let contextContent = ''; + try { + const files = fs.readdirSync(phaseDirFull); + const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'); + if (contextFile) { + contextContent = safeReadFile(path.join(phaseDirFull, contextFile)) || ''; + } + } catch {} + + // Optional: RESEARCH.md + let researchContent = ''; + try { + const files = fs.readdirSync(phaseDirFull); + const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'); + if (researchFile) { + researchContent = safeReadFile(path.join(phaseDirFull, researchFile)) || ''; + } + } catch {} + + // Build prompt + const prompt = `# Cross-AI Review Request + +## Project Overview (first 50 lines of PROJECT.md) + +${projectSnippet} + +## Phase ${phaseInfo.phase_number}: ${phaseInfo.phase_name || 'Unnamed'} + +### Roadmap Section + +${roadmapSection} + +### Requirements + +${requirementsContent} + +${contextContent ? `### Context\n\n${contextContent}\n` : ''} +${researchContent ? `### Research\n\n${researchContent}\n` : ''} +### Plans (${planContents.length} total) + +${planContents.join('\n\n---\n\n')} + +## Review Instructions + +Please review the above phase plans and provide: + +1. **Summary** — Brief overview of what the plans cover +2. **Strengths** — What's well-designed or thorough +3. **Concerns** — Potential issues, gaps, or risks +4. **Suggestions** — Concrete improvements or alternatives +5. **Risk Assessment** — Overall risk level (low/medium/high) with justification + +Focus on: +- Completeness: Do the plans cover all requirements? +- Feasibility: Are the tasks achievable as described? +- Dependencies: Are inter-plan dependencies correctly identified? +- Testing: Is the verification strategy adequate? +- Architecture: Are there design concerns or anti-patterns? +`; + + // Write to temp file + const os = require('os'); + const promptPath = path.join(os.tmpdir(), `gsd-review-prompt-${padded}-${Date.now()}.md`); + fs.writeFileSync(promptPath, prompt, 'utf-8'); + + output({ + prompt_path: promptPath, + plan_count: planContents.length, + phase_number: phaseInfo.phase_number, + phase_name: phaseInfo.phase_name, + }, raw); +} + +function cmdReviewWriteReviews(cwd, options, raw) { + const { phase, reviews } = options; + if (!phase) { + error('phase required for review write-reviews'); + } + + const phaseInfo = findPhaseInternal(cwd, phase); + if (!phaseInfo) { + error(`Phase ${phase} not found`); + } + + const padded = normalizePhaseName(phase); + const phaseDirFull = path.join(cwd, phaseInfo.directory); + const today = new Date().toISOString().split('T')[0]; + + // reviews is an object like { gemini: '/tmp/gemini-review.md', claude: '/tmp/claude-review.md' } + const reviewerNames = Object.keys(reviews || {}); + if (reviewerNames.length === 0) { + error('No review files provided'); + } + + // Build REVIEWS.md content + let content = `--- +phase: "${padded}" +name: "${phaseInfo.phase_name || 'Unnamed'}" +created: ${today} +reviewers: [${reviewerNames.join(', ')}] +status: complete +--- + +# Phase ${phaseInfo.phase_number}: ${phaseInfo.phase_name || 'Unnamed'} -- Cross-AI Reviews + +`; + + for (const reviewer of reviewerNames) { + const reviewFile = reviews[reviewer]; + const reviewContent = safeReadFile(reviewFile); + if (!reviewContent) { + content += `## ${reviewer.charAt(0).toUpperCase() + reviewer.slice(1)} Review\n\n_Review not available (file not found: ${reviewFile})_\n\n`; + continue; + } + content += `## ${reviewer.charAt(0).toUpperCase() + reviewer.slice(1)} Review\n\n${reviewContent.trim()}\n\n`; + } + + // Write REVIEWS.md + const reviewsPath = path.join(phaseDirFull, `${padded}-REVIEWS.md`); + fs.writeFileSync(reviewsPath, content, 'utf-8'); + + const relPath = toPosixPath(path.relative(cwd, reviewsPath)); + output({ + created: true, + path: relPath, + reviewers: reviewerNames, + phase_number: phaseInfo.phase_number, + }, raw); +} + module.exports = { cmdGenerateSlug, cmdCurrentTimestamp, @@ -545,4 +724,7 @@ module.exports = { cmdProgressRender, cmdTodoComplete, cmdScaffold, + cmdReviewCheckCli, + cmdReviewBuildPrompt, + cmdReviewWriteReviews, }; diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 6db9f95406..d9414572cf 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -229,6 +229,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) { const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'); const hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'); const hasVerification = phaseFiles.some(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'); + const hasReviews = phaseFiles.some(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md'); const completedPlanIds = new Set( summaries.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '')) @@ -250,6 +251,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) { has_research: hasResearch, has_context: hasContext, has_verification: hasVerification, + has_reviews: hasReviews, }; } catch { return null; diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 7e551a01fb..06536263aa 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -119,6 +119,7 @@ function cmdInitPlanPhase(cwd, phase, raw) { // Existing artifacts has_research: phaseInfo?.has_research || false, has_context: phaseInfo?.has_context || false, + has_reviews: phaseInfo?.has_reviews || false, has_plans: (phaseInfo?.plans?.length || 0) > 0, plan_count: phaseInfo?.plans?.length || 0, @@ -153,6 +154,10 @@ function cmdInitPlanPhase(cwd, phase, raw) { if (uatFile) { result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile)); } + const reviewsFile = files.find(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md'); + if (reviewsFile) { + result.reviews_path = toPosixPath(path.join(phaseInfo.directory, reviewsFile)); + } } catch {} } @@ -694,6 +699,78 @@ function cmdInitProgress(cwd, raw) { output(result, raw); } +function cmdInitReview(cwd, phase, raw) { + if (!phase) { + error('phase required for init review'); + } + + const phaseInfo = findPhaseInternal(cwd, phase); + + // Check CLI availability using execFileSync (safe, no shell) + const { execFileSync } = require('child_process'); + const cliAvailable = {}; + for (const cli of ['gemini', 'claude', 'codex']) { + try { + execFileSync('which', [cli], { stdio: 'pipe' }); + cliAvailable[cli] = true; + } catch { + cliAvailable[cli] = false; + } + } + + const result = { + // Phase info + phase_found: !!phaseInfo, + phase_dir: phaseInfo?.directory || null, + phase_number: phaseInfo?.phase_number || null, + phase_name: phaseInfo?.phase_name || null, + padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null, + + // Plan inventory + plans: phaseInfo?.plans || [], + plan_count: phaseInfo?.plans?.length || 0, + + // Existing artifacts + has_research: phaseInfo?.has_research || false, + has_context: phaseInfo?.has_context || false, + has_reviews: phaseInfo?.has_reviews || false, + + // CLI availability + cli_available: cliAvailable, + + // File existence + roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'), + requirements_exists: pathExistsInternal(cwd, '.planning/REQUIREMENTS.md'), + project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), + + // File paths + roadmap_path: '.planning/ROADMAP.md', + requirements_path: '.planning/REQUIREMENTS.md', + project_path: '.planning/PROJECT.md', + }; + + if (phaseInfo?.directory) { + const phaseDirFull = path.join(cwd, phaseInfo.directory); + try { + const files = fs.readdirSync(phaseDirFull); + const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'); + if (contextFile) { + result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile)); + } + const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'); + if (researchFile) { + result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile)); + } + const reviewsFile = files.find(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md'); + if (reviewsFile) { + result.reviews_path = toPosixPath(path.join(phaseInfo.directory, reviewsFile)); + } + } catch {} + } + + output(result, raw); +} + module.exports = { cmdInitExecutePhase, cmdInitPlanPhase, @@ -707,4 +784,5 @@ module.exports = { cmdInitMilestoneOp, cmdInitMapCodebase, cmdInitProgress, + cmdInitReview, }; diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index 9f4ccc5769..8d8713c757 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -21,13 +21,13 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_enabled`, `plan_checker_enabled`, `nyquist_validation_enabled`, `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_plans`, `plan_count`, `planning_exists`, `roadmap_exists`, `phase_req_ids`. -**File paths (for blocks):** `state_path`, `roadmap_path`, `requirements_path`, `context_path`, `research_path`, `verification_path`, `uat_path`. These are null if files don't exist. +**File paths (for blocks):** `state_path`, `roadmap_path`, `requirements_path`, `context_path`, `research_path`, `verification_path`, `uat_path`, `reviews_path`. These are null if files don't exist. **If `planning_exists` is false:** Error — run `/gsd:new-project` first. ## 2. Parse and Normalize Arguments -Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--gaps`, `--skip-verify`, `--prd `). +Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--gaps`, `--skip-verify`, `--prd `, `--reviews`). Extract `--prd ` from $ARGUMENTS. If present, set PRD_FILE to the filepath. @@ -304,6 +304,7 @@ Planner prompt: - {research_path} (Technical Research) - {verification_path} (Verification Gaps - if --gaps) - {uat_path} (UAT Gaps - if --gaps) +- {reviews_path} (Cross-AI Reviews - if --reviews flag AND reviews_path exists. Incorporate feedback: address concerns, consider suggestions, strengthen areas flagged as risks.) **Phase requirement IDs (every ID MUST appear in a plan's `requirements` field):** {phase_req_ids} diff --git a/get-shit-done/workflows/review.md b/get-shit-done/workflows/review.md new file mode 100644 index 0000000000..018a3e4dc7 --- /dev/null +++ b/get-shit-done/workflows/review.md @@ -0,0 +1,159 @@ + +Orchestrate cross-AI peer review of phase plans by invoking external AI CLIs (Gemini, Claude, Codex) and assembling their feedback into a REVIEWS.md document. + + + +@~/.claude/get-shit-done/references/ui-brand.md + + + + +## 1. Initialize + +```bash +INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init review "$PHASE") +if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi +``` + +Parse JSON for: `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`, `plans`, `plan_count`, `has_reviews`, `cli_available`, `roadmap_exists`, `requirements_exists`, `project_exists`. + +**If `phase_found` is false:** Error — phase not found. +**If `plan_count` is 0:** Error — no plans to review. Run `/gsd:plan-phase` first. + +## 2. Parse Arguments + +Extract from $ARGUMENTS: phase number, flags (`--gemini`, `--claude`, `--codex`, `--all`). + +**If `--all`:** Enable all available CLIs. +**If no CLI flags:** Default to `--all`. + +## 3. Check CLI Availability + +```bash +CLI_CHECK=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" review check-cli) +``` + +Cross-reference requested CLIs with availability. Warn about unavailable CLIs. + +**If no requested CLIs are available:** Error — install at least one AI CLI. + +Display banner: +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + GSD ► CROSS-AI REVIEW — PHASE {X} +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Reviewers: {list of available requested CLIs} +Plans: {plan_count} +``` + +## 4. Build Review Prompt + +```bash +PROMPT_INFO=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" review build-prompt --phase "$PHASE") +PROMPT_FILE=$(echo "$PROMPT_INFO" | jq -r '.prompt_path') +``` + +## 5. Invoke CLIs (Sequential) + +For each enabled and available CLI, invoke sequentially: + +### Gemini +```bash +GEMINI_OUT=$(mktemp /tmp/gsd-review-gemini-XXXXXX.md) +cat "$PROMPT_FILE" | gemini -p > "$GEMINI_OUT" 2>/dev/null +GEMINI_EXIT=$? +``` + +### Claude +```bash +CLAUDE_OUT=$(mktemp /tmp/gsd-review-claude-XXXXXX.md) +cat "$PROMPT_FILE" | claude -p --model sonnet > "$CLAUDE_OUT" 2>/dev/null +CLAUDE_EXIT=$? +``` + +### Codex +```bash +CODEX_OUT=$(mktemp /tmp/gsd-review-codex-XXXXXX.md) +cat "$PROMPT_FILE" | codex -p > "$CODEX_OUT" 2>/dev/null +CODEX_EXIT=$? +``` + +Display progress for each: `◆ {CLI} review... {done|failed}` + +**Per-CLI failure handling:** If a CLI fails (non-zero exit), skip it and continue with remaining CLIs. Warn user about failures. + +## 6. Write REVIEWS.md + +Build the write-reviews command with all successful review files: + +```bash +WRITE_ARGS="review write-reviews --phase $PHASE" +if [ $GEMINI_EXIT -eq 0 ]; then WRITE_ARGS="$WRITE_ARGS --gemini-file $GEMINI_OUT"; fi +if [ $CLAUDE_EXIT -eq 0 ]; then WRITE_ARGS="$WRITE_ARGS --claude-file $CLAUDE_OUT"; fi +if [ $CODEX_EXIT -eq 0 ]; then WRITE_ARGS="$WRITE_ARGS --codex-file $CODEX_OUT"; fi + +RESULT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" $WRITE_ARGS) +``` + +## 7. Commit + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-${PHASE}): add cross-AI reviews" --files "${phase_dir}/${padded_phase}-REVIEWS.md" +``` + +## 8. Present Results + +Display banner: +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + GSD ► REVIEWS COMPLETE — PHASE {X} +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Reviewers: {list} +Output: {reviews_path} +``` + +## 9. Cleanup + +Remove temporary files (prompt file, individual review files). + + + + +Output this markdown directly: + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + GSD ► PHASE {X} REVIEWED ✓ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +**Phase {X}: {Name}** — Reviews from {N} AI(s) + +| Reviewer | Status | +|----------|--------| +| Gemini | ✓/✗ | +| Claude | ✓/✗ | +| Codex | ✓/✗ | + +─────────────────────────────────────────────────────────────── + +## ▶ Next Up + +**Incorporate feedback into plans:** + +/gsd:plan-phase {X} --reviews + +Reviews are saved in {reviews_path} + +─────────────────────────────────────────────────────────────── + + + +- [ ] Phase validated and plans found +- [ ] CLI availability checked +- [ ] Prompt built from project/phase context +- [ ] At least one CLI invoked successfully +- [ ] REVIEWS.md written with per-reviewer sections +- [ ] Temporary files cleaned up +- [ ] User sees status and next steps + diff --git a/tests/review.test.cjs b/tests/review.test.cjs new file mode 100644 index 0000000000..d772a040f9 --- /dev/null +++ b/tests/review.test.cjs @@ -0,0 +1,276 @@ +/** + * GSD Tools Tests - Review (Cross-AI Peer Review) + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +// ───────────────────────────────────────────────────────────────────────────── +// init review +// ───────────────────────────────────────────────────────────────────────────── + +describe('init review', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('returns phase info and cli availability', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan 1'); + fs.writeFileSync(path.join(phaseDir, '03-02-PLAN.md'), '# Plan 2'); + + const result = runGsdTools('init review 03', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_found, true); + assert.strictEqual(output.phase_number, '03'); + assert.strictEqual(output.plan_count, 2); + assert.ok('cli_available' in output, 'Should have cli_available'); + assert.ok('gemini' in output.cli_available, 'Should check gemini'); + assert.ok('claude' in output.cli_available, 'Should check claude'); + assert.ok('codex' in output.cli_available, 'Should check codex'); + }); + + test('returns has_reviews when REVIEWS.md exists', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '03-REVIEWS.md'), '# Reviews'); + + const result = runGsdTools('init review 03', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.has_reviews, true); + assert.ok(output.reviews_path.includes('03-REVIEWS.md')); + }); + + test('phase not found returns phase_found false', () => { + const result = runGsdTools('init review 99', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.phase_found, false); + assert.strictEqual(output.phase_dir, null); + }); + + test('errors without phase argument', () => { + const result = runGsdTools('init review', tmpDir); + assert.strictEqual(result.success, false); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// review check-cli +// ───────────────────────────────────────────────────────────────────────────── + +describe('review check-cli', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('returns boolean values for each CLI', () => { + const result = runGsdTools('review check-cli', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(typeof output.gemini, 'boolean'); + assert.strictEqual(typeof output.claude, 'boolean'); + assert.strictEqual(typeof output.codex, 'boolean'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// review build-prompt +// ───────────────────────────────────────────────────────────────────────────── + +describe('review build-prompt', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('builds prompt file with plans', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan 1\n\nBuild the API endpoints.'); + fs.writeFileSync(path.join(phaseDir, '03-02-PLAN.md'), '# Plan 2\n\nAdd authentication.'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Test Project\n\nA test project.'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap\n\n### Phase 3: API\n**Goal:** Build API\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), '# Requirements\n\n- [ ] REQ-01: Build API'); + + const result = runGsdTools('review build-prompt --phase 03', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.plan_count, 2); + assert.ok(output.prompt_path, 'Should have prompt_path'); + assert.ok(fs.existsSync(output.prompt_path), 'Prompt file should exist'); + + // Verify prompt content includes plans + const promptContent = fs.readFileSync(output.prompt_path, 'utf-8'); + assert.ok(promptContent.includes('Build the API endpoints'), 'Prompt should include plan content'); + assert.ok(promptContent.includes('Add authentication'), 'Prompt should include second plan'); + assert.ok(promptContent.includes('Cross-AI Review Request'), 'Prompt should have review header'); + + // Cleanup temp file + fs.unlinkSync(output.prompt_path); + }); + + test('errors without phase', () => { + const result = runGsdTools('review build-prompt', tmpDir); + assert.strictEqual(result.success, false); + }); + + test('errors when phase not found', () => { + const result = runGsdTools('review build-prompt --phase 99', tmpDir); + assert.strictEqual(result.success, false); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// review write-reviews +// ───────────────────────────────────────────────────────────────────────────── + +describe('review write-reviews', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('writes REVIEWS.md from reviewer files', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + + // Create temp review files + const os = require('os'); + const geminiFile = path.join(os.tmpdir(), `gsd-test-gemini-${Date.now()}.md`); + const claudeFile = path.join(os.tmpdir(), `gsd-test-claude-${Date.now()}.md`); + fs.writeFileSync(geminiFile, '### Summary\n\nGood plans overall.\n\n### Concerns\n\nNone major.'); + fs.writeFileSync(claudeFile, '### Summary\n\nWell structured.\n\n### Suggestions\n\nAdd more tests.'); + + const result = runGsdTools( + ['review', 'write-reviews', '--phase', '03', '--gemini-file', geminiFile, '--claude-file', claudeFile], + tmpDir + ); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.created, true); + assert.ok(output.path.includes('03-REVIEWS.md')); + assert.deepStrictEqual(output.reviewers, ['gemini', 'claude']); + + // Verify file content + const reviewsContent = fs.readFileSync(path.join(phaseDir, '03-REVIEWS.md'), 'utf-8'); + assert.ok(reviewsContent.includes('Gemini Review'), 'Should have Gemini section'); + assert.ok(reviewsContent.includes('Claude Review'), 'Should have Claude section'); + assert.ok(reviewsContent.includes('Good plans overall'), 'Should include gemini review content'); + assert.ok(reviewsContent.includes('Add more tests'), 'Should include claude review content'); + assert.ok(reviewsContent.includes('reviewers: [gemini, claude]'), 'Should have reviewers in frontmatter'); + + // Cleanup + fs.unlinkSync(geminiFile); + fs.unlinkSync(claudeFile); + }); + + test('handles missing review file gracefully', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + + const result = runGsdTools( + ['review', 'write-reviews', '--phase', '03', '--gemini-file', '/tmp/nonexistent-file.md'], + tmpDir + ); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.created, true); + + const reviewsContent = fs.readFileSync(path.join(phaseDir, '03-REVIEWS.md'), 'utf-8'); + assert.ok(reviewsContent.includes('Review not available'), 'Should note missing review'); + }); + + test('errors without phase', () => { + const result = runGsdTools('review write-reviews', tmpDir); + assert.strictEqual(result.success, false); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// has_reviews in init plan-phase +// ───────────────────────────────────────────────────────────────────────────── + +describe('plan-phase --reviews integration', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('init plan-phase includes has_reviews when REVIEWS.md exists', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '03-REVIEWS.md'), '# Reviews\n\nSome reviews.'); + + const result = runGsdTools('init plan-phase 03', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.has_reviews, true); + assert.ok(output.reviews_path.includes('03-REVIEWS.md'), 'Should have reviews_path'); + }); + + test('init plan-phase has_reviews false when no REVIEWS.md', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + + const result = runGsdTools('init plan-phase 03', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.has_reviews, false); + assert.strictEqual(output.reviews_path, undefined); + }); + + test('has_reviews detected via init review', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '05-deploy'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '05-REVIEWS.md'), '# Reviews'); + + const result = runGsdTools('init review 05', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.has_reviews, true); + }); +});