gsd-opencode 1.20.3 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/gsd-codebase-mapper.md +9 -1
- package/agents/gsd-debugger.md +66 -10
- package/agents/gsd-executor.md +36 -16
- package/agents/gsd-integration-checker.md +2 -0
- package/agents/gsd-nyquist-auditor.md +178 -0
- package/agents/gsd-phase-researcher.md +28 -34
- package/agents/gsd-plan-checker.md +42 -78
- package/agents/gsd-planner.md +139 -24
- package/agents/gsd-project-researcher.md +11 -1
- package/agents/gsd-research-synthesizer.md +13 -3
- package/agents/gsd-roadmapper.md +25 -15
- package/agents/gsd-verifier.md +29 -6
- package/bin/dm/lib/constants.js +6 -1
- package/bin/dm/src/services/file-ops.js +14 -1
- package/commands/gsd/gsd-add-phase.md +6 -6
- package/commands/gsd/gsd-add-tests.md +41 -0
- package/commands/gsd/gsd-add-todo.md +7 -7
- package/commands/gsd/gsd-audit-milestone.md +9 -9
- package/commands/gsd/gsd-check-profile.md +3 -3
- package/commands/gsd/gsd-check-todos.md +7 -7
- package/commands/gsd/gsd-cleanup.md +2 -2
- package/commands/gsd/gsd-complete-milestone.md +6 -6
- package/commands/gsd/gsd-debug.md +11 -7
- package/commands/gsd/gsd-discuss-phase.md +26 -19
- package/commands/gsd/gsd-execute-phase.md +13 -13
- package/commands/gsd/gsd-health.md +7 -7
- package/commands/gsd/gsd-help.md +2 -2
- package/commands/gsd/gsd-insert-phase.md +6 -6
- package/commands/gsd/gsd-join-discord.md +1 -1
- package/commands/gsd/gsd-list-phase-assumptions.md +6 -6
- package/commands/gsd/gsd-map-codebase.md +8 -8
- package/commands/gsd/gsd-new-milestone.md +12 -12
- package/commands/gsd/gsd-new-project.md +12 -12
- package/commands/gsd/gsd-pause-work.md +6 -6
- package/commands/gsd/gsd-plan-milestone-gaps.md +9 -9
- package/commands/gsd/gsd-plan-phase.md +14 -13
- package/commands/gsd/gsd-progress.md +8 -8
- package/commands/gsd/gsd-quick.md +17 -13
- package/commands/gsd/gsd-reapply-patches.md +19 -11
- package/commands/gsd/gsd-remove-phase.md +7 -7
- package/commands/gsd/gsd-research-phase.md +12 -11
- package/commands/gsd/gsd-resume-work.md +8 -8
- package/commands/gsd/gsd-set-profile.md +6 -6
- package/commands/gsd/gsd-settings.md +7 -7
- package/commands/gsd/gsd-update.md +5 -5
- package/commands/gsd/gsd-validate-phase.md +35 -0
- package/commands/gsd/gsd-verify-work.md +11 -11
- package/get-shit-done/bin/gsd-oc-commands/allow-read-config.cjs +235 -0
- package/get-shit-done/bin/gsd-oc-tools.cjs +11 -5
- package/get-shit-done/bin/gsd-tools.cjs +45 -6
- package/get-shit-done/bin/lib/commands.cjs +11 -19
- package/get-shit-done/bin/lib/config.cjs +8 -1
- package/get-shit-done/bin/lib/core.cjs +131 -16
- package/get-shit-done/bin/lib/init.cjs +28 -12
- package/get-shit-done/bin/lib/milestone.cjs +34 -8
- package/get-shit-done/bin/lib/phase.cjs +74 -50
- package/get-shit-done/bin/lib/roadmap.cjs +7 -7
- package/get-shit-done/bin/lib/state.cjs +294 -63
- package/get-shit-done/bin/lib/template.cjs +3 -3
- package/get-shit-done/bin/lib/verify.cjs +56 -8
- package/get-shit-done/bin/test/allow-read-config.test.cjs +262 -0
- package/get-shit-done/references/checkpoints.md +1 -1
- package/get-shit-done/references/decimal-phase-calculation.md +6 -6
- package/get-shit-done/references/git-integration.md +3 -3
- package/get-shit-done/references/git-planning-commit.md +2 -2
- package/get-shit-done/references/model-profile-resolution.md +1 -1
- package/get-shit-done/references/model-profiles.md +1 -0
- package/get-shit-done/references/phase-argument-parsing.md +4 -4
- package/get-shit-done/references/planning-config.md +10 -6
- package/get-shit-done/references/questioning.md +17 -0
- package/get-shit-done/references/verification-patterns.md +1 -1
- package/get-shit-done/templates/DEBUG.md +7 -2
- package/get-shit-done/templates/VALIDATION.md +18 -46
- package/get-shit-done/templates/codebase/structure.md +3 -3
- package/get-shit-done/templates/config.json +2 -2
- package/get-shit-done/templates/context.md +14 -0
- package/get-shit-done/templates/phase-prompt.md +10 -10
- package/get-shit-done/templates/retrospective.md +54 -0
- package/get-shit-done/templates/roadmap.md +1 -1
- package/get-shit-done/workflows/add-phase.md +3 -2
- package/get-shit-done/workflows/add-tests.md +351 -0
- package/get-shit-done/workflows/add-todo.md +4 -3
- package/get-shit-done/workflows/audit-milestone.md +40 -5
- package/get-shit-done/workflows/check-todos.md +3 -2
- package/get-shit-done/workflows/cleanup.md +1 -1
- package/get-shit-done/workflows/complete-milestone.md +69 -5
- package/get-shit-done/workflows/diagnose-issues.md +2 -2
- package/get-shit-done/workflows/discovery-phase.md +6 -6
- package/get-shit-done/workflows/discuss-phase.md +194 -58
- package/get-shit-done/workflows/execute-phase.md +29 -23
- package/get-shit-done/workflows/execute-plan.md +22 -18
- package/get-shit-done/workflows/health.md +5 -2
- package/get-shit-done/workflows/help.md +4 -1
- package/get-shit-done/workflows/insert-phase.md +3 -2
- package/get-shit-done/workflows/map-codebase.md +3 -2
- package/get-shit-done/workflows/new-milestone.md +12 -10
- package/get-shit-done/workflows/new-project.md +44 -49
- package/get-shit-done/workflows/oc-set-profile.md +24 -0
- package/get-shit-done/workflows/pause-work.md +2 -2
- package/get-shit-done/workflows/plan-milestone-gaps.md +3 -3
- package/get-shit-done/workflows/plan-phase.md +155 -73
- package/get-shit-done/workflows/progress.md +8 -7
- package/get-shit-done/workflows/quick.md +158 -10
- package/get-shit-done/workflows/remove-phase.md +5 -4
- package/get-shit-done/workflows/research-phase.md +5 -4
- package/get-shit-done/workflows/resume-project.md +3 -2
- package/get-shit-done/workflows/set-profile.md +3 -2
- package/get-shit-done/workflows/settings.md +6 -6
- package/get-shit-done/workflows/transition.md +5 -5
- package/get-shit-done/workflows/update.md +45 -19
- package/get-shit-done/workflows/validate-phase.md +167 -0
- package/get-shit-done/workflows/verify-phase.md +10 -9
- package/get-shit-done/workflows/verify-work.md +18 -4
- package/package.json +1 -1
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { normalizePhaseName, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, output, error } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, toPosixPath, output, error } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
|
+
const { writeStateMd } = require('./state.cjs');
|
|
9
10
|
|
|
10
11
|
function cmdPhasesList(cwd, options, raw) {
|
|
11
12
|
const phasesDir = path.join(cwd, '.planning', 'phases');
|
|
@@ -34,12 +35,8 @@ function cmdPhasesList(cwd, options, raw) {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
// Sort numerically (handles
|
|
38
|
-
dirs.sort((a, b) =>
|
|
39
|
-
const aNum = parseFloat(a.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
|
|
40
|
-
const bNum = parseFloat(b.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
|
|
41
|
-
return aNum - bNum;
|
|
42
|
-
});
|
|
38
|
+
// Sort numerically (handles integers, decimals, letter-suffix, hybrids)
|
|
39
|
+
dirs.sort((a, b) => comparePhaseNum(a, b));
|
|
43
40
|
|
|
44
41
|
// If filtering by phase number
|
|
45
42
|
if (phase) {
|
|
@@ -74,7 +71,7 @@ function cmdPhasesList(cwd, options, raw) {
|
|
|
74
71
|
const result = {
|
|
75
72
|
files,
|
|
76
73
|
count: files.length,
|
|
77
|
-
phase_dir: phase ? dirs[0].replace(/^\d+(?:\.\d+)
|
|
74
|
+
phase_dir: phase ? dirs[0].replace(/^\d+(?:\.\d+)*-?/, '') : null,
|
|
78
75
|
};
|
|
79
76
|
output(result, raw, files.join('\n'));
|
|
80
77
|
return;
|
|
@@ -125,11 +122,7 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
|
|
|
125
122
|
}
|
|
126
123
|
|
|
127
124
|
// Sort numerically
|
|
128
|
-
existingDecimals.sort((a, b) =>
|
|
129
|
-
const aNum = parseFloat(a);
|
|
130
|
-
const bNum = parseFloat(b);
|
|
131
|
-
return aNum - bNum;
|
|
132
|
-
});
|
|
125
|
+
existingDecimals.sort((a, b) => comparePhaseNum(a, b));
|
|
133
126
|
|
|
134
127
|
// Calculate next decimal
|
|
135
128
|
let nextDecimal;
|
|
@@ -168,7 +161,7 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
168
161
|
|
|
169
162
|
try {
|
|
170
163
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
171
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
164
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
172
165
|
|
|
173
166
|
const match = dirs.find(d => d.startsWith(normalized));
|
|
174
167
|
if (!match) {
|
|
@@ -176,7 +169,7 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
176
169
|
return;
|
|
177
170
|
}
|
|
178
171
|
|
|
179
|
-
const dirMatch = match.match(/^(\d+(?:\.\d+)
|
|
172
|
+
const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
180
173
|
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
|
181
174
|
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
182
175
|
|
|
@@ -187,7 +180,7 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
187
180
|
|
|
188
181
|
const result = {
|
|
189
182
|
found: true,
|
|
190
|
-
directory: path.join('.planning', 'phases', match),
|
|
183
|
+
directory: toPosixPath(path.join('.planning', 'phases', match)),
|
|
191
184
|
phase_number: phaseNumber,
|
|
192
185
|
phase_name: phaseName,
|
|
193
186
|
plans,
|
|
@@ -200,6 +193,11 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
200
193
|
}
|
|
201
194
|
}
|
|
202
195
|
|
|
196
|
+
function extractObjective(content) {
|
|
197
|
+
const m = content.match(/<objective>\s*\n?\s*(.+)/);
|
|
198
|
+
return m ? m[1].trim() : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
203
201
|
function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
204
202
|
if (!phase) {
|
|
205
203
|
error('phase required for phase-plan-index');
|
|
@@ -213,7 +211,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
213
211
|
let phaseDirName = null;
|
|
214
212
|
try {
|
|
215
213
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
216
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
214
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
217
215
|
const match = dirs.find(d => d.startsWith(normalized));
|
|
218
216
|
if (match) {
|
|
219
217
|
phaseDir = path.join(phasesDir, match);
|
|
@@ -249,9 +247,10 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
249
247
|
const content = fs.readFileSync(planPath, 'utf-8');
|
|
250
248
|
const fm = extractFrontmatter(content);
|
|
251
249
|
|
|
252
|
-
// Count tasks (## task N
|
|
253
|
-
const
|
|
254
|
-
const
|
|
250
|
+
// Count tasks: XML <task> tags (canonical) or ## task N markdown (legacy)
|
|
251
|
+
const xmlTasks = content.match(/<task[\s>]/gi) || [];
|
|
252
|
+
const mdTasks = content.match(/##\s*task\s*\d+/gi) || [];
|
|
253
|
+
const taskCount = xmlTasks.length || mdTasks.length;
|
|
255
254
|
|
|
256
255
|
// Parse wave as integer
|
|
257
256
|
const wave = parseInt(fm.wave, 10) || 1;
|
|
@@ -266,10 +265,11 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
266
265
|
hasCheckpoints = true;
|
|
267
266
|
}
|
|
268
267
|
|
|
269
|
-
// Parse
|
|
268
|
+
// Parse files_modified (underscore is canonical; also accept hyphenated for compat)
|
|
270
269
|
let filesModified = [];
|
|
271
|
-
|
|
272
|
-
|
|
270
|
+
const fmFiles = fm['files_modified'] || fm['files-modified'];
|
|
271
|
+
if (fmFiles) {
|
|
272
|
+
filesModified = Array.isArray(fmFiles) ? fmFiles : [fmFiles];
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
const hasSummary = completedPlanIds.has(planId);
|
|
@@ -281,7 +281,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
281
281
|
id: planId,
|
|
282
282
|
wave,
|
|
283
283
|
autonomous,
|
|
284
|
-
objective: fm.objective || null,
|
|
284
|
+
objective: extractObjective(content) || fm.objective || null,
|
|
285
285
|
files_modified: filesModified,
|
|
286
286
|
task_count: taskCount,
|
|
287
287
|
has_summary: hasSummary,
|
|
@@ -322,7 +322,7 @@ function cmdPhaseAdd(cwd, description, raw) {
|
|
|
322
322
|
const slug = generateSlugInternal(description);
|
|
323
323
|
|
|
324
324
|
// Find highest integer phase number
|
|
325
|
-
const phasePattern = /#{2,4}\s*Phase\s+(\d+)(?:\.\d+)
|
|
325
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
|
|
326
326
|
let maxPhase = 0;
|
|
327
327
|
let m;
|
|
328
328
|
while ((m = phasePattern.exec(content)) !== null) {
|
|
@@ -340,7 +340,7 @@ function cmdPhaseAdd(cwd, description, raw) {
|
|
|
340
340
|
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
|
|
341
341
|
|
|
342
342
|
// Build phase entry
|
|
343
|
-
const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${newPhaseNum} to break down)\n`;
|
|
343
|
+
const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${newPhaseNum} to break down)\n`;
|
|
344
344
|
|
|
345
345
|
// Find insertion point: before last "---" or at end
|
|
346
346
|
let updatedContent;
|
|
@@ -411,7 +411,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
411
411
|
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
|
|
412
412
|
|
|
413
413
|
// Build phase entry
|
|
414
|
-
const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${decimalPhase} to break down)\n`;
|
|
414
|
+
const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${decimalPhase} to break down)\n`;
|
|
415
415
|
|
|
416
416
|
// Insert after the target phase section
|
|
417
417
|
const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
|
|
@@ -466,7 +466,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
466
466
|
let targetDir = null;
|
|
467
467
|
try {
|
|
468
468
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
469
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
469
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
470
470
|
targetDir = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
|
|
471
471
|
} catch {}
|
|
472
472
|
|
|
@@ -497,7 +497,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
497
497
|
|
|
498
498
|
try {
|
|
499
499
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
500
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
500
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
501
501
|
|
|
502
502
|
// Find sibling decimals with higher numbers
|
|
503
503
|
const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
|
|
@@ -544,20 +544,21 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
544
544
|
|
|
545
545
|
try {
|
|
546
546
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
547
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
547
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
548
548
|
|
|
549
|
-
// Collect directories that need renumbering (integer phases > removed, and their decimals)
|
|
549
|
+
// Collect directories that need renumbering (integer phases > removed, and their decimals/letters)
|
|
550
550
|
const toRename = [];
|
|
551
551
|
for (const dir of dirs) {
|
|
552
|
-
const dm = dir.match(/^(\d+)(?:\.(\d+))?-(.+)$/);
|
|
552
|
+
const dm = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
|
|
553
553
|
if (!dm) continue;
|
|
554
554
|
const dirInt = parseInt(dm[1], 10);
|
|
555
555
|
if (dirInt > removedInt) {
|
|
556
556
|
toRename.push({
|
|
557
557
|
dir,
|
|
558
558
|
oldInt: dirInt,
|
|
559
|
-
|
|
560
|
-
|
|
559
|
+
letter: dm[2] ? dm[2].toUpperCase() : '',
|
|
560
|
+
decimal: dm[3] ? parseInt(dm[3], 10) : null,
|
|
561
|
+
slug: dm[4],
|
|
561
562
|
});
|
|
562
563
|
}
|
|
563
564
|
}
|
|
@@ -572,9 +573,10 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
572
573
|
const newInt = item.oldInt - 1;
|
|
573
574
|
const newPadded = String(newInt).padStart(2, '0');
|
|
574
575
|
const oldPadded = String(item.oldInt).padStart(2, '0');
|
|
576
|
+
const letterSuffix = item.letter || '';
|
|
575
577
|
const decimalSuffix = item.decimal !== null ? `.${item.decimal}` : '';
|
|
576
|
-
const oldPrefix = `${oldPadded}${decimalSuffix}`;
|
|
577
|
-
const newPrefix = `${newPadded}${decimalSuffix}`;
|
|
578
|
+
const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
|
|
579
|
+
const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
|
|
578
580
|
const newDirName = `${newPrefix}-${item.slug}`;
|
|
579
581
|
|
|
580
582
|
// Rename directory
|
|
@@ -601,7 +603,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
601
603
|
let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
602
604
|
|
|
603
605
|
// Remove the target phase section
|
|
604
|
-
const targetEscaped = targetPhase
|
|
606
|
+
const targetEscaped = escapeRegex(targetPhase);
|
|
605
607
|
const sectionPattern = new RegExp(
|
|
606
608
|
`\\n?#{2,4}\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`,
|
|
607
609
|
'i'
|
|
@@ -681,7 +683,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
681
683
|
const oldTotal = parseInt(ofMatch[2], 10);
|
|
682
684
|
stateContent = stateContent.replace(ofPattern, `$1${oldTotal - 1}$3`);
|
|
683
685
|
}
|
|
684
|
-
|
|
686
|
+
writeStateMd(statePath, stateContent, cwd);
|
|
685
687
|
}
|
|
686
688
|
|
|
687
689
|
const result = {
|
|
@@ -722,13 +724,13 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
722
724
|
|
|
723
725
|
// Checkbox: - [ ] Phase N: → - [x] Phase N: (...completed DATE)
|
|
724
726
|
const checkboxPattern = new RegExp(
|
|
725
|
-
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseNum
|
|
727
|
+
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
|
|
726
728
|
'i'
|
|
727
729
|
);
|
|
728
730
|
roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
|
|
729
731
|
|
|
730
732
|
// Progress table: update Status to Complete, add date
|
|
731
|
-
const phaseEscaped = phaseNum
|
|
733
|
+
const phaseEscaped = escapeRegex(phaseNum);
|
|
732
734
|
const tablePattern = new RegExp(
|
|
733
735
|
`(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
|
|
734
736
|
'i'
|
|
@@ -755,7 +757,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
755
757
|
if (fs.existsSync(reqPath)) {
|
|
756
758
|
// Extract Requirements line from roadmap for this phase
|
|
757
759
|
const reqMatch = roadmapContent.match(
|
|
758
|
-
new RegExp(`Phase\\s+${phaseNum
|
|
760
|
+
new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
|
|
759
761
|
);
|
|
760
762
|
|
|
761
763
|
if (reqMatch) {
|
|
@@ -763,14 +765,15 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
763
765
|
let reqContent = fs.readFileSync(reqPath, 'utf-8');
|
|
764
766
|
|
|
765
767
|
for (const reqId of reqIds) {
|
|
768
|
+
const reqEscaped = escapeRegex(reqId);
|
|
766
769
|
// Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
|
|
767
770
|
reqContent = reqContent.replace(
|
|
768
|
-
new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${
|
|
771
|
+
new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
|
|
769
772
|
'$1x$2'
|
|
770
773
|
);
|
|
771
774
|
// Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete |
|
|
772
775
|
reqContent = reqContent.replace(
|
|
773
|
-
new RegExp(`(\\|\\s*${
|
|
776
|
+
new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
|
|
774
777
|
'$1 Complete $2'
|
|
775
778
|
);
|
|
776
779
|
}
|
|
@@ -780,22 +783,25 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
780
783
|
}
|
|
781
784
|
}
|
|
782
785
|
|
|
783
|
-
// Find next phase
|
|
786
|
+
// Find next phase — check both filesystem AND roadmap
|
|
787
|
+
// Phases may be defined in ROADMAP.md but not yet scaffolded to disk,
|
|
788
|
+
// so a filesystem-only scan would incorrectly report is_last_phase:true
|
|
784
789
|
let nextPhaseNum = null;
|
|
785
790
|
let nextPhaseName = null;
|
|
786
791
|
let isLastPhase = true;
|
|
787
792
|
|
|
788
793
|
try {
|
|
794
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
789
795
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
790
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
791
|
-
|
|
796
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
797
|
+
.filter(isDirInMilestone)
|
|
798
|
+
.sort((a, b) => comparePhaseNum(a, b));
|
|
792
799
|
|
|
793
800
|
// Find the next phase directory after current
|
|
794
801
|
for (const dir of dirs) {
|
|
795
|
-
const dm = dir.match(/^(\d+(?:\.\d+)
|
|
802
|
+
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
796
803
|
if (dm) {
|
|
797
|
-
|
|
798
|
-
if (dirFloat > currentFloat) {
|
|
804
|
+
if (comparePhaseNum(dm[1], phaseNum) > 0) {
|
|
799
805
|
nextPhaseNum = dm[1];
|
|
800
806
|
nextPhaseName = dm[2] || null;
|
|
801
807
|
isLastPhase = false;
|
|
@@ -805,6 +811,24 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
805
811
|
}
|
|
806
812
|
} catch {}
|
|
807
813
|
|
|
814
|
+
// Fallback: if filesystem found no next phase, check ROADMAP.md
|
|
815
|
+
// for phases that are defined but not yet planned (no directory on disk)
|
|
816
|
+
if (isLastPhase && fs.existsSync(roadmapPath)) {
|
|
817
|
+
try {
|
|
818
|
+
const roadmapForPhases = fs.readFileSync(roadmapPath, 'utf-8');
|
|
819
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
820
|
+
let pm;
|
|
821
|
+
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
|
|
822
|
+
if (comparePhaseNum(pm[1], phaseNum) > 0) {
|
|
823
|
+
nextPhaseNum = pm[1];
|
|
824
|
+
nextPhaseName = pm[2].replace(/\(INSERTED\)/i, '').trim().toLowerCase().replace(/\s+/g, '-');
|
|
825
|
+
isLastPhase = false;
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
} catch {}
|
|
830
|
+
}
|
|
831
|
+
|
|
808
832
|
// Update STATE.md
|
|
809
833
|
if (fs.existsSync(statePath)) {
|
|
810
834
|
let stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
@@ -847,7 +871,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
847
871
|
`$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`
|
|
848
872
|
);
|
|
849
873
|
|
|
850
|
-
|
|
874
|
+
writeStateMd(statePath, stateContent, cwd);
|
|
851
875
|
}
|
|
852
876
|
|
|
853
877
|
const result = {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs');
|
|
8
8
|
|
|
9
9
|
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
10
10
|
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
|
|
@@ -18,7 +18,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
|
18
18
|
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
19
19
|
|
|
20
20
|
// Escape special regex chars in phase number, handle decimal
|
|
21
|
-
const escapedPhase = phaseNum
|
|
21
|
+
const escapedPhase = escapeRegex(phaseNum);
|
|
22
22
|
|
|
23
23
|
// Match "## Phase X:", "### Phase X:", or "#### Phase X:" with optional name
|
|
24
24
|
const phasePattern = new RegExp(
|
|
@@ -102,7 +102,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
102
102
|
const phasesDir = path.join(cwd, '.planning', 'phases');
|
|
103
103
|
|
|
104
104
|
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name
|
|
105
|
-
const phasePattern = /#{2,4}\s*Phase\s+(\d+(?:\.\d+)
|
|
105
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
106
106
|
const phases = [];
|
|
107
107
|
let match;
|
|
108
108
|
|
|
@@ -153,7 +153,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
153
153
|
} catch {}
|
|
154
154
|
|
|
155
155
|
// Check ROADMAP checkbox status
|
|
156
|
-
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum
|
|
156
|
+
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}`, 'i');
|
|
157
157
|
const checkboxMatch = content.match(checkboxPattern);
|
|
158
158
|
const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
|
|
159
159
|
|
|
@@ -192,7 +192,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
192
192
|
const completedPhases = phases.filter(p => p.disk_status === 'complete').length;
|
|
193
193
|
|
|
194
194
|
// Detect phases in summary list without detail sections (malformed ROADMAP)
|
|
195
|
-
const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+(?:\.\d+)
|
|
195
|
+
const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:\.\d+)*)/gi;
|
|
196
196
|
const checklistPhases = new Set();
|
|
197
197
|
let checklistMatch;
|
|
198
198
|
while ((checklistMatch = checklistPattern.exec(content)) !== null) {
|
|
@@ -208,7 +208,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
|
|
208
208
|
completed_phases: completedPhases,
|
|
209
209
|
total_plans: totalPlans,
|
|
210
210
|
total_summaries: totalSummaries,
|
|
211
|
-
progress_percent: totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0,
|
|
211
|
+
progress_percent: totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0,
|
|
212
212
|
current_phase: currentPhase ? currentPhase.number : null,
|
|
213
213
|
next_phase: nextPhase ? nextPhase.number : null,
|
|
214
214
|
missing_phase_details: missingDetails.length > 0 ? missingDetails : null,
|
|
@@ -247,7 +247,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
250
|
-
const phaseEscaped = phaseNum
|
|
250
|
+
const phaseEscaped = escapeRegex(phaseNum);
|
|
251
251
|
|
|
252
252
|
// Progress table row: update Plans column (summaries/plans) and Status column
|
|
253
253
|
const tablePattern = new RegExp(
|