feat(handoffs): include project identity in all Next Up blocks (#1948) (#2287)

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Higgins <brandonscotthiggins@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-15 16:43:02 -04:00
committed by GitHub
parent 779bd1a383
commit 55877d372f
22 changed files with 143 additions and 40 deletions

View File

@@ -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;
}

View File

@@ -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))) {

View File

@@ -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
<sub>Final plan in Phase 2</sub>
@@ -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

View File

@@ -83,7 +83,7 @@ Roadmap updated: .planning/ROADMAP.md
---
## ▶ Next Up
## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE}
**Phase {N}: {description}**

View File

@@ -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}`

View File

@@ -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

View File

@@ -780,7 +780,7 @@ Tag: v[X.Y]
---
## ▶ Next Up
## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE}
**Start Next Milestone** — questioning → research → requirements → roadmap

View File

@@ -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}

View File

@@ -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]

View File

@@ -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:

View File

@@ -345,7 +345,7 @@ Created .planning/codebase/:
---
## ▶ Next Up
## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE}
**Initialize project** — use codebase context for planning

View File

@@ -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]

View File

@@ -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]

View File

@@ -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**

View File

@@ -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

View File

@@ -211,7 +211,7 @@ Read its `<objective>` 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}
<sub>✓ Context gathered, ready to plan</sub>
@@ -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

View File

@@ -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]

View File

@@ -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]
<sub>✓ Context gathered, ready to plan</sub>
@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -676,7 +676,7 @@ Plans verified and ready for execution.
───────────────────────────────────────────────────────────────
## ▶ Next Up
## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE}
**Execute fixes** — run fix plans

View File

@@ -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
// ─────────────────────────────────────────────────────────────────────────────