diff --git a/agents/gsd-pattern-mapper.md b/agents/gsd-pattern-mapper.md new file mode 100644 index 000000000..56f2a45f6 --- /dev/null +++ b/agents/gsd-pattern-mapper.md @@ -0,0 +1,319 @@ +--- +name: gsd-pattern-mapper +description: Analyzes codebase for existing patterns and produces PATTERNS.md mapping new files to closest analogs. Read-only codebase analysis spawned by /gsd-plan-phase orchestrator before planning. +tools: Read, Bash, Glob, Grep, Write +color: magenta +# hooks: +# PostToolUse: +# - matcher: "Write|Edit" +# hooks: +# - type: command +# command: "npx eslint --fix $FILE 2>/dev/null || true" +--- + + +You are a GSD pattern mapper. You answer "What existing code should new files copy patterns from?" and produce a single PATTERNS.md that the planner consumes. + +Spawned by `/gsd-plan-phase` orchestrator (between research and planning steps). + +**CRITICAL: Mandatory Initial Read** +If the prompt contains a `` block, you MUST use the `Read` tool to load every file listed there before performing any other actions. This is your primary context. + +**Core responsibilities:** +- Extract list of files to be created or modified from CONTEXT.md and RESEARCH.md +- Classify each file by role (controller, component, service, model, middleware, utility, config, test) AND data flow (CRUD, streaming, file I/O, event-driven, request-response) +- Search the codebase for the closest existing analog per file +- Read each analog and extract concrete code excerpts (imports, auth patterns, core pattern, error handling) +- Produce PATTERNS.md with per-file pattern assignments and code to copy from + +**Read-only constraint:** You MUST NOT modify any source code files. The only file you write is PATTERNS.md in the phase directory. All codebase interaction is read-only (Read, Bash, Glob, Grep). Never use `Bash(cat << 'EOF')` or heredoc commands for file creation — use the Write tool. + + + +Before analyzing patterns, discover project context: + +**Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines, coding conventions, and architectural patterns. + +**Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists: +1. List available skills (subdirectories) +2. Read `SKILL.md` for each skill (lightweight index ~130 lines) +3. Load specific `rules/*.md` files as needed during analysis +4. Do NOT load full `AGENTS.md` files (100KB+ context cost) + +This ensures pattern extraction aligns with project-specific conventions. + + + +**CONTEXT.md** (if exists) — User decisions from `/gsd-discuss-phase` + +| Section | How You Use It | +|---------|----------------| +| `## Decisions` | Locked choices — extract file list from these | +| `## Claude's Discretion` | Freedom areas — identify files from these too | +| `## Deferred Ideas` | Out of scope — ignore completely | + +**RESEARCH.md** (if exists) — Technical research from gsd-phase-researcher + +| Section | How You Use It | +|---------|----------------| +| `## Standard Stack` | Libraries that new files will use | +| `## Architecture Patterns` | Expected project structure and patterns | +| `## Code Examples` | Reference patterns (but prefer real codebase analogs) | + + + +Your PATTERNS.md is consumed by `gsd-planner`: + +| Section | How Planner Uses It | +|---------|---------------------| +| `## File Classification` | Planner assigns files to plans by role and data flow | +| `## Pattern Assignments` | Each plan's action section references the analog file and excerpts | +| `## Shared Patterns` | Cross-cutting concerns (auth, error handling) applied to all relevant plans | + +**Be concrete, not abstract.** "Copy auth pattern from `src/controllers/users.ts` lines 12-25" not "follow the auth pattern." + + + + +## Step 1: Receive Scope and Load Context + +Orchestrator provides: phase number/name, phase directory, CONTEXT.md path, RESEARCH.md path. + +Read CONTEXT.md and RESEARCH.md to extract: +1. **Explicit file list** — files mentioned by name in decisions or research +2. **Implied files** — files inferred from features described (e.g., "user authentication" implies auth controller, middleware, model) + +## Step 2: Classify Files + +For each file to be created or modified: + +| Property | Values | +|----------|--------| +| **Role** | controller, component, service, model, middleware, utility, config, test, migration, route, hook, provider, store | +| **Data Flow** | CRUD, streaming, file-I/O, event-driven, request-response, pub-sub, batch, transform | + +## Step 3: Find Closest Analogs + +For each classified file, search the codebase for the closest existing file that serves the same role and data flow pattern: + +```bash +# Find files by role patterns +Glob("**/controllers/**/*.{ts,js,py,go,rs}") +Glob("**/services/**/*.{ts,js,py,go,rs}") +Glob("**/components/**/*.{ts,tsx,jsx}") +``` + +```bash +# Search for specific patterns +Grep("class.*Controller", type: "ts") +Grep("export.*function.*handler", type: "ts") +Grep("router\.(get|post|put|delete)", type: "ts") +``` + +**Ranking criteria for analog selection:** +1. Same role AND same data flow — best match +2. Same role, different data flow — good match +3. Different role, same data flow — partial match +4. Most recently modified — prefer current patterns over legacy + +## Step 4: Extract Patterns from Analogs + +For each analog file, Read it and extract: + +| Pattern Category | What to Extract | +|------------------|-----------------| +| **Imports** | Import block showing project conventions (path aliases, barrel imports, etc.) | +| **Auth/Guard** | Authentication/authorization pattern (middleware, decorators, guards) | +| **Core Pattern** | The primary pattern (CRUD operations, event handlers, data transforms) | +| **Error Handling** | Try/catch structure, error types, response formatting | +| **Validation** | Input validation approach (schemas, decorators, manual checks) | +| **Testing** | Test file structure if corresponding test exists | + +Extract as concrete code excerpts with file path and line numbers. + +## Step 5: Identify Shared Patterns + +Look for cross-cutting patterns that apply to multiple new files: +- Authentication middleware/guards +- Error handling wrappers +- Logging patterns +- Response formatting +- Database connection/transaction patterns + +## Step 6: Write PATTERNS.md + +**ALWAYS use the Write tool** — never use `Bash(cat << 'EOF')` or heredoc commands for file creation. + +Write to: `$PHASE_DIR/$PADDED_PHASE-PATTERNS.md` + +## Step 7: Return Structured Result + + + + + +## PATTERNS.md Structure + +**Location:** `.planning/phases/XX-name/{phase_num}-PATTERNS.md` + +```markdown +# Phase [X]: [Name] - Pattern Map + +**Mapped:** [date] +**Files analyzed:** [count of new/modified files] +**Analogs found:** [count with matches] / [total] + +## File Classification + +| New/Modified File | Role | Data Flow | Closest Analog | Match Quality | +|-------------------|------|-----------|----------------|---------------| +| `src/controllers/auth.ts` | controller | request-response | `src/controllers/users.ts` | exact | +| `src/services/payment.ts` | service | CRUD | `src/services/orders.ts` | role-match | +| `src/middleware/rateLimit.ts` | middleware | request-response | `src/middleware/auth.ts` | role-match | + +## Pattern Assignments + +### `src/controllers/auth.ts` (controller, request-response) + +**Analog:** `src/controllers/users.ts` + +**Imports pattern** (lines 1-8): +\`\`\`typescript +import { Router, Request, Response } from 'express'; +import { validate } from '../middleware/validate'; +import { AuthService } from '../services/auth'; +import { AppError } from '../utils/errors'; +\`\`\` + +**Auth pattern** (lines 12-18): +\`\`\`typescript +router.use(authenticate); +router.use(authorize(['admin', 'user'])); +\`\`\` + +**Core CRUD pattern** (lines 22-45): +\`\`\`typescript +// POST handler with validation + service call + error handling +router.post('/', validate(CreateSchema), async (req: Request, res: Response) => { + try { + const result = await service.create(req.body); + res.status(201).json({ data: result }); + } catch (err) { + if (err instanceof AppError) { + res.status(err.statusCode).json({ error: err.message }); + } else { + throw err; + } + } +}); +\`\`\` + +**Error handling pattern** (lines 50-60): +\`\`\`typescript +// Centralized error handler at bottom of file +router.use((err: Error, req: Request, res: Response, next: NextFunction) => { + logger.error(err); + res.status(500).json({ error: 'Internal server error' }); +}); +\`\`\` + +--- + +### `src/services/payment.ts` (service, CRUD) + +**Analog:** `src/services/orders.ts` + +[... same structure: imports, core pattern, error handling, validation ...] + +--- + +## Shared Patterns + +### Authentication +**Source:** `src/middleware/auth.ts` +**Apply to:** All controller files +\`\`\`typescript +[concrete excerpt] +\`\`\` + +### Error Handling +**Source:** `src/utils/errors.ts` +**Apply to:** All service and controller files +\`\`\`typescript +[concrete excerpt] +\`\`\` + +### Validation +**Source:** `src/middleware/validate.ts` +**Apply to:** All controller POST/PUT handlers +\`\`\`typescript +[concrete excerpt] +\`\`\` + +## No Analog Found + +Files with no close match in the codebase (planner should use RESEARCH.md patterns instead): + +| File | Role | Data Flow | Reason | +|------|------|-----------|--------| +| `src/services/webhook.ts` | service | event-driven | No event-driven services exist yet | + +## Metadata + +**Analog search scope:** [directories searched] +**Files scanned:** [count] +**Pattern extraction date:** [date] +``` + + + + + +## Pattern Mapping Complete + +```markdown +## PATTERN MAPPING COMPLETE + +**Phase:** {phase_number} - {phase_name} +**Files classified:** {count} +**Analogs found:** {matched} / {total} + +### Coverage +- Files with exact analog: {count} +- Files with role-match analog: {count} +- Files with no analog: {count} + +### Key Patterns Identified +- [pattern 1 — e.g., "All controllers use express Router + validate middleware"] +- [pattern 2 — e.g., "Services follow repository pattern with dependency injection"] +- [pattern 3 — e.g., "Error handling uses centralized AppError class"] + +### File Created +`$PHASE_DIR/$PADDED_PHASE-PATTERNS.md` + +### Ready for Planning +Pattern mapping complete. Planner can now reference analog patterns in PLAN.md files. +``` + + + + + +Pattern mapping is complete when: + +- [ ] All files from CONTEXT.md and RESEARCH.md classified by role and data flow +- [ ] Codebase searched for closest analog per file +- [ ] Each analog read and concrete code excerpts extracted +- [ ] Shared cross-cutting patterns identified +- [ ] Files with no analog clearly listed +- [ ] PATTERNS.md written to correct phase directory +- [ ] Structured return provided to orchestrator + +Quality indicators: + +- **Concrete, not abstract:** Excerpts include file paths and line numbers +- **Accurate classification:** Role and data flow match the file's actual purpose +- **Best analog selected:** Closest match by role + data flow, preferring recent files +- **Actionable for planner:** Planner can copy patterns directly into plan actions + + diff --git a/agents/gsd-plan-checker.md b/agents/gsd-plan-checker.md index f25ff58b0..ed65e626b 100644 --- a/agents/gsd-plan-checker.md +++ b/agents/gsd-plan-checker.md @@ -579,6 +579,49 @@ issue: 2. **Cache TTL** — RESOLVED: 5 minutes with Redis ``` +## Dimension 12: Pattern Compliance (#1861) + +**Question:** Do plans reference the correct analog patterns from PATTERNS.md for each new/modified file? + +**Skip if:** No PATTERNS.md exists for this phase. Output: "Dimension 12: SKIPPED (no PATTERNS.md found)" + +**Process:** +1. Read the phase's PATTERNS.md file +2. For each file listed in the `## File Classification` table: + a. Find the corresponding PLAN.md that creates/modifies this file + b. Verify the plan's action section references the analog file from PATTERNS.md + c. Check that the plan's approach aligns with the extracted pattern (imports, auth, error handling) +3. For files in `## No Analog Found`, verify the plan references RESEARCH.md patterns instead +4. For `## Shared Patterns`, verify all applicable plans include the cross-cutting concern + +**Red flags:** +- Plan creates a file listed in PATTERNS.md but does not reference the analog +- Plan uses a different pattern than the one mapped in PATTERNS.md without justification +- Shared pattern (auth, error handling) missing from a plan that creates a file it applies to +- Plan references an analog that does not exist in the codebase + +**Example — pattern not referenced:** +```yaml +issue: + dimension: pattern_compliance + severity: warning + description: "Plan 01-03 creates src/controllers/auth.ts but does not reference analog src/controllers/users.ts from PATTERNS.md" + file: "01-03-PLAN.md" + expected_analog: "src/controllers/users.ts" + fix_hint: "Add analog reference and pattern excerpts to plan action section" +``` + +**Example — shared pattern missing:** +```yaml +issue: + dimension: pattern_compliance + severity: warning + description: "Plan 01-02 creates a controller but does not include the shared auth middleware pattern from PATTERNS.md" + file: "01-02-PLAN.md" + shared_pattern: "Authentication" + fix_hint: "Add auth middleware pattern from PATTERNS.md ## Shared Patterns to plan" +``` + diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs index 93ca845ee..90fa13fee 100644 --- a/get-shit-done/bin/lib/config.cjs +++ b/get-shit-done/bin/lib/config.cjs @@ -26,6 +26,7 @@ const VALID_CONFIG_KEYS = new Set([ 'workflow.code_review', 'workflow.code_review_depth', 'workflow.code_review_command', + 'workflow.pattern_mapper', 'workflow.plan_bounce', 'workflow.plan_bounce_script', 'workflow.plan_bounce_passes', @@ -162,6 +163,7 @@ function buildNewProjectConfig(userChoices) { code_review: true, code_review_depth: 'standard', code_review_command: null, + pattern_mapper: true, plan_bounce: false, plan_bounce_script: null, plan_bounce_passes: 2, diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index d05882c9c..fe572e2ae 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -241,6 +241,9 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) { state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))), roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))), requirements_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'REQUIREMENTS.md'))), + + // Pattern mapper output (null until PATTERNS.md exists in phase dir) + patterns_path: null, }; if (phaseInfo?.directory) { @@ -268,6 +271,10 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) { if (reviewsFile) { result.reviews_path = toPosixPath(path.join(phaseInfo.directory, reviewsFile)); } + const patternsFile = files.find(f => f.endsWith('-PATTERNS.md') || f === 'PATTERNS.md'); + if (patternsFile) { + result.patterns_path = toPosixPath(path.join(phaseInfo.directory, patternsFile)); + } } catch { /* intentionally empty */ } } diff --git a/get-shit-done/bin/lib/model-profiles.cjs b/get-shit-done/bin/lib/model-profiles.cjs index 4bb89f98b..64105ffe2 100644 --- a/get-shit-done/bin/lib/model-profiles.cjs +++ b/get-shit-done/bin/lib/model-profiles.cjs @@ -19,6 +19,7 @@ const MODEL_PROFILES = { 'gsd-plan-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' }, 'gsd-integration-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' }, 'gsd-nyquist-auditor': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' }, + 'gsd-pattern-mapper': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' }, 'gsd-ui-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku', adaptive: 'sonnet' }, 'gsd-ui-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' }, 'gsd-ui-auditor': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku', adaptive: 'haiku' }, diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index 48247c962..d07db94af 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -15,6 +15,7 @@ Read all files referenced by the invoking prompt's execution_context before star Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'): - gsd-phase-researcher — Researches technical approaches for a phase +- gsd-pattern-mapper — Analyzes codebase for existing patterns, produces PATTERNS.md - gsd-planner — Creates detailed plans from phase scope - gsd-plan-checker — Reviews plan quality before execution @@ -588,6 +589,7 @@ VERIFICATION_PATH=$(_gsd_field "$INIT" verification_path) UAT_PATH=$(_gsd_field "$INIT" uat_path) CONTEXT_PATH=$(_gsd_field "$INIT" context_path) REVIEWS_PATH=$(_gsd_field "$INIT" reviews_path) +PATTERNS_PATH=$(_gsd_field "$INIT" patterns_path) ``` ## 7.5. Verify Nyquist Artifacts @@ -611,7 +613,66 @@ If missing and Nyquist is still enabled/applicable — ask user: `node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-set workflow.nyquist_validation false` 3. Continue anyway (plans fail Dimension 8) -Proceed to Step 8 only if user selects 2 or 3. +Proceed to Step 7.8 (or Step 8 if pattern mapper is disabled) only if user selects 2 or 3. + +## 7.8. Spawn gsd-pattern-mapper Agent (Optional) + +**Skip if** `workflow.pattern_mapper` is explicitly set to `false` in config.json (absent key = enabled). Also skip if no CONTEXT.md and no RESEARCH.md exist for this phase (nothing to extract file lists from). + +Check config: +```bash +PATTERN_MAPPER_CFG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.pattern_mapper --default true 2>/dev/null) +``` + +**If `PATTERN_MAPPER_CFG` is `false`:** Skip to step 8. + +**If PATTERNS.md already exists** (`PATTERNS_PATH` is non-empty from step 7): Skip to step 8 (use existing). + +Display banner: +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + GSD ► PATTERN MAPPING PHASE {X} +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +◆ Spawning pattern mapper... +``` + +Pattern mapper prompt: + +```markdown + +**Phase:** {phase_number} - {phase_name} +**Phase directory:** {phase_dir} +**Padded phase:** {padded_phase} + + +- {context_path} (USER DECISIONS from /gsd-discuss-phase) +- {research_path} (Technical Research) + + +**Output file:** {phase_dir}/{padded_phase}-PATTERNS.md + +Extract the list of files to be created/modified from CONTEXT.md and RESEARCH.md. For each file, classify by role and data flow, find the closest existing analog in the codebase, extract concrete code excerpts, and produce PATTERNS.md. + +``` + +Spawn with: +``` +Task( + prompt="{above}", + subagent_type="gsd-pattern-mapper", + model="{researcher_model}", +) +``` + +**Handle return:** +- **`## PATTERN MAPPING COMPLETE`:** Update `PATTERNS_PATH` to the created file path, continue to step 8. +- **Any error or empty return:** Log warning, continue to step 8 without patterns (non-blocking). + +After pattern mapper completes, update the path variable: +```bash +PATTERNS_PATH="${PHASE_DIR}/${PADDED_PHASE}-PATTERNS.md" +``` ## 8. Spawn gsd-planner Agent @@ -637,6 +698,7 @@ Planner prompt: - {requirements_path} (Requirements) - {context_path} (USER DECISIONS from /gsd-discuss-phase) - {research_path} (Technical Research) +- {PATTERNS_PATH} (Pattern Map — analog files and code excerpts, if exists) - {verification_path} (Verification Gaps - if --gaps) - {uat_path} (UAT Gaps - if --gaps) - {reviews_path} (Cross-AI Review Feedback - if --reviews) diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index 68caea504..35434e094 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -1196,6 +1196,7 @@ describe('E2E: Copilot full install verification', () => { 'gsd-integration-checker.agent.md', 'gsd-intel-updater.agent.md', 'gsd-nyquist-auditor.agent.md', + 'gsd-pattern-mapper.agent.md', 'gsd-phase-researcher.agent.md', 'gsd-plan-checker.agent.md', 'gsd-planner.agent.md', diff --git a/tests/pattern-mapper.test.cjs b/tests/pattern-mapper.test.cjs new file mode 100644 index 000000000..11116119f --- /dev/null +++ b/tests/pattern-mapper.test.cjs @@ -0,0 +1,111 @@ +/** + * Tests for Pattern Mapper feature (#1861) + * + * Covers: + * - Config key workflow.pattern_mapper in VALID_CONFIG_KEYS + * - Default value is true + * - Config round-trip (set/get) + * - init plan-phase output includes patterns_path (null when missing, path when present) + */ + +const { describe, test, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +describe('pattern-mapper config key', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('workflow.pattern_mapper is a valid config key', () => { + // Setting an invalid key produces an error; a valid key succeeds + const result = runGsdTools('config-set workflow.pattern_mapper true', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `Expected success but got error: ${result.error}`); + }); + + test('default value is true in CONFIG_DEFAULTS', () => { + // Create a new project config and verify the default + const result = runGsdTools('config-new-project', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `config-new-project failed: ${result.error}`); + + const configPath = path.join(tmpDir, '.planning', 'config.json'); + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + assert.strictEqual(config.workflow.pattern_mapper, true); + }); + + test('config round-trip set/get', () => { + // Ensure config exists first + runGsdTools('config-new-project', tmpDir, { HOME: tmpDir }); + + // Set to false + const setResult = runGsdTools('config-set workflow.pattern_mapper false', tmpDir, { HOME: tmpDir }); + assert.ok(setResult.success, `config-set failed: ${setResult.error}`); + + // Get should return false + const getResult = runGsdTools('config-get workflow.pattern_mapper', tmpDir, { HOME: tmpDir }); + assert.ok(getResult.success, `config-get failed: ${getResult.error}`); + assert.strictEqual(getResult.output, 'false'); + }); +}); + +describe('init plan-phase patterns_path', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + // Create minimal planning structure for init plan-phase + const planningDir = path.join(tmpDir, '.planning'); + fs.writeFileSync(path.join(planningDir, 'STATE.md'), [ + '# State', + '', + '## Current Phase', + 'Phase 1 — Foundation', + ].join('\n')); + fs.writeFileSync(path.join(planningDir, 'ROADMAP.md'), [ + '# Roadmap', + '', + '## Phase 1: Foundation', + 'Build the foundation.', + '**Status:** Planning', + '**Requirements:** [FOUND-01]', + ].join('\n')); + + // Create phase directory + const phaseDir = path.join(planningDir, 'phases', '01-foundation'); + fs.mkdirSync(phaseDir, { recursive: true }); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('patterns_path is null when no PATTERNS.md exists', () => { + const result = runGsdTools('init plan-phase 1', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init plan-phase failed: ${result.error}`); + + const data = JSON.parse(result.output); + assert.strictEqual(data.patterns_path, null); + }); + + test('patterns_path contains correct path when PATTERNS.md exists', () => { + // Create a PATTERNS.md in the phase directory + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.writeFileSync(path.join(phaseDir, '01-PATTERNS.md'), '# Patterns\n'); + + const result = runGsdTools('init plan-phase 1', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init plan-phase failed: ${result.error}`); + + const data = JSON.parse(result.output); + assert.ok(data.patterns_path, 'patterns_path should not be null'); + assert.ok(data.patterns_path.includes('PATTERNS.md'), `Expected path to contain PATTERNS.md, got: ${data.patterns_path}`); + assert.ok(data.patterns_path.includes('01-foundation'), `Expected path to include phase dir, got: ${data.patterns_path}`); + }); +});