From 55877d372f915f9490a1c0bcf97e8dab9d51edbc Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 15 Apr 2026 16:43:02 -0400 Subject: [PATCH] feat(handoffs): include project identity in all Next Up blocks (#1948) (#2287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(handoffs): include project identity in all Next Up blocks Adds project_code and project_title to withProjectRoot() and updates all 30 Next Up headings across 18 workflow files to include [PROJECT_CODE] PROJECT_TITLE suffix for multi-project clarity. Closes #1948 Co-Authored-By: Claude Opus 4.6 (1M context) * fix(review): add withProjectRoot tests and fix placeholder syntax (#1951) Address code review feedback: - Add 4 tests for project_code/project_title injection in withProjectRoot() - Fix inconsistent placeholder syntax in continuation-format.md canonical template (bare-brace → shell-dollar to match variant examples) Co-Authored-By: Claude Opus 4.6 (1M context) * fix(phase): preserve zero-padded prefix in renameDecimalPhases Captures the zero-padded prefix (e.g. "06" from "06.3-slug") with (0*${baseInt}) so renamed directories keep their original format (06.2-slug) instead of stripping padding (6.2-slug). Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Brandon Higgins Co-authored-by: Claude Opus 4.6 (1M context) --- get-shit-done/bin/lib/init.cjs | 16 ++++ get-shit-done/bin/lib/phase.cjs | 8 +- .../references/continuation-format.md | 18 +++-- get-shit-done/workflows/add-phase.md | 2 +- get-shit-done/workflows/add-tests.md | 2 +- get-shit-done/workflows/audit-milestone.md | 4 +- get-shit-done/workflows/complete-milestone.md | 2 +- .../workflows/discuss-phase-assumptions.md | 2 +- get-shit-done/workflows/discuss-phase.md | 2 +- get-shit-done/workflows/execute-phase.md | 2 +- get-shit-done/workflows/map-codebase.md | 2 +- get-shit-done/workflows/new-milestone.md | 2 +- get-shit-done/workflows/new-project.md | 4 +- .../workflows/plan-milestone-gaps.md | 2 +- get-shit-done/workflows/plan-phase.md | 2 +- get-shit-done/workflows/progress.md | 16 ++-- get-shit-done/workflows/resume-project.md | 4 +- get-shit-done/workflows/transition.md | 6 +- get-shit-done/workflows/ui-phase.md | 2 +- get-shit-done/workflows/undo.md | 2 +- get-shit-done/workflows/verify-work.md | 2 +- tests/init.test.cjs | 81 +++++++++++++++++++ 22 files changed, 143 insertions(+), 40 deletions(-) diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 10b8633d2..04766e671 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -44,6 +44,22 @@ function withProjectRoot(cwd, result) { if (config.response_language) { result.response_language = config.response_language; } + // Inject project identity into all init outputs so handoff blocks + // can include project context for cross-session continuity. + if (config.project_code) { + result.project_code = config.project_code; + } + // Extract project title from PROJECT.md first H1 heading. + const projectMdPath = path.join(planningDir(cwd), 'PROJECT.md'); + try { + if (fs.existsSync(projectMdPath)) { + const content = fs.readFileSync(projectMdPath, 'utf8'); + const h1Match = content.match(/^#\s+(.+)$/m); + if (h1Match) { + result.project_title = h1Match[1].trim(); + } + } + } catch { /* intentionally empty */ } return result; } diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index 7e8d58cee..805c475b2 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -585,10 +585,12 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) { */ function renameDecimalPhases(phasesDir, baseInt, removedDecimal) { const renamedDirs = [], renamedFiles = []; - const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`); + // Capture the zero-padded prefix (e.g. "06" from "06.3-slug") so the renamed + // directory preserves the original padding format. + const decPattern = new RegExp(`^(0*${baseInt})\\.(\\d+)-(.+)$`); const dirs = readSubdirectories(phasesDir, true); const toRename = dirs - .map(dir => { const m = dir.match(decPattern); return m ? { dir, oldDecimal: parseInt(m[1], 10), slug: m[2] } : null; }) + .map(dir => { const m = dir.match(decPattern); return m ? { dir, prefix: m[1], oldDecimal: parseInt(m[2], 10), slug: m[3] } : null; }) .filter(item => item && item.oldDecimal > removedDecimal) .sort((a, b) => b.oldDecimal - a.oldDecimal); // descending to avoid conflicts @@ -596,7 +598,7 @@ function renameDecimalPhases(phasesDir, baseInt, removedDecimal) { const newDecimal = item.oldDecimal - 1; const oldPhaseId = `${baseInt}.${item.oldDecimal}`; const newPhaseId = `${baseInt}.${newDecimal}`; - const newDirName = `${baseInt}.${newDecimal}-${item.slug}`; + const newDirName = `${item.prefix}.${newDecimal}-${item.slug}`; fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName)); renamedDirs.push({ from: item.dir, to: newDirName }); for (const f of fs.readdirSync(path.join(phasesDir, newDirName))) { diff --git a/get-shit-done/references/continuation-format.md b/get-shit-done/references/continuation-format.md index bed4e7146..e810859b5 100644 --- a/get-shit-done/references/continuation-format.md +++ b/get-shit-done/references/continuation-format.md @@ -7,7 +7,7 @@ Standard format for presenting next steps after completing a command or workflow ``` --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **{identifier}: {name}** — {one-line description} @@ -24,6 +24,9 @@ Standard format for presenting next steps after completing a command or workflow --- ``` +> If `project_code` is not set in the init context, omit the project identity suffix: +> `## ▶ Next Up` (no ` — [CODE] Title`). + ## Format Rules 1. **Always show what it is** — name + description, never just a command path @@ -32,6 +35,7 @@ Standard format for presenting next steps after completing a command or workflow 4. **`/clear` first** — always show `/clear` before the command so users run it in the correct order 5. **"Also available" not "Other options"** — sounds more app-like 6. **Visual separators** — `---` above and below to make it stand out +7. **Project identity in heading** — include `[PROJECT_CODE] PROJECT_TITLE` from init context so handoffs are self-identifying across sessions. If `project_code` is not set, omit the suffix entirely (just `## ▶ Next Up`) ## Variants @@ -40,7 +44,7 @@ Standard format for presenting next steps after completing a command or workflow ``` --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **02-03: Refresh Token Rotation** — Add /api/auth/refresh with sliding expiry @@ -64,7 +68,7 @@ Add note that this is the last plan and what comes after: ``` --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **02-03: Refresh Token Rotation** — Add /api/auth/refresh with sliding expiry Final plan in Phase 2 @@ -87,7 +91,7 @@ Add note that this is the last plan and what comes after: ``` --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase 2: Authentication** — JWT login flow with refresh tokens @@ -116,7 +120,7 @@ Show completion status before next action: 3/3 plans executed -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase 3: Core Features** — User dashboard, settings, and data export @@ -141,7 +145,7 @@ When there's no clear primary action: ``` --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase 3: Core Features** — User dashboard, settings, and data export @@ -165,7 +169,7 @@ When there's no clear primary action: All 4 phases shipped -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Start v1.1** — questioning → research → requirements → roadmap diff --git a/get-shit-done/workflows/add-phase.md b/get-shit-done/workflows/add-phase.md index e438c24a5..e8fc06eba 100644 --- a/get-shit-done/workflows/add-phase.md +++ b/get-shit-done/workflows/add-phase.md @@ -83,7 +83,7 @@ Roadmap updated: .planning/ROADMAP.md --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase {N}: {description}** diff --git a/get-shit-done/workflows/add-tests.md b/get-shit-done/workflows/add-tests.md index 527450237..ce87b3c9f 100644 --- a/get-shit-done/workflows/add-tests.md +++ b/get-shit-done/workflows/add-tests.md @@ -315,7 +315,7 @@ Present next steps: ``` --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} {if bugs discovered:} **Fix discovered bugs:** `/gsd-quick fix the {N} test failures discovered in phase ${phase_number}` diff --git a/get-shit-done/workflows/audit-milestone.md b/get-shit-done/workflows/audit-milestone.md index c56f34307..282836fe1 100644 --- a/get-shit-done/workflows/audit-milestone.md +++ b/get-shit-done/workflows/audit-milestone.md @@ -225,7 +225,7 @@ All requirements covered. Cross-phase integration verified. E2E flows complete. ─────────────────────────────────────────────────────────────── -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Complete milestone** — archive and tag @@ -270,7 +270,7 @@ Phases needing validation: run `/gsd-validate-phase {N}` for each flagged phase. ─────────────────────────────────────────────────────────────── -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Plan gap closure** — create phases to complete milestone diff --git a/get-shit-done/workflows/complete-milestone.md b/get-shit-done/workflows/complete-milestone.md index c287671e5..6f1ea3fc7 100644 --- a/get-shit-done/workflows/complete-milestone.md +++ b/get-shit-done/workflows/complete-milestone.md @@ -780,7 +780,7 @@ Tag: v[X.Y] --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Start Next Milestone** — questioning → research → requirements → roadmap diff --git a/get-shit-done/workflows/discuss-phase-assumptions.md b/get-shit-done/workflows/discuss-phase-assumptions.md index 985b3a7be..265eba4f9 100644 --- a/get-shit-done/workflows/discuss-phase-assumptions.md +++ b/get-shit-done/workflows/discuss-phase-assumptions.md @@ -593,7 +593,7 @@ Created: .planning/phases/${PADDED_PHASE}-${SLUG}/${PADDED_PHASE}-CONTEXT.md --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase ${PHASE}: {phase_name}** — {Goal from ROADMAP.md} diff --git a/get-shit-done/workflows/discuss-phase.md b/get-shit-done/workflows/discuss-phase.md index 7016be949..7ab323120 100644 --- a/get-shit-done/workflows/discuss-phase.md +++ b/get-shit-done/workflows/discuss-phase.md @@ -1038,7 +1038,7 @@ Created: .planning/phases/${PADDED_PHASE}-${SLUG}/${PADDED_PHASE}-CONTEXT.md --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase ${PHASE}: [Name]** — [Goal from ROADMAP.md] diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 9b077a5da..4308ea78e 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -1335,7 +1335,7 @@ Items saved to `{phase_num}-HUMAN-UAT.md` — they will appear in `/gsd-progress {Gap summaries from VERIFICATION.md} --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} `/clear` then: diff --git a/get-shit-done/workflows/map-codebase.md b/get-shit-done/workflows/map-codebase.md index c7b1e7a2f..75b770542 100644 --- a/get-shit-done/workflows/map-codebase.md +++ b/get-shit-done/workflows/map-codebase.md @@ -345,7 +345,7 @@ Created .planning/codebase/: --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Initialize project** — use codebase context for planning diff --git a/get-shit-done/workflows/new-milestone.md b/get-shit-done/workflows/new-milestone.md index ad1e90e1e..d82260bd9 100644 --- a/get-shit-done/workflows/new-milestone.md +++ b/get-shit-done/workflows/new-milestone.md @@ -514,7 +514,7 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create milest **[N] phases** | **[X] requirements** | Ready to build ✓ -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase [N]: [Phase Name]** — [Goal] diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index be37611f6..906eb3817 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -1192,7 +1192,7 @@ PHASE1_HAS_UI=$(echo "$PHASE1_SECTION" | grep -qi "UI hint.*yes" && echo "true" ``` ─────────────────────────────────────────────────────────────── -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase 1: [Phase Name]** — [Goal from ROADMAP.md] @@ -1214,7 +1214,7 @@ PHASE1_HAS_UI=$(echo "$PHASE1_SECTION" | grep -qi "UI hint.*yes" && echo "true" ``` ─────────────────────────────────────────────────────────────── -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase 1: [Phase Name]** — [Goal from ROADMAP.md] diff --git a/get-shit-done/workflows/plan-milestone-gaps.md b/get-shit-done/workflows/plan-milestone-gaps.md index 4e127fb07..2c6eb4b03 100644 --- a/get-shit-done/workflows/plan-milestone-gaps.md +++ b/get-shit-done/workflows/plan-milestone-gaps.md @@ -159,7 +159,7 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(roadmap): add --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Plan first gap closure phase** diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index ff4d54cd0..9a04c71f7 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -1228,7 +1228,7 @@ Verification: {Passed | Passed with override | Skipped} ─────────────────────────────────────────────────────────────── -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Execute Phase {X}** — run all {N} plans diff --git a/get-shit-done/workflows/progress.md b/get-shit-done/workflows/progress.md index b3ebbc4c6..1d6e13c1b 100644 --- a/get-shit-done/workflows/progress.md +++ b/get-shit-done/workflows/progress.md @@ -211,7 +211,7 @@ Read its `` section. ``` --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **{phase}-{plan}: [Plan Name]** — [objective summary from PLAN.md] @@ -240,7 +240,7 @@ PHASE_HAS_UI=$(echo "$PHASE_SECTION" | grep -qi "UI hint.*yes" && echo "true" || ``` --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase {N}: {Name}** — {Goal from ROADMAP.md} ✓ Context gathered, ready to plan @@ -257,7 +257,7 @@ PHASE_HAS_UI=$(echo "$PHASE_SECTION" | grep -qi "UI hint.*yes" && echo "true" || ``` --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase {N}: {Name}** — {Goal from ROADMAP.md} @@ -280,7 +280,7 @@ PHASE_HAS_UI=$(echo "$PHASE_SECTION" | grep -qi "UI hint.*yes" && echo "true" || ``` --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase {N}: {Name}** — {Goal from ROADMAP.md} @@ -388,7 +388,7 @@ NEXT_HAS_UI=$(echo "$NEXT_PHASE_SECTION" | grep -qi "UI hint.*yes" && echo "true ## ✓ Phase {Z} Complete -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase {Z+1}: {Name}** — {Goal from ROADMAP.md} @@ -413,7 +413,7 @@ NEXT_HAS_UI=$(echo "$NEXT_PHASE_SECTION" | grep -qi "UI hint.*yes" && echo "true ## ✓ Phase {Z} Complete -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase {Z+1}: {Name}** — {Goal from ROADMAP.md} @@ -441,7 +441,7 @@ NEXT_HAS_UI=$(echo "$NEXT_PHASE_SECTION" | grep -qi "UI hint.*yes" && echo "true All {N} phases finished! -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Complete Milestone** — archive and prepare for next @@ -472,7 +472,7 @@ Read MILESTONES.md to find the last completed milestone version. Ready to plan the next milestone. -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Start Next Milestone** — questioning → research → requirements → roadmap diff --git a/get-shit-done/workflows/resume-project.md b/get-shit-done/workflows/resume-project.md index 1c91732ee..71088c5c0 100644 --- a/get-shit-done/workflows/resume-project.md +++ b/get-shit-done/workflows/resume-project.md @@ -231,7 +231,7 @@ Based on user selection, route to appropriate workflow: ``` --- - ## ▶ Next Up + ## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **{phase}-{plan}: [Plan Name]** — [objective from PLAN.md] @@ -245,7 +245,7 @@ Based on user selection, route to appropriate workflow: ``` --- - ## ▶ Next Up + ## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase [N]: [Name]** — [Goal from ROADMAP.md] diff --git a/get-shit-done/workflows/transition.md b/get-shit-done/workflows/transition.md index 0d87281c8..37e8c48be 100644 --- a/get-shit-done/workflows/transition.md +++ b/get-shit-done/workflows/transition.md @@ -471,7 +471,7 @@ Exit skill and invoke SlashCommand("/gsd-discuss-phase [X+1] --auto ${GSD_WS}") --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase [X+1]: [Name]** — [Goal from ROADMAP.md] @@ -495,7 +495,7 @@ Exit skill and invoke SlashCommand("/gsd-discuss-phase [X+1] --auto ${GSD_WS}") --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Phase [X+1]: [Name]** — [Goal from ROADMAP.md] ✓ Context gathered, ready to plan @@ -606,7 +606,7 @@ Exit skill and invoke SlashCommand("/gsd-complete-milestone {version} ${GSD_WS}" --- -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Complete Milestone {version}** — archive and prepare for next diff --git a/get-shit-done/workflows/ui-phase.md b/get-shit-done/workflows/ui-phase.md index c736474ef..2c7ec3a46 100644 --- a/get-shit-done/workflows/ui-phase.md +++ b/get-shit-done/workflows/ui-phase.md @@ -261,7 +261,7 @@ Dimensions: 6/6 passed ─────────────────────────────────────────────────────────────── -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} {If CONTEXT.md exists for this phase:} **Plan Phase {N}** — planner will use UI-SPEC.md as design context diff --git a/get-shit-done/workflows/undo.md b/get-shit-done/workflows/undo.md index 5eb294f98..375ef46a4 100644 --- a/get-shit-done/workflows/undo.md +++ b/get-shit-done/workflows/undo.md @@ -279,7 +279,7 @@ Show next steps: ``` ─────────────────────────────────────────────────────────────── -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Review state** — verify project is in expected state after revert diff --git a/get-shit-done/workflows/verify-work.md b/get-shit-done/workflows/verify-work.md index d30020cc8..3b4c02d0e 100644 --- a/get-shit-done/workflows/verify-work.md +++ b/get-shit-done/workflows/verify-work.md @@ -676,7 +676,7 @@ Plans verified and ready for execution. ─────────────────────────────────────────────────────────────── -## ▶ Next Up +## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} **Execute fixes** — run fix plans diff --git a/tests/init.test.cjs b/tests/init.test.cjs index b8d5036f6..96307891a 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -1426,6 +1426,87 @@ describe('#2192: init plan-phase includes auto-advance config to prevent separat }); }); +// ───────────────────────────────────────────────────────────────────────────── +// withProjectRoot: project identity injection (#1948) +// ───────────────────────────────────────────────────────────────────────────── + +describe('withProjectRoot project identity', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('injects project_code when config.project_code is set', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ project_code: 'CK' }) + ); + + const result = runGsdTools(['init', 'quick', 'test task'], tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.project_code, 'CK'); + }); + + test('injects project_title extracted from PROJECT.md H1', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ project_code: 'CK' }) + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'PROJECT.md'), + '# CareKit\n\nA care management platform.\n' + ); + + const result = runGsdTools(['init', 'quick', 'test task'], tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.project_title, 'CareKit'); + }); + + test('omits project_code and project_title when project_code is not set', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'config.json'), + JSON.stringify({}) + ); + + const result = runGsdTools(['init', 'quick', 'test task'], tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.project_code, undefined, + 'project_code should not be present when not configured'); + // project_title may or may not be present depending on PROJECT.md existence, + // but without project_code the workflow omits the identity suffix entirely + }); + + test('omits project_title when PROJECT.md does not exist', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ project_code: 'CK' }) + ); + // Ensure no PROJECT.md exists (createTempProject doesn't create one) + const projectMdPath = path.join(tmpDir, '.planning', 'PROJECT.md'); + if (fs.existsSync(projectMdPath)) fs.unlinkSync(projectMdPath); + + const result = runGsdTools(['init', 'quick', 'test task'], tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.project_code, 'CK', + 'project_code should still be present'); + assert.strictEqual(output.project_title, undefined, + 'project_title should not be present when PROJECT.md is missing'); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // roadmap analyze command // ─────────────────────────────────────────────────────────────────────────────