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
|
@@ -6,6 +6,13 @@ const fs = require('fs');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { execSync } = require('child_process');
|
|
8
8
|
|
|
9
|
+
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/** Normalize a relative path to always use forward slashes (cross-platform). */
|
|
12
|
+
function toPosixPath(p) {
|
|
13
|
+
return p.split(path.sep).join('/');
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
// ─── Model Profile Table ─────────────────────────────────────────────────────
|
|
10
17
|
|
|
11
18
|
const MODEL_PROFILES = {
|
|
@@ -20,6 +27,7 @@ const MODEL_PROFILES = {
|
|
|
20
27
|
'gsd-verifier': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
21
28
|
'gsd-plan-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
22
29
|
'gsd-integration-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
30
|
+
'gsd-nyquist-auditor': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
23
31
|
};
|
|
24
32
|
|
|
25
33
|
// ─── Output helpers ───────────────────────────────────────────────────────────
|
|
@@ -69,6 +77,7 @@ function loadConfig(cwd) {
|
|
|
69
77
|
research: true,
|
|
70
78
|
plan_checker: true,
|
|
71
79
|
verifier: true,
|
|
80
|
+
nyquist_validation: true,
|
|
72
81
|
parallelization: true,
|
|
73
82
|
brave_search: false,
|
|
74
83
|
};
|
|
@@ -77,6 +86,14 @@ function loadConfig(cwd) {
|
|
|
77
86
|
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
78
87
|
const parsed = JSON.parse(raw);
|
|
79
88
|
|
|
89
|
+
// Migrate deprecated "depth" key to "granularity" with value mapping
|
|
90
|
+
if ('depth' in parsed && !('granularity' in parsed)) {
|
|
91
|
+
const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
|
|
92
|
+
parsed.granularity = depthToGranularity[parsed.depth] || parsed.depth;
|
|
93
|
+
delete parsed.depth;
|
|
94
|
+
try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch {}
|
|
95
|
+
}
|
|
96
|
+
|
|
80
97
|
const get = (key, nested) => {
|
|
81
98
|
if (parsed[key] !== undefined) return parsed[key];
|
|
82
99
|
if (nested && parsed[nested.section] && parsed[nested.section][nested.field] !== undefined) {
|
|
@@ -102,8 +119,10 @@ function loadConfig(cwd) {
|
|
|
102
119
|
research: get('research', { section: 'workflow', field: 'research' }) ?? defaults.research,
|
|
103
120
|
plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
|
|
104
121
|
verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
|
|
122
|
+
nyquist_validation: get('nyquist_validation', { section: 'workflow', field: 'nyquist_validation' }) ?? defaults.nyquist_validation,
|
|
105
123
|
parallelization,
|
|
106
124
|
brave_search: get('brave_search') ?? defaults.brave_search,
|
|
125
|
+
model_overrides: parsed.model_overrides || null,
|
|
107
126
|
};
|
|
108
127
|
} catch {
|
|
109
128
|
return defaults;
|
|
@@ -114,7 +133,11 @@ function loadConfig(cwd) {
|
|
|
114
133
|
|
|
115
134
|
function isGitIgnored(cwd, targetPath) {
|
|
116
135
|
try {
|
|
117
|
-
|
|
136
|
+
// --no-index checks .gitignore rules regardless of whether the file is tracked.
|
|
137
|
+
// Without it, git check-ignore returns "not ignored" for tracked files even when
|
|
138
|
+
// .gitignore explicitly lists them — a common source of confusion when .planning/
|
|
139
|
+
// was committed before being added to .gitignore.
|
|
140
|
+
execSync('git check-ignore -q --no-index -- ' + targetPath.replace(/[^a-zA-Z0-9._\-/]/g, ''), {
|
|
118
141
|
cwd,
|
|
119
142
|
stdio: 'pipe',
|
|
120
143
|
});
|
|
@@ -147,23 +170,55 @@ function execGit(cwd, args) {
|
|
|
147
170
|
|
|
148
171
|
// ─── Phase utilities ──────────────────────────────────────────────────────────
|
|
149
172
|
|
|
173
|
+
function escapeRegex(value) {
|
|
174
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
175
|
+
}
|
|
176
|
+
|
|
150
177
|
function normalizePhaseName(phase) {
|
|
151
|
-
const match = phase.match(/^(\d+(?:\.\d+)
|
|
178
|
+
const match = String(phase).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
152
179
|
if (!match) return phase;
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
return
|
|
180
|
+
const padded = match[1].padStart(2, '0');
|
|
181
|
+
const letter = match[2] ? match[2].toUpperCase() : '';
|
|
182
|
+
const decimal = match[3] || '';
|
|
183
|
+
return padded + letter + decimal;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function comparePhaseNum(a, b) {
|
|
187
|
+
const pa = String(a).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
188
|
+
const pb = String(b).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
|
|
189
|
+
if (!pa || !pb) return String(a).localeCompare(String(b));
|
|
190
|
+
const intDiff = parseInt(pa[1], 10) - parseInt(pb[1], 10);
|
|
191
|
+
if (intDiff !== 0) return intDiff;
|
|
192
|
+
// No letter sorts before letter: 12 < 12A < 12B
|
|
193
|
+
const la = (pa[2] || '').toUpperCase();
|
|
194
|
+
const lb = (pb[2] || '').toUpperCase();
|
|
195
|
+
if (la !== lb) {
|
|
196
|
+
if (!la) return -1;
|
|
197
|
+
if (!lb) return 1;
|
|
198
|
+
return la < lb ? -1 : 1;
|
|
199
|
+
}
|
|
200
|
+
// Segment-by-segment decimal comparison: 12A < 12A.1 < 12A.1.2 < 12A.2
|
|
201
|
+
const aDecParts = pa[3] ? pa[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
|
|
202
|
+
const bDecParts = pb[3] ? pb[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
|
|
203
|
+
const maxLen = Math.max(aDecParts.length, bDecParts.length);
|
|
204
|
+
if (aDecParts.length === 0 && bDecParts.length > 0) return -1;
|
|
205
|
+
if (bDecParts.length === 0 && aDecParts.length > 0) return 1;
|
|
206
|
+
for (let i = 0; i < maxLen; i++) {
|
|
207
|
+
const av = Number.isFinite(aDecParts[i]) ? aDecParts[i] : 0;
|
|
208
|
+
const bv = Number.isFinite(bDecParts[i]) ? bDecParts[i] : 0;
|
|
209
|
+
if (av !== bv) return av - bv;
|
|
210
|
+
}
|
|
211
|
+
return 0;
|
|
157
212
|
}
|
|
158
213
|
|
|
159
214
|
function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
160
215
|
try {
|
|
161
216
|
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
162
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
217
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
163
218
|
const match = dirs.find(d => d.startsWith(normalized));
|
|
164
219
|
if (!match) return null;
|
|
165
220
|
|
|
166
|
-
const dirMatch = match.match(/^(\d+(?:\.\d+)
|
|
221
|
+
const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
167
222
|
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
|
168
223
|
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
169
224
|
const phaseDir = path.join(baseDir, match);
|
|
@@ -185,7 +240,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
|
185
240
|
|
|
186
241
|
return {
|
|
187
242
|
found: true,
|
|
188
|
-
directory: path.join(relBase, match),
|
|
243
|
+
directory: toPosixPath(path.join(relBase, match)),
|
|
189
244
|
phase_number: phaseNumber,
|
|
190
245
|
phase_name: phaseName,
|
|
191
246
|
phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
|
|
@@ -208,7 +263,7 @@ function findPhaseInternal(cwd, phase) {
|
|
|
208
263
|
const normalized = normalizePhaseName(phase);
|
|
209
264
|
|
|
210
265
|
// Search current phases first
|
|
211
|
-
const current = searchPhaseInDir(phasesDir,
|
|
266
|
+
const current = searchPhaseInDir(phasesDir, '.planning/phases', normalized);
|
|
212
267
|
if (current) return current;
|
|
213
268
|
|
|
214
269
|
// Search archived milestone phases (newest first)
|
|
@@ -226,7 +281,7 @@ function findPhaseInternal(cwd, phase) {
|
|
|
226
281
|
for (const archiveName of archiveDirs) {
|
|
227
282
|
const version = archiveName.match(/^(v[\d.]+)-phases$/)[1];
|
|
228
283
|
const archivePath = path.join(milestonesDir, archiveName);
|
|
229
|
-
const relBase =
|
|
284
|
+
const relBase = '.planning/milestones/' + archiveName;
|
|
230
285
|
const result = searchPhaseInDir(archivePath, relBase, normalized);
|
|
231
286
|
if (result) {
|
|
232
287
|
result.archived = version;
|
|
@@ -257,7 +312,7 @@ function getArchivedPhaseDirs(cwd) {
|
|
|
257
312
|
const version = archiveName.match(/^(v[\d.]+)-phases$/)[1];
|
|
258
313
|
const archivePath = path.join(milestonesDir, archiveName);
|
|
259
314
|
const entries = fs.readdirSync(archivePath, { withFileTypes: true });
|
|
260
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
315
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
261
316
|
|
|
262
317
|
for (const dir of dirs) {
|
|
263
318
|
results.push({
|
|
@@ -282,7 +337,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
|
|
|
282
337
|
|
|
283
338
|
try {
|
|
284
339
|
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
285
|
-
const escapedPhase = phaseNum.toString()
|
|
340
|
+
const escapedPhase = escapeRegex(phaseNum.toString());
|
|
286
341
|
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i');
|
|
287
342
|
const headerMatch = content.match(phasePattern);
|
|
288
343
|
if (!headerMatch) return null;
|
|
@@ -346,17 +401,73 @@ function generateSlugInternal(text) {
|
|
|
346
401
|
function getMilestoneInfo(cwd) {
|
|
347
402
|
try {
|
|
348
403
|
const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
349
|
-
|
|
350
|
-
|
|
404
|
+
|
|
405
|
+
// First: check for list-format roadmaps using 🚧 (in-progress) marker
|
|
406
|
+
// e.g. "- 🚧 **v2.1 Belgium** — Phases 24-28 (in progress)"
|
|
407
|
+
const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+\.\d+)\s+([^*]+)\*\*/);
|
|
408
|
+
if (inProgressMatch) {
|
|
409
|
+
return {
|
|
410
|
+
version: 'v' + inProgressMatch[1],
|
|
411
|
+
name: inProgressMatch[2].trim(),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Second: heading-format roadmaps — strip shipped milestones in <details> blocks
|
|
416
|
+
const cleaned = roadmap.replace(/<details>[\s\S]*?<\/details>/gi, '');
|
|
417
|
+
// Extract version and name from the same ## heading for consistency
|
|
418
|
+
const headingMatch = cleaned.match(/## .*v(\d+\.\d+)[:\s]+([^\n(]+)/);
|
|
419
|
+
if (headingMatch) {
|
|
420
|
+
return {
|
|
421
|
+
version: 'v' + headingMatch[1],
|
|
422
|
+
name: headingMatch[2].trim(),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
// Fallback: try bare version match
|
|
426
|
+
const versionMatch = cleaned.match(/v(\d+\.\d+)/);
|
|
351
427
|
return {
|
|
352
428
|
version: versionMatch ? versionMatch[0] : 'v1.0',
|
|
353
|
-
name:
|
|
429
|
+
name: 'milestone',
|
|
354
430
|
};
|
|
355
431
|
} catch {
|
|
356
432
|
return { version: 'v1.0', name: 'milestone' };
|
|
357
433
|
}
|
|
358
434
|
}
|
|
359
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Returns a filter function that checks whether a phase directory belongs
|
|
438
|
+
* to the current milestone based on ROADMAP.md phase headings.
|
|
439
|
+
* If no ROADMAP exists or no phases are listed, returns a pass-all filter.
|
|
440
|
+
*/
|
|
441
|
+
function getMilestonePhaseFilter(cwd) {
|
|
442
|
+
const milestonePhaseNums = new Set();
|
|
443
|
+
try {
|
|
444
|
+
const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
445
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
|
|
446
|
+
let m;
|
|
447
|
+
while ((m = phasePattern.exec(roadmap)) !== null) {
|
|
448
|
+
milestonePhaseNums.add(m[1]);
|
|
449
|
+
}
|
|
450
|
+
} catch {}
|
|
451
|
+
|
|
452
|
+
if (milestonePhaseNums.size === 0) {
|
|
453
|
+
const passAll = () => true;
|
|
454
|
+
passAll.phaseCount = 0;
|
|
455
|
+
return passAll;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const normalized = new Set(
|
|
459
|
+
[...milestonePhaseNums].map(n => (n.replace(/^0+/, '') || '0').toLowerCase())
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
function isDirInMilestone(dirName) {
|
|
463
|
+
const m = dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/);
|
|
464
|
+
if (!m) return false;
|
|
465
|
+
return normalized.has(m[1].toLowerCase());
|
|
466
|
+
}
|
|
467
|
+
isDirInMilestone.phaseCount = milestonePhaseNums.size;
|
|
468
|
+
return isDirInMilestone;
|
|
469
|
+
}
|
|
470
|
+
|
|
360
471
|
module.exports = {
|
|
361
472
|
MODEL_PROFILES,
|
|
362
473
|
output,
|
|
@@ -365,7 +476,9 @@ module.exports = {
|
|
|
365
476
|
loadConfig,
|
|
366
477
|
isGitIgnored,
|
|
367
478
|
execGit,
|
|
479
|
+
escapeRegex,
|
|
368
480
|
normalizePhaseName,
|
|
481
|
+
comparePhaseNum,
|
|
369
482
|
searchPhaseInDir,
|
|
370
483
|
findPhaseInternal,
|
|
371
484
|
getArchivedPhaseDirs,
|
|
@@ -374,4 +487,6 @@ module.exports = {
|
|
|
374
487
|
pathExistsInternal,
|
|
375
488
|
generateSlugInternal,
|
|
376
489
|
getMilestoneInfo,
|
|
490
|
+
getMilestonePhaseFilter,
|
|
491
|
+
toPosixPath,
|
|
377
492
|
};
|
|
@@ -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
|
|