Merge pull request #1124 from trek-e/refactor/deduplicate-and-cleanup
This commit is contained in:
@@ -71,9 +71,9 @@ function cmdListTodos(cwd, area, raw) {
|
|||||||
area: todoArea,
|
area: todoArea,
|
||||||
path: toPosixPath(path.join('.planning', 'todos', 'pending', file)),
|
path: toPosixPath(path.join('.planning', 'todos', 'pending', file)),
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
const result = { count, todos };
|
const result = { count, todos };
|
||||||
output(result, raw, count.toString());
|
output(result, raw, count.toString());
|
||||||
@@ -120,7 +120,7 @@ function cmdHistoryDigest(cwd, raw) {
|
|||||||
for (const dir of currentDirs) {
|
for (const dir of currentDirs) {
|
||||||
allPhaseDirs.push({ name: dir, fullPath: path.join(phasesDir, dir), milestone: null });
|
allPhaseDirs.push({ name: dir, fullPath: path.join(phasesDir, dir), milestone: null });
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allPhaseDirs.length === 0) {
|
if (allPhaseDirs.length === 0) {
|
||||||
@@ -412,7 +412,7 @@ function cmdProgressRender(cwd, format, raw) {
|
|||||||
|
|
||||||
phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
|
phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
|
const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
|
||||||
|
|
||||||
@@ -559,7 +559,7 @@ function cmdStats(cwd, format, raw) {
|
|||||||
status: 'Not Started',
|
status: 'Not Started',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
||||||
@@ -595,7 +595,7 @@ function cmdStats(cwd, format, raw) {
|
|||||||
status,
|
status,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
const phases = [...phasesByNumber.values()].sort((a, b) => comparePhaseNum(a.number, b.number));
|
const phases = [...phasesByNumber.values()].sort((a, b) => comparePhaseNum(a.number, b.number));
|
||||||
const completedPhases = phases.filter(p => p.status === 'Complete').length;
|
const completedPhases = phases.filter(p => p.status === 'Complete').length;
|
||||||
@@ -613,7 +613,7 @@ function cmdStats(cwd, format, raw) {
|
|||||||
requirementsComplete = checked ? checked.length : 0;
|
requirementsComplete = checked ? checked.length : 0;
|
||||||
requirementsTotal = requirementsComplete + (unchecked ? unchecked.length : 0);
|
requirementsTotal = requirementsComplete + (unchecked ? unchecked.length : 0);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
// Last activity from STATE.md
|
// Last activity from STATE.md
|
||||||
let lastActivity = null;
|
let lastActivity = null;
|
||||||
@@ -626,7 +626,7 @@ function cmdStats(cwd, format, raw) {
|
|||||||
|| stateContent.match(/^Last activity:\s*(.+)$/im);
|
|| stateContent.match(/^Last activity:\s*(.+)$/im);
|
||||||
if (activityMatch) lastActivity = activityMatch[1].trim();
|
if (activityMatch) lastActivity = activityMatch[1].trim();
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
// Git stats
|
// Git stats
|
||||||
let gitCommits = 0;
|
let gitCommits = 0;
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ function ensureConfigFile(cwd) {
|
|||||||
delete userDefaults.depth;
|
delete userDefaults.depth;
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(globalDefaultsPath, JSON.stringify(userDefaults, null, 2), 'utf-8');
|
fs.writeFileSync(globalDefaultsPath, JSON.stringify(userDefaults, null, 2), 'utf-8');
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ function loadConfig(cwd) {
|
|||||||
const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
|
const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
|
||||||
parsed.granularity = depthToGranularity[parsed.depth] || parsed.depth;
|
parsed.granularity = depthToGranularity[parsed.depth] || parsed.depth;
|
||||||
delete parsed.depth;
|
delete parsed.depth;
|
||||||
try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch {}
|
try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
const get = (key, nested) => {
|
const get = (key, nested) => {
|
||||||
@@ -250,6 +250,27 @@ function execGit(cwd, args) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Common path helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Get the .planning directory path */
|
||||||
|
function planningDir(cwd) {
|
||||||
|
return path.join(cwd, '.planning');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get common .planning file paths */
|
||||||
|
function planningPaths(cwd) {
|
||||||
|
const base = path.join(cwd, '.planning');
|
||||||
|
return {
|
||||||
|
planning: base,
|
||||||
|
state: path.join(base, 'STATE.md'),
|
||||||
|
roadmap: path.join(base, 'ROADMAP.md'),
|
||||||
|
project: path.join(base, 'PROJECT.md'),
|
||||||
|
config: path.join(base, 'config.json'),
|
||||||
|
phases: path.join(base, 'phases'),
|
||||||
|
requirements: path.join(base, 'REQUIREMENTS.md'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Phase utilities ──────────────────────────────────────────────────────────
|
// ─── Phase utilities ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function escapeRegex(value) {
|
function escapeRegex(value) {
|
||||||
@@ -370,7 +391,7 @@ function findPhaseInternal(cwd, phase) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -405,7 +426,7 @@ function getArchivedPhaseDirs(cwd) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
@@ -663,7 +684,7 @@ function getMilestonePhaseFilter(cwd) {
|
|||||||
while ((m = phasePattern.exec(roadmap)) !== null) {
|
while ((m = phasePattern.exec(roadmap)) !== null) {
|
||||||
milestonePhaseNums.add(m[1]);
|
milestonePhaseNums.add(m[1]);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
if (milestonePhaseNums.size === 0) {
|
if (milestonePhaseNums.size === 0) {
|
||||||
const passAll = () => true;
|
const passAll = () => true;
|
||||||
@@ -709,4 +730,6 @@ module.exports = {
|
|||||||
replaceInCurrentMilestone,
|
replaceInCurrentMilestone,
|
||||||
toPosixPath,
|
toPosixPath,
|
||||||
MODEL_ALIAS_MAP,
|
MODEL_ALIAS_MAP,
|
||||||
|
planningDir,
|
||||||
|
planningPaths,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
|||||||
if (uatFile) {
|
if (uatFile) {
|
||||||
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
|
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
output(result, raw);
|
output(result, raw);
|
||||||
@@ -167,17 +167,26 @@ function cmdInitNewProject(cwd, raw) {
|
|||||||
const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
|
const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
|
||||||
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
|
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
|
||||||
|
|
||||||
// Detect existing code
|
// Detect existing code (cross-platform — no Unix `find` dependency)
|
||||||
let hasCode = false;
|
let hasCode = false;
|
||||||
let hasPackageFile = false;
|
let hasPackageFile = false;
|
||||||
try {
|
try {
|
||||||
const files = execSync('find . -maxdepth 3 \\( -name "*.ts" -o -name "*.js" -o -name "*.py" -o -name "*.go" -o -name "*.rs" -o -name "*.swift" -o -name "*.java" \\) 2>/dev/null | grep -v node_modules | grep -v .git | head -5', {
|
const codeExtensions = new Set(['.ts', '.js', '.py', '.go', '.rs', '.swift', '.java']);
|
||||||
cwd,
|
const skipDirs = new Set(['node_modules', '.git', '.planning', '.claude', '__pycache__', 'target', 'dist', 'build']);
|
||||||
encoding: 'utf-8',
|
function findCodeFiles(dir, depth) {
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
if (depth > 3) return false;
|
||||||
});
|
let entries;
|
||||||
hasCode = files.trim().length > 0;
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return false; }
|
||||||
} catch {}
|
for (const entry of entries) {
|
||||||
|
if (entry.isFile() && codeExtensions.has(path.extname(entry.name))) return true;
|
||||||
|
if (entry.isDirectory() && !skipDirs.has(entry.name)) {
|
||||||
|
if (findCodeFiles(path.join(dir, entry.name), depth + 1)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
hasCode = findCodeFiles(cwd, 0);
|
||||||
|
} catch { /* intentionally empty — best-effort detection */ }
|
||||||
|
|
||||||
hasPackageFile = pathExistsInternal(cwd, 'package.json') ||
|
hasPackageFile = pathExistsInternal(cwd, 'package.json') ||
|
||||||
pathExistsInternal(cwd, 'requirements.txt') ||
|
pathExistsInternal(cwd, 'requirements.txt') ||
|
||||||
@@ -307,7 +316,7 @@ function cmdInitResume(cwd, raw) {
|
|||||||
let interruptedAgentId = null;
|
let interruptedAgentId = null;
|
||||||
try {
|
try {
|
||||||
interruptedAgentId = fs.readFileSync(path.join(cwd, '.planning', 'current-agent-id.txt'), 'utf-8').trim();
|
interruptedAgentId = fs.readFileSync(path.join(cwd, '.planning', 'current-agent-id.txt'), 'utf-8').trim();
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
// File existence
|
// File existence
|
||||||
@@ -459,7 +468,7 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
|||||||
if (uatFile) {
|
if (uatFile) {
|
||||||
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
|
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
output(result, raw);
|
output(result, raw);
|
||||||
@@ -494,9 +503,9 @@ function cmdInitTodos(cwd, area, raw) {
|
|||||||
area: todoArea,
|
area: todoArea,
|
||||||
path: '.planning/todos/pending/' + file,
|
path: '.planning/todos/pending/' + file,
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
// Config
|
// Config
|
||||||
@@ -543,9 +552,9 @@ function cmdInitMilestoneOp(cwd, raw) {
|
|||||||
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
||||||
const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
||||||
if (hasSummary) completedPhases++;
|
if (hasSummary) completedPhases++;
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
// Check archive
|
// Check archive
|
||||||
const archiveDir = path.join(cwd, '.planning', 'archive');
|
const archiveDir = path.join(cwd, '.planning', 'archive');
|
||||||
@@ -554,7 +563,7 @@ function cmdInitMilestoneOp(cwd, raw) {
|
|||||||
archivedMilestones = fs.readdirSync(archiveDir, { withFileTypes: true })
|
archivedMilestones = fs.readdirSync(archiveDir, { withFileTypes: true })
|
||||||
.filter(e => e.isDirectory())
|
.filter(e => e.isDirectory())
|
||||||
.map(e => e.name);
|
.map(e => e.name);
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
// Config
|
// Config
|
||||||
@@ -593,7 +602,7 @@ function cmdInitMapCodebase(cwd, raw) {
|
|||||||
let existingMaps = [];
|
let existingMaps = [];
|
||||||
try {
|
try {
|
||||||
existingMaps = fs.readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
|
existingMaps = fs.readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
// Models
|
// Models
|
||||||
@@ -642,7 +651,7 @@ function cmdInitProgress(cwd, raw) {
|
|||||||
roadmapPhaseNums.add(hm[1]);
|
roadmapPhaseNums.add(hm[1]);
|
||||||
roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim());
|
roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim());
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
||||||
const seenPhaseNums = new Set();
|
const seenPhaseNums = new Set();
|
||||||
@@ -695,7 +704,7 @@ function cmdInitProgress(cwd, raw) {
|
|||||||
nextPhase = phaseInfo;
|
nextPhase = phaseInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
// Add phases defined in ROADMAP but not yet scaffolded to disk
|
// Add phases defined in ROADMAP but not yet scaffolded to disk
|
||||||
for (const [num, name] of roadmapPhaseNames) {
|
for (const [num, name] of roadmapPhaseNames) {
|
||||||
@@ -726,7 +735,7 @@ function cmdInitProgress(cwd, raw) {
|
|||||||
const state = fs.readFileSync(path.join(cwd, '.planning', 'STATE.md'), 'utf-8');
|
const state = fs.readFileSync(path.join(cwd, '.planning', 'STATE.md'), 'utf-8');
|
||||||
const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/);
|
const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/);
|
||||||
if (pauseMatch) pausedAt = pauseMatch[1].trim();
|
if (pauseMatch) pausedAt = pauseMatch[1].trim();
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
// Models
|
// Models
|
||||||
|
|||||||
@@ -137,10 +137,10 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|||||||
// Count tasks
|
// Count tasks
|
||||||
const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
|
const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
|
||||||
totalTasks += taskMatches.length;
|
totalTasks += taskMatches.length;
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
// Archive ROADMAP.md
|
// Archive ROADMAP.md
|
||||||
if (fs.existsSync(roadmapPath)) {
|
if (fs.existsSync(roadmapPath)) {
|
||||||
@@ -220,7 +220,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|||||||
archivedCount++;
|
archivedCount++;
|
||||||
}
|
}
|
||||||
phasesArchived = archivedCount > 0;
|
phasesArchived = archivedCount > 0;
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|||||||
const dm = dir.match(decimalPattern);
|
const dm = dir.match(decimalPattern);
|
||||||
if (dm) existingDecimals.push(parseInt(dm[1], 10));
|
if (dm) existingDecimals.push(parseInt(dm[1], 10));
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1;
|
const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1;
|
||||||
const decimalPhase = `${normalizedBase}.${nextDecimal}`;
|
const decimalPhase = `${normalizedBase}.${nextDecimal}`;
|
||||||
@@ -470,7 +470,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|||||||
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
||||||
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
||||||
targetDir = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
|
targetDir = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
// Check for executed work (SUMMARY.md files)
|
// Check for executed work (SUMMARY.md files)
|
||||||
if (targetDir && !force) {
|
if (targetDir && !force) {
|
||||||
@@ -538,7 +538,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Integer removal: renumber all subsequent integer phases
|
// Integer removal: renumber all subsequent integer phases
|
||||||
@@ -598,7 +598,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update ROADMAP.md
|
// Update ROADMAP.md
|
||||||
@@ -818,7 +818,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
// Fallback: if filesystem found no next phase, check ROADMAP.md
|
// Fallback: if filesystem found no next phase, check ROADMAP.md
|
||||||
// for phases that are defined but not yet planned (no directory on disk)
|
// for phases that are defined but not yet planned (no directory on disk)
|
||||||
@@ -835,7 +835,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update STATE.md
|
// Update STATE.md
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|||||||
else if (hasContext) diskStatus = 'discussed';
|
else if (hasContext) diskStatus = 'discussed';
|
||||||
else diskStatus = 'empty';
|
else diskStatus = 'empty';
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
// Check ROADMAP checkbox status
|
// Check ROADMAP checkbox status
|
||||||
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s]`, 'i');
|
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s]`, 'i');
|
||||||
|
|||||||
@@ -4,9 +4,14 @@
|
|||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, output, error } = require('./core.cjs');
|
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, planningPaths, output, error } = require('./core.cjs');
|
||||||
const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
|
const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
|
||||||
|
|
||||||
|
/** Shorthand — every state command needs this path */
|
||||||
|
function getStatePath(cwd) {
|
||||||
|
return planningPaths(cwd).state;
|
||||||
|
}
|
||||||
|
|
||||||
// Shared helper: extract a field value from STATE.md content.
|
// Shared helper: extract a field value from STATE.md content.
|
||||||
// Supports both **Field:** bold and plain Field: format.
|
// Supports both **Field:** bold and plain Field: format.
|
||||||
function stateExtractField(content, fieldName) {
|
function stateExtractField(content, fieldName) {
|
||||||
@@ -26,7 +31,7 @@ function cmdStateLoad(cwd, raw) {
|
|||||||
let stateRaw = '';
|
let stateRaw = '';
|
||||||
try {
|
try {
|
||||||
stateRaw = fs.readFileSync(path.join(planningDir, 'STATE.md'), 'utf-8');
|
stateRaw = fs.readFileSync(path.join(planningDir, 'STATE.md'), 'utf-8');
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
const configExists = fs.existsSync(path.join(planningDir, 'config.json'));
|
const configExists = fs.existsSync(path.join(planningDir, 'config.json'));
|
||||||
const roadmapExists = fs.existsSync(path.join(planningDir, 'ROADMAP.md'));
|
const roadmapExists = fs.existsSync(path.join(planningDir, 'ROADMAP.md'));
|
||||||
@@ -75,7 +80,7 @@ function cmdStateGet(cwd, section, raw) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to find markdown section or field
|
// Try to find markdown section or field
|
||||||
const fieldEscaped = section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const fieldEscaped = escapeRegex(section);
|
||||||
|
|
||||||
// Check for **field:** value (bold format)
|
// Check for **field:** value (bold format)
|
||||||
const boldPattern = new RegExp(`\\*\\*${fieldEscaped}:\\*\\*\\s*(.*)`, 'i');
|
const boldPattern = new RegExp(`\\*\\*${fieldEscaped}:\\*\\*\\s*(.*)`, 'i');
|
||||||
@@ -125,7 +130,7 @@ function cmdStatePatch(cwd, patches, raw) {
|
|||||||
const results = { updated: [], failed: [] };
|
const results = { updated: [], failed: [] };
|
||||||
|
|
||||||
for (const [field, value] of Object.entries(patches)) {
|
for (const [field, value] of Object.entries(patches)) {
|
||||||
const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const fieldEscaped = escapeRegex(field);
|
||||||
// Try **Field:** bold format first, then plain Field: format
|
// Try **Field:** bold format first, then plain Field: format
|
||||||
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||||
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
||||||
@@ -159,7 +164,7 @@ function cmdStateUpdate(cwd, field, value) {
|
|||||||
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
||||||
try {
|
try {
|
||||||
let content = fs.readFileSync(statePath, 'utf-8');
|
let content = fs.readFileSync(statePath, 'utf-8');
|
||||||
const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const fieldEscaped = escapeRegex(field);
|
||||||
// Try **Field:** bold format first, then plain Field: format
|
// Try **Field:** bold format first, then plain Field: format
|
||||||
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||||
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
||||||
@@ -180,21 +185,10 @@ function cmdStateUpdate(cwd, field, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── State Progression Engine ────────────────────────────────────────────────
|
// ─── State Progression Engine ────────────────────────────────────────────────
|
||||||
|
// stateExtractField is defined above (shared helper) — do not duplicate.
|
||||||
function stateExtractField(content, fieldName) {
|
|
||||||
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
// Try **Field:** bold format first
|
|
||||||
const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.+)`, 'i');
|
|
||||||
const boldMatch = content.match(boldPattern);
|
|
||||||
if (boldMatch) return boldMatch[1].trim();
|
|
||||||
// Fall back to plain Field: format
|
|
||||||
const plainPattern = new RegExp(`^${escaped}:\\s*(.+)`, 'im');
|
|
||||||
const plainMatch = content.match(plainPattern);
|
|
||||||
return plainMatch ? plainMatch[1].trim() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stateReplaceField(content, fieldName, newValue) {
|
function stateReplaceField(content, fieldName, newValue) {
|
||||||
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const escaped = escapeRegex(fieldName);
|
||||||
// Try **Field:** bold format first, then plain Field: format
|
// Try **Field:** bold format first, then plain Field: format
|
||||||
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
|
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||||
if (boldPattern.test(content)) {
|
if (boldPattern.test(content)) {
|
||||||
@@ -576,7 +570,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|||||||
const info = getMilestoneInfo(cwd);
|
const info = getMilestoneInfo(cwd);
|
||||||
milestone = info.version;
|
milestone = info.version;
|
||||||
milestoneName = info.name;
|
milestoneName = info.name;
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
|
let totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
|
||||||
@@ -611,7 +605,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|||||||
totalPlans = diskTotalPlans;
|
totalPlans = diskTotalPlans;
|
||||||
completedPlans = diskTotalSummaries;
|
completedPlans = diskTotalSummaries;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
let progressPercent = null;
|
let progressPercent = null;
|
||||||
|
|||||||
@@ -428,7 +428,7 @@ function cmdValidateConsistency(cwd, raw) {
|
|||||||
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
|
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
|
||||||
if (dm) diskPhases.add(dm[1]);
|
if (dm) diskPhases.add(dm[1]);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
// Check: phases in ROADMAP but not on disk
|
// Check: phases in ROADMAP but not on disk
|
||||||
for (const p of roadmapPhases) {
|
for (const p of roadmapPhases) {
|
||||||
@@ -490,7 +490,7 @@ function cmdValidateConsistency(cwd, raw) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
// Check: frontmatter in plans has required fields
|
// Check: frontmatter in plans has required fields
|
||||||
try {
|
try {
|
||||||
@@ -510,7 +510,7 @@ function cmdValidateConsistency(cwd, raw) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
const passed = errors.length === 0;
|
const passed = errors.length === 0;
|
||||||
output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed');
|
output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed');
|
||||||
@@ -599,7 +599,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|||||||
if (m) diskPhases.add(m[1]);
|
if (m) diskPhases.add(m[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
// Check for invalid references
|
// Check for invalid references
|
||||||
for (const ref of phaseRefs) {
|
for (const ref of phaseRefs) {
|
||||||
const normalizedRef = String(parseInt(ref, 10)).padStart(2, '0');
|
const normalizedRef = String(parseInt(ref, 10)).padStart(2, '0');
|
||||||
@@ -641,7 +641,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|||||||
addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', 'Run /gsd:health --repair to add key', true);
|
addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', 'Run /gsd:health --repair to add key', true);
|
||||||
if (!repairs.includes('addNyquistKey')) repairs.push('addNyquistKey');
|
if (!repairs.includes('addNyquistKey')) repairs.push('addNyquistKey');
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Check 6: Phase directory naming (NN-name format) ─────────────────────
|
// ─── Check 6: Phase directory naming (NN-name format) ─────────────────────
|
||||||
@@ -652,7 +652,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|||||||
addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
|
addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
// ─── Check 7: Orphaned plans (PLAN without SUMMARY) ───────────────────────
|
// ─── Check 7: Orphaned plans (PLAN without SUMMARY) ───────────────────────
|
||||||
try {
|
try {
|
||||||
@@ -671,7 +671,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
// ─── Check 7b: Nyquist VALIDATION.md consistency ────────────────────────
|
// ─── Check 7b: Nyquist VALIDATION.md consistency ────────────────────────
|
||||||
try {
|
try {
|
||||||
@@ -689,7 +689,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
// ─── Check 8: Run existing consistency checks ─────────────────────────────
|
// ─── Check 8: Run existing consistency checks ─────────────────────────────
|
||||||
// Inline subset of cmdValidateConsistency
|
// Inline subset of cmdValidateConsistency
|
||||||
@@ -712,7 +712,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|||||||
if (dm) diskPhases.add(dm[1]);
|
if (dm) diskPhases.add(dm[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { /* intentionally empty */ }
|
||||||
|
|
||||||
// Phases in ROADMAP but not on disk
|
// Phases in ROADMAP but not on disk
|
||||||
for (const p of roadmapPhases) {
|
for (const p of roadmapPhases) {
|
||||||
|
|||||||
Reference in New Issue
Block a user