diff --git a/.changeset/904-init-execute-phase-normalize-branch.md b/.changeset/904-init-execute-phase-normalize-branch.md new file mode 100644 index 000000000..3e1a3dd6c --- /dev/null +++ b/.changeset/904-init-execute-phase-normalize-branch.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 904 +--- +**`init execute-phase` and `cmdCommit` now produce correct `branch_name` when `project_code` is set** — the `{phase}` substitution in `phase_branch_template` now calls `normalizePhaseName()`, stripping the project-code prefix and zero-padding the number, so the generated branch is e.g. `gsd/phase-01-foundation` instead of `gsd/phase-CK-01-foundation`. Both the execute-phase output path (`src/init.cts`) and the pre-execution commit path (`src/commands.cts`) are fixed. (#904) diff --git a/src/commands.cts b/src/commands.cts index 0d72e81bc..78e37f5f4 100644 --- a/src/commands.cts +++ b/src/commands.cts @@ -547,7 +547,7 @@ function cmdCommit(cwd: string, message: string | undefined, files: string[] | u const phaseInfo = findPhaseInternal(cwd, phaseNum) as Record | null; if (phaseInfo) { branchName = (config['phase_branch_template'] as string) - .replace('{phase}', phaseInfo['phase_number'] as string) + .replace('{phase}', normalizePhaseName(phaseInfo['phase_number'])) .replace('{slug}', (phaseInfo['phase_slug'] as string) || 'phase'); } } diff --git a/src/init.cts b/src/init.cts index 444ae31d3..a6379a169 100644 --- a/src/init.cts +++ b/src/init.cts @@ -260,7 +260,7 @@ function cmdInitExecutePhase( config.branching_strategy === 'phase' && phaseInfo ? (config.phase_branch_template as string) .replace('{project}', (config.project_code as string) || '') - .replace('{phase}', phaseInfo['phase_number'] as string) + .replace('{phase}', normalizePhaseName(phaseInfo['phase_number'])) .replace('{slug}', (phaseInfo['phase_slug'] as string) || 'phase') : config.branching_strategy === 'milestone' ? (config.milestone_branch_template as string) diff --git a/tests/init.test.cjs b/tests/init.test.cjs index ec5957998..14bbe38ec 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -538,6 +538,35 @@ describe('init plan-phase zero-padded phase number (bug #2391)', () => { assert.strictEqual(out03.phase_req_ids, out3.phase_req_ids, 'phase_req_ids must be identical regardless of padding'); }); + + // ── #904: branch_name must use normalized (stripped + zero-padded) phase number ── + // When project_code is set (e.g. "CK") the phase directory is prefixed: + // "CK-01-foundation". extractPhaseToken returns "CK-01" as phase_number. + // branch_name must call normalizePhaseName so it strips the prefix and zero-pads, + // producing "gsd/phase-01-foundation" rather than "gsd/phase-CK-01-foundation". + test('branch_name uses normalized phase number when project_code prefixes phase dir (#904)', () => { + seedPhase(tmpDir, 'CK-01-foundation', { + 'CK-01-01-PLAN.md': '# Plan', + }); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ + project_code: 'CK', + git: { + branching_strategy: 'phase', + phase_branch_template: 'gsd/phase-{phase}-{slug}', + }, + }, null, 2) + ); + + const result = runGsdTools('init execute-phase 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + // branch_name must use the normalized phase number, not the raw "CK-01" token + assert.strictEqual(output.branch_name, 'gsd/phase-01-foundation', + 'branch_name must use normalized phase number (strip project_code prefix, zero-pad), not raw phase_number'); + }); }); // ─────────────────────────────────────────────────────────────────────────────