diff --git a/get-shit-done/bin/gsd-tools.js b/get-shit-done/bin/gsd-tools.js index f850b20d8..fd339f1dd 100755 --- a/get-shit-done/bin/gsd-tools.js +++ b/get-shit-done/bin/gsd-tools.js @@ -53,6 +53,22 @@ const MODEL_PROFILES = { // ─── Helpers ────────────────────────────────────────────────────────────────── +function parseIncludeFlag(args) { + const includeIndex = args.indexOf('--include'); + if (includeIndex === -1) return new Set(); + const includeValue = args[includeIndex + 1]; + if (!includeValue) return new Set(); + return new Set(includeValue.split(',').map(s => s.trim())); +} + +function safeReadFile(filePath) { + try { + return fs.readFileSync(filePath, 'utf-8'); + } catch { + return null; + } +} + function loadConfig(cwd) { const configPath = path.join(cwd, '.planning', 'config.json'); const defaults = { @@ -1392,7 +1408,7 @@ function getMilestoneInfo(cwd) { } } -function cmdInitExecutePhase(cwd, phase, raw) { +function cmdInitExecutePhase(cwd, phase, includes, raw) { if (!phase) { error('phase required for init execute-phase'); } @@ -1450,10 +1466,21 @@ function cmdInitExecutePhase(cwd, phase, raw) { config_exists: pathExistsInternal(cwd, '.planning/config.json'), }; + // Include file contents if requested via --include + if (includes.has('state')) { + result.state_content = safeReadFile(path.join(cwd, '.planning', 'STATE.md')); + } + if (includes.has('config')) { + result.config_content = safeReadFile(path.join(cwd, '.planning', 'config.json')); + } + if (includes.has('roadmap')) { + result.roadmap_content = safeReadFile(path.join(cwd, '.planning', 'ROADMAP.md')); + } + output(result, raw); } -function cmdInitPlanPhase(cwd, phase, raw) { +function cmdInitPlanPhase(cwd, phase, includes, raw) { if (!phase) { error('phase required for init plan-phase'); } @@ -1491,6 +1518,61 @@ function cmdInitPlanPhase(cwd, phase, raw) { roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'), }; + // Include file contents if requested via --include + if (includes.has('state')) { + result.state_content = safeReadFile(path.join(cwd, '.planning', 'STATE.md')); + } + if (includes.has('roadmap')) { + result.roadmap_content = safeReadFile(path.join(cwd, '.planning', 'ROADMAP.md')); + } + if (includes.has('requirements')) { + result.requirements_content = safeReadFile(path.join(cwd, '.planning', 'REQUIREMENTS.md')); + } + if (includes.has('context') && phaseInfo?.directory) { + // Find *-CONTEXT.md in phase 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_content = safeReadFile(path.join(phaseDirFull, contextFile)); + } + } catch {} + } + if (includes.has('research') && phaseInfo?.directory) { + // Find *-RESEARCH.md in phase directory + const phaseDirFull = path.join(cwd, phaseInfo.directory); + try { + const files = fs.readdirSync(phaseDirFull); + const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'); + if (researchFile) { + result.research_content = safeReadFile(path.join(phaseDirFull, researchFile)); + } + } catch {} + } + if (includes.has('verification') && phaseInfo?.directory) { + // Find *-VERIFICATION.md in phase directory + const phaseDirFull = path.join(cwd, phaseInfo.directory); + try { + const files = fs.readdirSync(phaseDirFull); + const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'); + if (verificationFile) { + result.verification_content = safeReadFile(path.join(phaseDirFull, verificationFile)); + } + } catch {} + } + if (includes.has('uat') && phaseInfo?.directory) { + // Find *-UAT.md in phase directory + const phaseDirFull = path.join(cwd, phaseInfo.directory); + try { + const files = fs.readdirSync(phaseDirFull); + const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md'); + if (uatFile) { + result.uat_content = safeReadFile(path.join(phaseDirFull, uatFile)); + } + } catch {} + } + output(result, raw); } @@ -1857,7 +1939,7 @@ function cmdInitMapCodebase(cwd, raw) { output(result, raw); } -function cmdInitProgress(cwd, raw) { +function cmdInitProgress(cwd, includes, raw) { const config = loadConfig(cwd); const milestone = getMilestoneInfo(cwd); @@ -1947,6 +2029,20 @@ function cmdInitProgress(cwd, raw) { state_exists: pathExistsInternal(cwd, '.planning/STATE.md'), }; + // Include file contents if requested via --include + if (includes.has('state')) { + result.state_content = safeReadFile(path.join(cwd, '.planning', 'STATE.md')); + } + if (includes.has('roadmap')) { + result.roadmap_content = safeReadFile(path.join(cwd, '.planning', 'ROADMAP.md')); + } + if (includes.has('project')) { + result.project_content = safeReadFile(path.join(cwd, '.planning', 'PROJECT.md')); + } + if (includes.has('config')) { + result.config_content = safeReadFile(path.join(cwd, '.planning', 'config.json')); + } + output(result, raw); } @@ -2091,12 +2187,13 @@ function main() { case 'init': { const workflow = args[1]; + const includes = parseIncludeFlag(args); switch (workflow) { case 'execute-phase': - cmdInitExecutePhase(cwd, args[2], raw); + cmdInitExecutePhase(cwd, args[2], includes, raw); break; case 'plan-phase': - cmdInitPlanPhase(cwd, args[2], raw); + cmdInitPlanPhase(cwd, args[2], includes, raw); break; case 'new-project': cmdInitNewProject(cwd, raw); @@ -2126,7 +2223,7 @@ function main() { cmdInitMapCodebase(cwd, raw); break; case 'progress': - cmdInitProgress(cwd, raw); + cmdInitProgress(cwd, includes, 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`); diff --git a/get-shit-done/bin/gsd-tools.test.js b/get-shit-done/bin/gsd-tools.test.js index cddb80fea..5810aa741 100644 --- a/get-shit-done/bin/gsd-tools.test.js +++ b/get-shit-done/bin/gsd-tools.test.js @@ -1052,3 +1052,142 @@ key-decisions: assert.strictEqual(output.decisions[1].rationale, 'Stateless auth for scalability', 'second decision rationale'); }); }); + +// ───────────────────────────────────────────────────────────────────────────── +// init --include flag tests +// ───────────────────────────────────────────────────────────────────────────── + +describe('init commands with --include flag', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('init execute-phase includes state and config content', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan'); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + '# State\n\n**Current Phase:** 03\n**Status:** In progress' + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ model_profile: 'balanced' }) + ); + + const result = runGsdTools('init execute-phase 03 --include state,config', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.state_content, 'state_content should be included'); + assert.ok(output.state_content.includes('Current Phase'), 'state content correct'); + assert.ok(output.config_content, 'config_content should be included'); + assert.ok(output.config_content.includes('model_profile'), 'config content correct'); + }); + + test('init execute-phase without --include omits content', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State'); + + const result = runGsdTools('init execute-phase 03', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.state_content, undefined, 'state_content should be omitted'); + assert.strictEqual(output.config_content, undefined, 'config_content should be omitted'); + }); + + test('init plan-phase includes multiple file contents', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# Project State'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap v1.0'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), '# Requirements'); + fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Phase Context'); + fs.writeFileSync(path.join(phaseDir, '03-RESEARCH.md'), '# Research Findings'); + + const result = runGsdTools('init plan-phase 03 --include state,roadmap,requirements,context,research', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.state_content, 'state_content included'); + assert.ok(output.state_content.includes('Project State'), 'state content correct'); + assert.ok(output.roadmap_content, 'roadmap_content included'); + assert.ok(output.roadmap_content.includes('Roadmap v1.0'), 'roadmap content correct'); + assert.ok(output.requirements_content, 'requirements_content included'); + assert.ok(output.context_content, 'context_content included'); + assert.ok(output.research_content, 'research_content included'); + }); + + test('init plan-phase includes verification and uat content', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '03-VERIFICATION.md'), '# Verification Results'); + fs.writeFileSync(path.join(phaseDir, '03-UAT.md'), '# UAT Findings'); + + const result = runGsdTools('init plan-phase 03 --include verification,uat', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.verification_content, 'verification_content included'); + assert.ok(output.verification_content.includes('Verification Results'), 'verification content correct'); + assert.ok(output.uat_content, 'uat_content included'); + assert.ok(output.uat_content.includes('UAT Findings'), 'uat content correct'); + }); + + test('init progress includes state, roadmap, project, config', () => { + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project'); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ model_profile: 'quality' }) + ); + + const result = runGsdTools('init progress --include state,roadmap,project,config', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.state_content, 'state_content included'); + assert.ok(output.roadmap_content, 'roadmap_content included'); + assert.ok(output.project_content, 'project_content included'); + assert.ok(output.config_content, 'config_content included'); + }); + + test('missing files return null in content fields', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan'); + + const result = runGsdTools('init execute-phase 03 --include state,config', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.state_content, null, 'missing state returns null'); + assert.strictEqual(output.config_content, null, 'missing config returns null'); + }); + + test('partial includes work correctly', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap'); + + // Only request state, not roadmap + const result = runGsdTools('init execute-phase 03 --include state', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.state_content, 'state_content included'); + assert.strictEqual(output.roadmap_content, undefined, 'roadmap_content not requested, should be undefined'); + }); +}); diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index d20ad404d..0a44315ac 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -13,14 +13,16 @@ Read STATE.md before any operation to load project context. -Load all context in one call: +Load all context in one call (include file contents to avoid redundant reads): ```bash -INIT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js init execute-phase "${PHASE_ARG}") +INIT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js init execute-phase "${PHASE_ARG}" --include state,config) ``` Parse JSON for: `executor_model`, `verifier_model`, `commit_docs`, `parallelization`, `branching_strategy`, `branch_name`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `plans`, `incomplete_plans`, `plan_count`, `incomplete_count`, `state_exists`, `roadmap_exists`. +**File contents (from --include):** `state_content`, `config_content`. These are null if files don't exist. + **If `phase_found` is false:** Error — phase directory not found. **If `plan_count` is 0:** Error — no plans found in phase. **If `state_exists` is false but `.planning/` exists:** Offer reconstruct or continue. @@ -97,11 +99,13 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true` 2. **Read files and spawn agents:** Content must be inlined — `@` syntax doesn't work across Task() boundaries. + STATE and CONFIG are already loaded via `--include` in initialize step. ```bash PLAN_CONTENT=$(cat "{plan_path}") - STATE_CONTENT=$(cat .planning/STATE.md) - CONFIG_CONTENT=$(cat .planning/config.json 2>/dev/null) + # Use state_content and config_content from INIT (no need to re-read) + STATE_CONTENT=$(echo "$INIT" | jq -r '.state_content // empty') + CONFIG_CONTENT=$(echo "$INIT" | jq -r '.config_content // empty') ``` Each agent prompt: diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index 9f8e051ae..bdaf124b7 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -12,17 +12,18 @@ Read config.json for planning behavior settings. -Load execution context (uses `init execute-phase` for full context): +Load execution context (uses `init execute-phase` for full context, including file contents): ```bash -INIT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js init execute-phase "${PHASE}") +INIT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js init execute-phase "${PHASE}" --include state,config) ``` Extract from init JSON: `executor_model`, `commit_docs`, `phase_dir`, `phase_number`, `plans`, `summaries`, `incomplete_plans`. -Also read STATE.md for current position, decisions, blockers: +**File contents (from --include):** `state_content`, `config_content`. Access with: ```bash -cat .planning/STATE.md 2>/dev/null +STATE_CONTENT=$(echo "$INIT" | jq -r '.state_content // empty') +CONFIG_CONTENT=$(echo "$INIT" | jq -r '.config_content // empty') ``` If `.planning/` missing: error. @@ -30,7 +31,7 @@ If `.planning/` missing: error. ```bash -cat .planning/ROADMAP.md +# Use plans/summaries from INIT JSON, or list files ls .planning/phases/XX-name/*-PLAN.md 2>/dev/null | sort ls .planning/phases/XX-name/*-SUMMARY.md 2>/dev/null | sort ``` @@ -39,7 +40,7 @@ Find first PLAN without matching SUMMARY. Decimal phases supported (`01.1-hotfix ```bash PHASE=$(echo "$PLAN_PATH" | grep -oE '[0-9]+(\.[0-9]+)?-[0-9]+') -cat .planning/config.json 2>/dev/null +# config_content already loaded via --include config in init_context ``` diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index 1b1d47c2a..7aefc43ab 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -12,14 +12,16 @@ Read all files referenced by the invoking prompt's execution_context before star ## 1. Initialize -Load all context in one call: +Load all context in one call (include file contents to avoid redundant reads): ```bash -INIT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js init plan-phase "$PHASE") +INIT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js init plan-phase "$PHASE" --include state,roadmap,requirements,context,research,verification,uat) ``` Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_enabled`, `plan_checker_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`. +**File contents (from --include):** `state_content`, `roadmap_content`, `requirements_content`, `context_content`, `research_content`, `verification_content`, `uat_content`. These are null if files don't exist. + **If `planning_exists` is false:** Error — run `/gsd:new-project` first. ## 2. Parse and Normalize Arguments @@ -45,13 +47,11 @@ PHASE_INFO=$(node ~/.claude/get-shit-done/bin/gsd-tools.js roadmap get-phase "${ ## 4. Load CONTEXT.md -```bash -CONTEXT_CONTENT=$(cat "${PHASE_DIR}"/*-CONTEXT.md 2>/dev/null) -``` +Use `context_content` from init JSON (already loaded via `--include context`). -**CRITICAL:** Store `CONTEXT_CONTENT` now — pass to researcher, planner, checker, and revision agents. +**CRITICAL:** Use `context_content` from INIT — pass to researcher, planner, checker, and revision agents. -If CONTEXT.md exists, display: `Using phase context from: ${PHASE_DIR}/*-CONTEXT.md` +If `context_content` is not null, display: `Using phase context from: ${PHASE_DIR}/*-CONTEXT.md` ## 5. Handle Research @@ -74,7 +74,8 @@ Display banner: ```bash PHASE_DESC=$(node ~/.claude/get-shit-done/bin/gsd-tools.js roadmap get-phase "${PHASE}" | jq -r '.section') -REQUIREMENTS=$(cat .planning/REQUIREMENTS.md 2>/dev/null | grep -A100 "## Requirements" | head -50) +# Use requirements_content from INIT (already loaded via --include requirements) +REQUIREMENTS=$(echo "$INIT" | jq -r '.requirements_content // empty' | grep -A100 "## Requirements" | head -50) STATE_SNAP=$(node ~/.claude/get-shit-done/bin/gsd-tools.js state-snapshot) # Extract decisions from state-snapshot JSON: jq '.decisions[] | "\(.phase): \(.summary) - \(.rationale)"' ``` @@ -129,17 +130,19 @@ ls "${PHASE_DIR}"/*-PLAN.md 2>/dev/null **If exists:** Offer: 1) Add more plans, 2) View existing, 3) Replan from scratch. -## 7. Read Context Files +## 7. Use Context Files from INIT -Read and store for planner agent (`@` syntax doesn't work across Task() boundaries): +All file contents are already loaded via `--include` in step 1 (`@` syntax doesn't work across Task() boundaries): ```bash -STATE_CONTENT=$(cat .planning/STATE.md) -ROADMAP_CONTENT=$(cat .planning/ROADMAP.md) -REQUIREMENTS_CONTENT=$(cat .planning/REQUIREMENTS.md 2>/dev/null) -RESEARCH_CONTENT=$(cat "${PHASE_DIR}"/*-RESEARCH.md 2>/dev/null) -VERIFICATION_CONTENT=$(cat "${PHASE_DIR}"/*-VERIFICATION.md 2>/dev/null) -UAT_CONTENT=$(cat "${PHASE_DIR}"/*-UAT.md 2>/dev/null) +# Extract from INIT JSON (no need to re-read files) +STATE_CONTENT=$(echo "$INIT" | jq -r '.state_content // empty') +ROADMAP_CONTENT=$(echo "$INIT" | jq -r '.roadmap_content // empty') +REQUIREMENTS_CONTENT=$(echo "$INIT" | jq -r '.requirements_content // empty') +RESEARCH_CONTENT=$(echo "$INIT" | jq -r '.research_content // empty') +VERIFICATION_CONTENT=$(echo "$INIT" | jq -r '.verification_content // empty') +UAT_CONTENT=$(echo "$INIT" | jq -r '.uat_content // empty') +CONTEXT_CONTENT=$(echo "$INIT" | jq -r '.context_content // empty') ``` ## 8. Spawn gsd-planner Agent diff --git a/get-shit-done/workflows/progress.md b/get-shit-done/workflows/progress.md index 36ae01159..e6d74ceb0 100644 --- a/get-shit-done/workflows/progress.md +++ b/get-shit-done/workflows/progress.md @@ -9,14 +9,16 @@ Read all files referenced by the invoking prompt's execution_context before star -**Load progress context:** +**Load progress context (with file contents to avoid redundant reads):** ```bash -INIT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js init progress) +INIT=$(node ~/.claude/get-shit-done/bin/gsd-tools.js init progress --include state,roadmap,project,config) ``` Extract from init JSON: `project_exists`, `roadmap_exists`, `state_exists`, `phases`, `current_phase`, `next_phase`, `milestone_version`, `completed_count`, `phase_count`, `paused_at`. +**File contents (from --include):** `state_content`, `roadmap_content`, `project_content`, `config_content`. These are null if files don't exist. + If `project_exists` is false (no `.planning/` directory): ``` @@ -37,13 +39,16 @@ If missing both ROADMAP.md and PROJECT.md: suggest `/gsd:new-project`. -**Load full project context:** +**Use project context from INIT:** -- Read `.planning/STATE.md` for living memory (position, decisions, issues) -- Read `.planning/ROADMAP.md` for phase structure and objectives -- Read `.planning/PROJECT.md` for current state (What This Is, Core Value, Requirements) -- Read `.planning/config.json` for settings (model_profile, workflow toggles) - +All file contents are already loaded via `--include` in init_context step: +- `state_content` — living memory (position, decisions, issues) +- `roadmap_content` — phase structure and objectives +- `project_content` — current state (What This Is, Core Value, Requirements) +- `config_content` — settings (model_profile, workflow toggles) + +No additional file reads needed. + **Gather recent work context:**