refactor: adopt planningPaths() across 4 modules — eliminate 34 inline path constructions

Consolidates repeated path.join(cwd, '.planning', ...) patterns into
calls to the shared planningPaths() helper from core.cjs.

Modules updated:
- state.cjs: 16 → planningPaths() (2 remain for non-standard paths)
- commands.cjs: 8 → planningPaths() (4 remain for todos dir)
- milestone.cjs: 5 → planningPaths() (3 remain for archive/milestones)
- roadmap.cjs: 4 → planningPaths()

Benefits:
- Single source of truth for .planning/ directory structure
- Easier to change planning dir location in the future
- Consistent path construction across the codebase
- No behavioral changes — pure refactor

797/797 tests pass.
This commit is contained in:
Tom Boucher
2026-03-18 23:38:36 -04:00
parent 5fd384f336
commit 1d4deb0f8b
5 changed files with 41 additions and 41 deletions

View File

@@ -4,7 +4,7 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, getRoadmapPhaseInternal } = require('./core.cjs');
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, planningPaths, toPosixPath, output, error, findPhaseInternal, getRoadmapPhaseInternal } = require('./core.cjs');
const { extractFrontmatter } = require('./frontmatter.cjs');
const { MODEL_PROFILES } = require('./model-profiles.cjs');
@@ -98,7 +98,7 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
}
function cmdHistoryDigest(cwd, raw) {
const phasesDir = path.join(cwd, '.planning', 'phases');
const phasesDir = planningPaths(cwd).phases;
const digest = { phases: {}, decisions: [], tech_stack: new Set() };
// Collect all phase directories: archived + current
@@ -382,8 +382,8 @@ async function cmdWebsearch(query, options, raw) {
}
function cmdProgressRender(cwd, format, raw) {
const phasesDir = path.join(cwd, '.planning', 'phases');
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
const phasesDir = planningPaths(cwd).phases;
const roadmapPath = planningPaths(cwd).roadmap;
const milestone = getMilestoneInfo(cwd);
const phases = [];
@@ -637,7 +637,7 @@ function cmdScaffold(cwd, type, options, raw) {
}
const slug = generateSlugInternal(name);
const dirName = `${padded}-${slug}`;
const phasesParent = path.join(cwd, '.planning', 'phases');
const phasesParent = planningPaths(cwd).phases;
fs.mkdirSync(phasesParent, { recursive: true });
const dirPath = path.join(phasesParent, dirName);
fs.mkdirSync(dirPath, { recursive: true });
@@ -659,10 +659,10 @@ function cmdScaffold(cwd, type, options, raw) {
}
function cmdStats(cwd, format, raw) {
const phasesDir = path.join(cwd, '.planning', 'phases');
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
const statePath = path.join(cwd, '.planning', 'STATE.md');
const phasesDir = planningPaths(cwd).phases;
const roadmapPath = planningPaths(cwd).roadmap;
const reqPath = planningPaths(cwd).requirements;
const statePath = planningPaths(cwd).state;
const milestone = getMilestoneInfo(cwd);
const isDirInMilestone = getMilestonePhaseFilter(cwd);

View File

@@ -4,7 +4,7 @@
const fs = require('fs');
const path = require('path');
const { escapeRegex, getMilestonePhaseFilter, normalizeMd, output, error } = require('./core.cjs');
const { escapeRegex, getMilestonePhaseFilter, normalizeMd, planningPaths, output, error } = require('./core.cjs');
const { extractFrontmatter } = require('./frontmatter.cjs');
const { writeStateMd } = require('./state.cjs');
@@ -25,7 +25,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
error('no valid requirement IDs found');
}
const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
const reqPath = planningPaths(cwd).requirements;
if (!fs.existsSync(reqPath)) {
output({ updated: false, reason: 'REQUIREMENTS.md not found', ids: reqIds }, raw, 'no requirements file');
return;
@@ -90,12 +90,12 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
error('version required for milestone complete (e.g., v1.0)');
}
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
const statePath = path.join(cwd, '.planning', 'STATE.md');
const roadmapPath = planningPaths(cwd).roadmap;
const reqPath = planningPaths(cwd).requirements;
const statePath = planningPaths(cwd).state;
const milestonesPath = path.join(cwd, '.planning', 'MILESTONES.md');
const archiveDir = path.join(cwd, '.planning', 'milestones');
const phasesDir = path.join(cwd, '.planning', 'phases');
const phasesDir = planningPaths(cwd).phases;
const today = new Date().toISOString().split('T')[0];
const milestoneName = options.name || version;

View File

@@ -4,10 +4,10 @@
const fs = require('fs');
const path = require('path');
const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = require('./core.cjs');
const { escapeRegex, normalizePhaseName, planningPaths, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = require('./core.cjs');
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
const roadmapPath = planningPaths(cwd).roadmap;
if (!fs.existsSync(roadmapPath)) {
output({ found: false, error: 'ROADMAP.md not found' }, raw, '');
@@ -91,7 +91,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
}
function cmdRoadmapAnalyze(cwd, raw) {
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
const roadmapPath = planningPaths(cwd).roadmap;
if (!fs.existsSync(roadmapPath)) {
output({ error: 'ROADMAP.md not found', milestones: [], phases: [], current_phase: null }, raw);
@@ -100,7 +100,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
const content = extractCurrentMilestone(rawContent, cwd);
const phasesDir = path.join(cwd, '.planning', 'phases');
const phasesDir = planningPaths(cwd).phases;
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
@@ -230,7 +230,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
error('phase number required for roadmap update-plan-progress');
}
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
const roadmapPath = planningPaths(cwd).roadmap;
const phaseInfo = findPhaseInternal(cwd, phaseNum);
if (!phaseInfo) {

View File

@@ -26,15 +26,15 @@ function stateExtractField(content, fieldName) {
function cmdStateLoad(cwd, raw) {
const config = loadConfig(cwd);
const planningDir = path.join(cwd, '.planning');
const planDir = planningPaths(cwd).planning;
let stateRaw = '';
try {
stateRaw = fs.readFileSync(path.join(planningDir, 'STATE.md'), 'utf-8');
stateRaw = fs.readFileSync(path.join(planDir, 'STATE.md'), 'utf-8');
} catch { /* intentionally empty */ }
const configExists = fs.existsSync(path.join(planningDir, 'config.json'));
const roadmapExists = fs.existsSync(path.join(planningDir, 'ROADMAP.md'));
const configExists = fs.existsSync(path.join(planDir, 'config.json'));
const roadmapExists = fs.existsSync(path.join(planDir, 'ROADMAP.md'));
const stateExists = stateRaw.length > 0;
const result = {
@@ -70,7 +70,7 @@ function cmdStateLoad(cwd, raw) {
}
function cmdStateGet(cwd, section, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
try {
const content = fs.readFileSync(statePath, 'utf-8');
@@ -124,7 +124,7 @@ function readTextArgOrFile(cwd, value, filePath, label) {
}
function cmdStatePatch(cwd, patches, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
try {
let content = fs.readFileSync(statePath, 'utf-8');
const results = { updated: [], failed: [] };
@@ -161,7 +161,7 @@ function cmdStateUpdate(cwd, field, value) {
error('field and value required for state update');
}
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
try {
let content = fs.readFileSync(statePath, 'utf-8');
const fieldEscaped = escapeRegex(field);
@@ -202,7 +202,7 @@ function stateReplaceField(content, fieldName, newValue) {
}
function cmdStateAdvancePlan(cwd, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
let content = fs.readFileSync(statePath, 'utf-8');
@@ -231,7 +231,7 @@ function cmdStateAdvancePlan(cwd, raw) {
}
function cmdStateRecordMetric(cwd, options, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
let content = fs.readFileSync(statePath, 'utf-8');
@@ -265,13 +265,13 @@ function cmdStateRecordMetric(cwd, options, raw) {
}
function cmdStateUpdateProgress(cwd, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
let content = fs.readFileSync(statePath, 'utf-8');
// Count summaries across current milestone phases only
const phasesDir = path.join(cwd, '.planning', 'phases');
const phasesDir = planningPaths(cwd).phases;
let totalPlans = 0;
let totalSummaries = 0;
@@ -310,7 +310,7 @@ function cmdStateUpdateProgress(cwd, raw) {
}
function cmdStateAddDecision(cwd, options, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
const { phase, summary, summary_file, rationale, rationale_file } = options;
@@ -348,7 +348,7 @@ function cmdStateAddDecision(cwd, options, raw) {
}
function cmdStateAddBlocker(cwd, text, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
const blockerOptions = typeof text === 'object' && text !== null ? text : { text };
let blockerText = null;
@@ -381,7 +381,7 @@ function cmdStateAddBlocker(cwd, text, raw) {
}
function cmdStateResolveBlocker(cwd, text, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
if (!text) { output({ error: 'text required' }, raw); return; }
@@ -413,7 +413,7 @@ function cmdStateResolveBlocker(cwd, text, raw) {
}
function cmdStateRecordSession(cwd, options, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
let content = fs.readFileSync(statePath, 'utf-8');
@@ -448,7 +448,7 @@ function cmdStateRecordSession(cwd, options, raw) {
}
function cmdStateSnapshot(cwd, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) {
output({ error: 'STATE.md not found' }, raw);
@@ -580,7 +580,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
if (cwd) {
try {
const phasesDir = path.join(cwd, '.planning', 'phases');
const phasesDir = planningPaths(cwd).phases;
if (fs.existsSync(phasesDir)) {
const isDirInMilestone = getMilestonePhaseFilter(cwd);
const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
@@ -722,7 +722,7 @@ function writeStateMd(statePath, content, cwd) {
}
function cmdStateJson(cwd, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) {
output({ error: 'STATE.md not found' }, raw, 'STATE.md not found');
return;
@@ -748,7 +748,7 @@ function cmdStateJson(cwd, raw) {
* Fixes: #1102 (plan counts), #1103 (status/last_activity), #1104 (body text).
*/
function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
const statePath = path.join(cwd, '.planning', 'STATE.md');
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) {
output({ error: 'STATE.md not found' }, raw);
return;

View File

@@ -32,7 +32,7 @@ Test body
const nameMatch = result.match(/^name:\s*(.+)$/m);
assert.ok(nameMatch, 'frontmatter contains name field');
assert.equal(nameMatch[1], 'gsd-quick', 'skill name is plain scalar');
assert.strictEqual(nameMatch[1], 'gsd-quick', 'skill name is plain scalar');
assert.ok(!result.includes('name: "gsd-quick"'), 'quoted skill name is not emitted');
});
@@ -75,7 +75,7 @@ Planner body
const nameMatch = result.match(/^name:\s*(.+)$/m);
assert.ok(nameMatch, 'frontmatter contains name field');
assert.equal(nameMatch[1], 'gsd-planner', 'agent name is plain scalar');
assert.strictEqual(nameMatch[1], 'gsd-planner', 'agent name is plain scalar');
assert.ok(!result.includes('name: "gsd-planner"'), 'quoted agent name is not emitted');
});
});