gsd-opencode 1.20.4 → 1.22.1
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 +10 -1
- package/agents/gsd-debugger.md +67 -10
- package/agents/gsd-executor.md +37 -16
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-nyquist-auditor.md +179 -0
- package/agents/gsd-phase-researcher.md +29 -34
- package/agents/gsd-plan-checker.md +43 -78
- package/agents/gsd-planner.md +140 -24
- package/agents/gsd-project-researcher.md +12 -1
- package/agents/gsd-research-synthesizer.md +14 -3
- package/agents/gsd-roadmapper.md +26 -15
- package/agents/gsd-verifier.md +30 -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 +20 -9
- 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-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/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/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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { execSync } = require('child_process');
|
|
8
|
-
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, output, error } = require('./core.cjs');
|
|
8
|
+
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs');
|
|
9
9
|
|
|
10
10
|
function cmdInitExecutePhase(cwd, phase, raw) {
|
|
11
11
|
if (!phase) {
|
|
@@ -16,6 +16,13 @@ function cmdInitExecutePhase(cwd, phase, raw) {
|
|
|
16
16
|
const phaseInfo = findPhaseInternal(cwd, phase);
|
|
17
17
|
const milestone = getMilestoneInfo(cwd);
|
|
18
18
|
|
|
19
|
+
const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
|
|
20
|
+
const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
|
|
21
|
+
const reqExtracted = reqMatch
|
|
22
|
+
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
|
|
23
|
+
: null;
|
|
24
|
+
const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
|
|
25
|
+
|
|
19
26
|
const result = {
|
|
20
27
|
// Models
|
|
21
28
|
executor_model: resolveModelInternal(cwd, 'gsd-executor'),
|
|
@@ -35,6 +42,7 @@ function cmdInitExecutePhase(cwd, phase, raw) {
|
|
|
35
42
|
phase_number: phaseInfo?.phase_number || null,
|
|
36
43
|
phase_name: phaseInfo?.phase_name || null,
|
|
37
44
|
phase_slug: phaseInfo?.phase_slug || null,
|
|
45
|
+
phase_req_ids,
|
|
38
46
|
|
|
39
47
|
// Plan inventory
|
|
40
48
|
plans: phaseInfo?.plans || [],
|
|
@@ -80,6 +88,13 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
|
|
80
88
|
const config = loadConfig(cwd);
|
|
81
89
|
const phaseInfo = findPhaseInternal(cwd, phase);
|
|
82
90
|
|
|
91
|
+
const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
|
|
92
|
+
const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
|
|
93
|
+
const reqExtracted = reqMatch
|
|
94
|
+
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
|
|
95
|
+
: null;
|
|
96
|
+
const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
|
|
97
|
+
|
|
83
98
|
const result = {
|
|
84
99
|
// Models
|
|
85
100
|
researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
|
|
@@ -99,6 +114,7 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
|
|
99
114
|
phase_name: phaseInfo?.phase_name || null,
|
|
100
115
|
phase_slug: phaseInfo?.phase_slug || null,
|
|
101
116
|
padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null,
|
|
117
|
+
phase_req_ids,
|
|
102
118
|
|
|
103
119
|
// Existing artifacts
|
|
104
120
|
has_research: phaseInfo?.has_research || false,
|
|
@@ -123,19 +139,19 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
|
|
123
139
|
const files = fs.readdirSync(phaseDirFull);
|
|
124
140
|
const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
|
125
141
|
if (contextFile) {
|
|
126
|
-
result.context_path = path.join(phaseInfo.directory, contextFile);
|
|
142
|
+
result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
|
|
127
143
|
}
|
|
128
144
|
const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
129
145
|
if (researchFile) {
|
|
130
|
-
result.research_path = path.join(phaseInfo.directory, researchFile);
|
|
146
|
+
result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
|
|
131
147
|
}
|
|
132
148
|
const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
|
|
133
149
|
if (verificationFile) {
|
|
134
|
-
result.verification_path = path.join(phaseInfo.directory, verificationFile);
|
|
150
|
+
result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
|
|
135
151
|
}
|
|
136
152
|
const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
|
|
137
153
|
if (uatFile) {
|
|
138
|
-
result.uat_path = path.join(phaseInfo.directory, uatFile);
|
|
154
|
+
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
|
|
139
155
|
}
|
|
140
156
|
} catch {}
|
|
141
157
|
}
|
|
@@ -406,19 +422,19 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
|
|
406
422
|
const files = fs.readdirSync(phaseDirFull);
|
|
407
423
|
const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
|
408
424
|
if (contextFile) {
|
|
409
|
-
result.context_path = path.join(phaseInfo.directory, contextFile);
|
|
425
|
+
result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
|
|
410
426
|
}
|
|
411
427
|
const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
412
428
|
if (researchFile) {
|
|
413
|
-
result.research_path = path.join(phaseInfo.directory, researchFile);
|
|
429
|
+
result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
|
|
414
430
|
}
|
|
415
431
|
const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
|
|
416
432
|
if (verificationFile) {
|
|
417
|
-
result.verification_path = path.join(phaseInfo.directory, verificationFile);
|
|
433
|
+
result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
|
|
418
434
|
}
|
|
419
435
|
const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
|
|
420
436
|
if (uatFile) {
|
|
421
|
-
result.uat_path = path.join(phaseInfo.directory, uatFile);
|
|
437
|
+
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
|
|
422
438
|
}
|
|
423
439
|
} catch {}
|
|
424
440
|
}
|
|
@@ -453,7 +469,7 @@ function cmdInitTodos(cwd, area, raw) {
|
|
|
453
469
|
created: createdMatch ? createdMatch[1].trim() : 'unknown',
|
|
454
470
|
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
455
471
|
area: todoArea,
|
|
456
|
-
path:
|
|
472
|
+
path: '.planning/todos/pending/' + file,
|
|
457
473
|
});
|
|
458
474
|
} catch {}
|
|
459
475
|
}
|
|
@@ -595,7 +611,7 @@ function cmdInitProgress(cwd, raw) {
|
|
|
595
611
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
596
612
|
|
|
597
613
|
for (const dir of dirs) {
|
|
598
|
-
const match = dir.match(/^(\d+(?:\.\d+)
|
|
614
|
+
const match = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
|
|
599
615
|
const phaseNumber = match ? match[1] : dir;
|
|
600
616
|
const phaseName = match && match[2] ? match[2] : null;
|
|
601
617
|
|
|
@@ -613,7 +629,7 @@ function cmdInitProgress(cwd, raw) {
|
|
|
613
629
|
const phaseInfo = {
|
|
614
630
|
number: phaseNumber,
|
|
615
631
|
name: phaseName,
|
|
616
|
-
directory:
|
|
632
|
+
directory: '.planning/phases/' + dir,
|
|
617
633
|
status,
|
|
618
634
|
plan_count: plans.length,
|
|
619
635
|
summary_count: summaries.length,
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { output, error } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, getMilestonePhaseFilter, output, error } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
|
+
const { writeStateMd } = require('./state.cjs');
|
|
9
10
|
|
|
10
11
|
function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
|
|
11
12
|
if (!reqIdsRaw || reqIdsRaw.length === 0) {
|
|
@@ -36,20 +37,21 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
|
|
|
36
37
|
|
|
37
38
|
for (const reqId of reqIds) {
|
|
38
39
|
let found = false;
|
|
40
|
+
const reqEscaped = escapeRegex(reqId);
|
|
39
41
|
|
|
40
42
|
// Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
|
|
41
|
-
const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${
|
|
43
|
+
const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi');
|
|
42
44
|
if (checkboxPattern.test(reqContent)) {
|
|
43
45
|
reqContent = reqContent.replace(checkboxPattern, '$1x$2');
|
|
44
46
|
found = true;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
// Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete |
|
|
48
|
-
const tablePattern = new RegExp(`(\\|\\s*${
|
|
50
|
+
const tablePattern = new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi');
|
|
49
51
|
if (tablePattern.test(reqContent)) {
|
|
50
52
|
// Re-read since test() advances lastIndex for global regex
|
|
51
53
|
reqContent = reqContent.replace(
|
|
52
|
-
new RegExp(`(\\|\\s*${
|
|
54
|
+
new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
|
|
53
55
|
'$1 Complete $2'
|
|
54
56
|
);
|
|
55
57
|
found = true;
|
|
@@ -91,7 +93,12 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
91
93
|
// Ensure archive directory exists
|
|
92
94
|
fs.mkdirSync(archiveDir, { recursive: true });
|
|
93
95
|
|
|
94
|
-
//
|
|
96
|
+
// Scope stats and accomplishments to only the phases belonging to the
|
|
97
|
+
// current milestone's ROADMAP. Uses the shared filter from core.cjs
|
|
98
|
+
// (same logic used by cmdPhasesList and other callers).
|
|
99
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
100
|
+
|
|
101
|
+
// Gather stats from phases (scoped to current milestone only)
|
|
95
102
|
let phaseCount = 0;
|
|
96
103
|
let totalPlans = 0;
|
|
97
104
|
let totalTasks = 0;
|
|
@@ -102,6 +109,8 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
102
109
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
103
110
|
|
|
104
111
|
for (const dir of dirs) {
|
|
112
|
+
if (!isDirInMilestone(dir)) continue;
|
|
113
|
+
|
|
105
114
|
phaseCount++;
|
|
106
115
|
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
|
107
116
|
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
|
@@ -149,7 +158,21 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
149
158
|
|
|
150
159
|
if (fs.existsSync(milestonesPath)) {
|
|
151
160
|
const existing = fs.readFileSync(milestonesPath, 'utf-8');
|
|
152
|
-
|
|
161
|
+
if (!existing.trim()) {
|
|
162
|
+
// Empty file — treat like new
|
|
163
|
+
fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
|
|
164
|
+
} else {
|
|
165
|
+
// Insert after the header line(s) for reverse chronological order (newest first)
|
|
166
|
+
const headerMatch = existing.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
|
|
167
|
+
if (headerMatch) {
|
|
168
|
+
const header = headerMatch[1];
|
|
169
|
+
const rest = existing.slice(header.length);
|
|
170
|
+
fs.writeFileSync(milestonesPath, header + milestoneEntry + rest, 'utf-8');
|
|
171
|
+
} else {
|
|
172
|
+
// No recognizable header — prepend the entry
|
|
173
|
+
fs.writeFileSync(milestonesPath, milestoneEntry + existing, 'utf-8');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
153
176
|
} else {
|
|
154
177
|
fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
|
|
155
178
|
}
|
|
@@ -169,7 +192,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
169
192
|
/(\*\*Last Activity Description:\*\*\s*).*/,
|
|
170
193
|
`$1${version} milestone completed and archived`
|
|
171
194
|
);
|
|
172
|
-
|
|
195
|
+
writeStateMd(statePath, stateContent, cwd);
|
|
173
196
|
}
|
|
174
197
|
|
|
175
198
|
// Archive phase directories if requested
|
|
@@ -181,10 +204,13 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
181
204
|
|
|
182
205
|
const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
183
206
|
const phaseDirNames = phaseEntries.filter(e => e.isDirectory()).map(e => e.name);
|
|
207
|
+
let archivedCount = 0;
|
|
184
208
|
for (const dir of phaseDirNames) {
|
|
209
|
+
if (!isDirInMilestone(dir)) continue;
|
|
185
210
|
fs.renameSync(path.join(phasesDir, dir), path.join(phaseArchiveDir, dir));
|
|
211
|
+
archivedCount++;
|
|
186
212
|
}
|
|
187
|
-
phasesArchived =
|
|
213
|
+
phasesArchived = archivedCount > 0;
|
|
188
214
|
} catch {}
|
|
189
215
|
}
|
|
190
216
|
|
|
@@ -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(
|