@undeemed/get-shit-done-codex 1.23.2 → 1.24.2
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/README.md +51 -5
- package/agents/gsd-debugger.md +8 -56
- package/agents/gsd-planner.md +2 -118
- package/agents/gsd-project-researcher.md +0 -3
- package/agents/gsd-research-synthesizer.md +0 -3
- package/bin/install.js +267 -5
- package/commands/gsd/add-phase.md +2 -6
- package/commands/gsd/add-todo.md +1 -6
- package/commands/gsd/check-todos.md +2 -6
- package/commands/gsd/debug.md +1 -6
- package/commands/gsd/discuss-phase.md +16 -9
- package/commands/gsd/execute-phase.md +2 -1
- package/commands/gsd/new-milestone.md +8 -1
- package/commands/gsd/pause-work.md +1 -4
- package/commands/gsd/plan-phase.md +1 -2
- package/commands/gsd/research-phase.md +15 -17
- package/commands/gsd/verify-work.md +2 -1
- package/get-shit-done/bin/gsd-tools.cjs +4951 -121
- package/get-shit-done/bin/lib/commands.cjs +4 -9
- package/get-shit-done/bin/lib/core.cjs +102 -23
- package/get-shit-done/bin/lib/init.cjs +11 -11
- package/get-shit-done/bin/lib/milestone.cjs +54 -3
- package/get-shit-done/bin/lib/phase.cjs +40 -10
- package/get-shit-done/bin/lib/state.cjs +86 -33
- package/get-shit-done/references/checkpoints.md +0 -1
- package/get-shit-done/references/model-profile-resolution.md +13 -6
- package/get-shit-done/references/model-profiles.md +60 -51
- package/get-shit-done/templates/context.md +14 -0
- package/get-shit-done/templates/phase-prompt.md +0 -2
- package/get-shit-done/workflows/audit-milestone.md +8 -63
- package/get-shit-done/workflows/diagnose-issues.md +1 -1
- package/get-shit-done/workflows/execute-phase.md +9 -54
- package/get-shit-done/workflows/execute-plan.md +13 -17
- package/get-shit-done/workflows/help.md +3 -3
- package/get-shit-done/workflows/map-codebase.md +44 -32
- package/get-shit-done/workflows/new-milestone.md +7 -16
- package/get-shit-done/workflows/new-project.md +80 -49
- package/get-shit-done/workflows/progress.md +26 -14
- package/get-shit-done/workflows/quick.md +15 -24
- package/get-shit-done/workflows/set-profile.md +12 -8
- package/get-shit-done/workflows/settings.md +14 -21
- package/get-shit-done/workflows/transition.md +0 -5
- package/get-shit-done/workflows/verify-work.md +12 -11
- package/hooks/dist/gsd-context-monitor.js +1 -1
- package/package.json +3 -2
- package/scripts/run-tests.cjs +43 -0
|
@@ -204,17 +204,12 @@ function cmdResolveModel(cwd, agentType, raw) {
|
|
|
204
204
|
|
|
205
205
|
const config = loadConfig(cwd);
|
|
206
206
|
const profile = config.model_profile || 'balanced';
|
|
207
|
+
const model = resolveModelInternal(cwd, agentType);
|
|
207
208
|
|
|
208
209
|
const agentModels = MODEL_PROFILES[agentType];
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const resolved = agentModels[profile] || agentModels['balanced'] || 'sonnet';
|
|
216
|
-
const model = resolved === 'opus' ? 'inherit' : resolved;
|
|
217
|
-
const result = { model, profile };
|
|
210
|
+
const result = agentModels
|
|
211
|
+
? { model, profile }
|
|
212
|
+
: { model, profile, unknown_agent: true };
|
|
218
213
|
output(result, raw, model);
|
|
219
214
|
}
|
|
220
215
|
|
|
@@ -6,22 +6,32 @@ 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 = {
|
|
12
|
-
|
|
13
|
-
'gsd-
|
|
14
|
-
'gsd-
|
|
15
|
-
'gsd-
|
|
16
|
-
'gsd-
|
|
17
|
-
'gsd-
|
|
18
|
-
'gsd-
|
|
19
|
-
'gsd-
|
|
20
|
-
'gsd-
|
|
21
|
-
'gsd-
|
|
22
|
-
'gsd-
|
|
19
|
+
// quality balanced budget
|
|
20
|
+
'gsd-planner': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'xhigh' }, budget: { m: 'gpt-5.3-codex', t: 'high' } },
|
|
21
|
+
'gsd-roadmapper': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
|
|
22
|
+
'gsd-executor': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
|
|
23
|
+
'gsd-phase-researcher': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
|
|
24
|
+
'gsd-project-researcher': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
|
|
25
|
+
'gsd-research-synthesizer': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
|
|
26
|
+
'gsd-debugger': { quality: { m: 'gpt-5.3-codex', t: 'xhigh' }, balanced: { m: 'gpt-5.3-codex', t: 'xhigh' }, budget: { m: 'gpt-5.3-codex', t: 'high' } },
|
|
27
|
+
'gsd-codebase-mapper': { quality: { m: 'gpt-5.3-codex', t: 'medium' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
|
|
28
|
+
'gsd-verifier': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'high' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
|
|
29
|
+
'gsd-plan-checker': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
|
|
30
|
+
'gsd-integration-checker': { quality: { m: 'gpt-5.3-codex', t: 'high' }, balanced: { m: 'gpt-5.3-codex', t: 'medium' }, budget: { m: 'gpt-5.3-codex', t: 'medium' } },
|
|
23
31
|
};
|
|
24
32
|
|
|
33
|
+
const DEFAULT_ENTRY = { m: 'gpt-5.3-codex', t: 'high' };
|
|
34
|
+
|
|
25
35
|
// ─── Output helpers ───────────────────────────────────────────────────────────
|
|
26
36
|
|
|
27
37
|
function output(result, raw, rawValue) {
|
|
@@ -29,7 +39,7 @@ function output(result, raw, rawValue) {
|
|
|
29
39
|
process.stdout.write(String(rawValue));
|
|
30
40
|
} else {
|
|
31
41
|
const json = JSON.stringify(result, null, 2);
|
|
32
|
-
// Large payloads exceed Codex
|
|
42
|
+
// Large payloads exceed Codex CLI's tool buffer (~50KB).
|
|
33
43
|
// Write to tmpfile and output the path prefixed with @file: so callers can detect it.
|
|
34
44
|
if (json.length > 50000) {
|
|
35
45
|
const tmpPath = path.join(require('os').tmpdir(), `gsd-${Date.now()}.json`);
|
|
@@ -69,6 +79,7 @@ function loadConfig(cwd) {
|
|
|
69
79
|
research: true,
|
|
70
80
|
plan_checker: true,
|
|
71
81
|
verifier: true,
|
|
82
|
+
nyquist_validation: false,
|
|
72
83
|
parallelization: true,
|
|
73
84
|
brave_search: false,
|
|
74
85
|
};
|
|
@@ -102,8 +113,10 @@ function loadConfig(cwd) {
|
|
|
102
113
|
research: get('research', { section: 'workflow', field: 'research' }) ?? defaults.research,
|
|
103
114
|
plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
|
|
104
115
|
verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
|
|
116
|
+
nyquist_validation: get('nyquist_validation', { section: 'workflow', field: 'nyquist_validation' }) ?? defaults.nyquist_validation,
|
|
105
117
|
parallelization,
|
|
106
118
|
brave_search: get('brave_search') ?? defaults.brave_search,
|
|
119
|
+
model_overrides: parsed.model_overrides || null,
|
|
107
120
|
};
|
|
108
121
|
} catch {
|
|
109
122
|
return defaults;
|
|
@@ -114,7 +127,11 @@ function loadConfig(cwd) {
|
|
|
114
127
|
|
|
115
128
|
function isGitIgnored(cwd, targetPath) {
|
|
116
129
|
try {
|
|
117
|
-
|
|
130
|
+
// --no-index checks .gitignore rules regardless of whether the file is tracked.
|
|
131
|
+
// Without it, git check-ignore returns "not ignored" for tracked files even when
|
|
132
|
+
// .gitignore explicitly lists them — a common source of confusion when .planning/
|
|
133
|
+
// was committed before being added to .gitignore.
|
|
134
|
+
execSync('git check-ignore -q --no-index -- ' + targetPath.replace(/[^a-zA-Z0-9._\-/]/g, ''), {
|
|
118
135
|
cwd,
|
|
119
136
|
stdio: 'pipe',
|
|
120
137
|
});
|
|
@@ -217,7 +234,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
|
|
217
234
|
|
|
218
235
|
return {
|
|
219
236
|
found: true,
|
|
220
|
-
directory: path.join(relBase, match),
|
|
237
|
+
directory: toPosixPath(path.join(relBase, match)),
|
|
221
238
|
phase_number: phaseNumber,
|
|
222
239
|
phase_name: phaseName,
|
|
223
240
|
phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
|
|
@@ -240,7 +257,7 @@ function findPhaseInternal(cwd, phase) {
|
|
|
240
257
|
const normalized = normalizePhaseName(phase);
|
|
241
258
|
|
|
242
259
|
// Search current phases first
|
|
243
|
-
const current = searchPhaseInDir(phasesDir,
|
|
260
|
+
const current = searchPhaseInDir(phasesDir, '.planning/phases', normalized);
|
|
244
261
|
if (current) return current;
|
|
245
262
|
|
|
246
263
|
// Search archived milestone phases (newest first)
|
|
@@ -258,7 +275,7 @@ function findPhaseInternal(cwd, phase) {
|
|
|
258
275
|
for (const archiveName of archiveDirs) {
|
|
259
276
|
const version = archiveName.match(/^(v[\d.]+)-phases$/)[1];
|
|
260
277
|
const archivePath = path.join(milestonesDir, archiveName);
|
|
261
|
-
const relBase =
|
|
278
|
+
const relBase = '.planning/milestones/' + archiveName;
|
|
262
279
|
const result = searchPhaseInDir(archivePath, relBase, normalized);
|
|
263
280
|
if (result) {
|
|
264
281
|
result.archived = version;
|
|
@@ -347,15 +364,19 @@ function resolveModelInternal(cwd, agentType) {
|
|
|
347
364
|
// Check per-agent override first
|
|
348
365
|
const override = config.model_overrides?.[agentType];
|
|
349
366
|
if (override) {
|
|
350
|
-
|
|
367
|
+
// Override can be a string (thinking level) or { m, t } object
|
|
368
|
+
if (typeof override === 'string') {
|
|
369
|
+
return { model: 'inherit', thinking: override === 'xhigh' || override === 'high' || override === 'medium' || override === 'low' ? override : 'high' };
|
|
370
|
+
}
|
|
371
|
+
return { model: 'inherit', thinking: override.t || 'high' };
|
|
351
372
|
}
|
|
352
373
|
|
|
353
374
|
// Fall back to profile lookup
|
|
354
375
|
const profile = config.model_profile || 'balanced';
|
|
355
376
|
const agentModels = MODEL_PROFILES[agentType];
|
|
356
|
-
if (!agentModels) return '
|
|
357
|
-
const
|
|
358
|
-
return
|
|
377
|
+
if (!agentModels) return { model: 'inherit', thinking: 'high' };
|
|
378
|
+
const entry = agentModels[profile] || agentModels['balanced'] || DEFAULT_ENTRY;
|
|
379
|
+
return { model: 'inherit', thinking: entry.t };
|
|
359
380
|
}
|
|
360
381
|
|
|
361
382
|
// ─── Misc utilities ───────────────────────────────────────────────────────────
|
|
@@ -378,17 +399,73 @@ function generateSlugInternal(text) {
|
|
|
378
399
|
function getMilestoneInfo(cwd) {
|
|
379
400
|
try {
|
|
380
401
|
const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
381
|
-
|
|
382
|
-
|
|
402
|
+
|
|
403
|
+
// First: check for list-format roadmaps using 🚧 (in-progress) marker
|
|
404
|
+
// e.g. "- 🚧 **v2.1 Belgium** — Phases 24-28 (in progress)"
|
|
405
|
+
const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+\.\d+)\s+([^*]+)\*\*/);
|
|
406
|
+
if (inProgressMatch) {
|
|
407
|
+
return {
|
|
408
|
+
version: 'v' + inProgressMatch[1],
|
|
409
|
+
name: inProgressMatch[2].trim(),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Second: heading-format roadmaps — strip shipped milestones in <details> blocks
|
|
414
|
+
const cleaned = roadmap.replace(/<details>[\s\S]*?<\/details>/gi, '');
|
|
415
|
+
// Extract version and name from the same ## heading for consistency
|
|
416
|
+
const headingMatch = cleaned.match(/## .*v(\d+\.\d+)[:\s]+([^\n(]+)/);
|
|
417
|
+
if (headingMatch) {
|
|
418
|
+
return {
|
|
419
|
+
version: 'v' + headingMatch[1],
|
|
420
|
+
name: headingMatch[2].trim(),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
// Fallback: try bare version match
|
|
424
|
+
const versionMatch = cleaned.match(/v(\d+\.\d+)/);
|
|
383
425
|
return {
|
|
384
426
|
version: versionMatch ? versionMatch[0] : 'v1.0',
|
|
385
|
-
name:
|
|
427
|
+
name: 'milestone',
|
|
386
428
|
};
|
|
387
429
|
} catch {
|
|
388
430
|
return { version: 'v1.0', name: 'milestone' };
|
|
389
431
|
}
|
|
390
432
|
}
|
|
391
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Returns a filter function that checks whether a phase directory belongs
|
|
436
|
+
* to the current milestone based on ROADMAP.md phase headings.
|
|
437
|
+
* If no ROADMAP exists or no phases are listed, returns a pass-all filter.
|
|
438
|
+
*/
|
|
439
|
+
function getMilestonePhaseFilter(cwd) {
|
|
440
|
+
const milestonePhaseNums = new Set();
|
|
441
|
+
try {
|
|
442
|
+
const roadmap = fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
443
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
|
|
444
|
+
let m;
|
|
445
|
+
while ((m = phasePattern.exec(roadmap)) !== null) {
|
|
446
|
+
milestonePhaseNums.add(m[1]);
|
|
447
|
+
}
|
|
448
|
+
} catch {}
|
|
449
|
+
|
|
450
|
+
if (milestonePhaseNums.size === 0) {
|
|
451
|
+
const passAll = () => true;
|
|
452
|
+
passAll.phaseCount = 0;
|
|
453
|
+
return passAll;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const normalized = new Set(
|
|
457
|
+
[...milestonePhaseNums].map(n => (n.replace(/^0+/, '') || '0').toLowerCase())
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
function isDirInMilestone(dirName) {
|
|
461
|
+
const m = dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/);
|
|
462
|
+
if (!m) return false;
|
|
463
|
+
return normalized.has(m[1].toLowerCase());
|
|
464
|
+
}
|
|
465
|
+
isDirInMilestone.phaseCount = milestonePhaseNums.size;
|
|
466
|
+
return isDirInMilestone;
|
|
467
|
+
}
|
|
468
|
+
|
|
392
469
|
module.exports = {
|
|
393
470
|
MODEL_PROFILES,
|
|
394
471
|
output,
|
|
@@ -408,4 +485,6 @@ module.exports = {
|
|
|
408
485
|
pathExistsInternal,
|
|
409
486
|
generateSlugInternal,
|
|
410
487
|
getMilestoneInfo,
|
|
488
|
+
getMilestonePhaseFilter,
|
|
489
|
+
toPosixPath,
|
|
411
490
|
};
|
|
@@ -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) {
|
|
@@ -139,19 +139,19 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
|
|
139
139
|
const files = fs.readdirSync(phaseDirFull);
|
|
140
140
|
const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
|
141
141
|
if (contextFile) {
|
|
142
|
-
result.context_path = path.join(phaseInfo.directory, contextFile);
|
|
142
|
+
result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
|
|
143
143
|
}
|
|
144
144
|
const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
145
145
|
if (researchFile) {
|
|
146
|
-
result.research_path = path.join(phaseInfo.directory, researchFile);
|
|
146
|
+
result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
|
|
147
147
|
}
|
|
148
148
|
const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
|
|
149
149
|
if (verificationFile) {
|
|
150
|
-
result.verification_path = path.join(phaseInfo.directory, verificationFile);
|
|
150
|
+
result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
|
|
151
151
|
}
|
|
152
152
|
const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
|
|
153
153
|
if (uatFile) {
|
|
154
|
-
result.uat_path = path.join(phaseInfo.directory, uatFile);
|
|
154
|
+
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
|
|
155
155
|
}
|
|
156
156
|
} catch {}
|
|
157
157
|
}
|
|
@@ -422,19 +422,19 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
|
|
422
422
|
const files = fs.readdirSync(phaseDirFull);
|
|
423
423
|
const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
|
424
424
|
if (contextFile) {
|
|
425
|
-
result.context_path = path.join(phaseInfo.directory, contextFile);
|
|
425
|
+
result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
|
|
426
426
|
}
|
|
427
427
|
const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
428
428
|
if (researchFile) {
|
|
429
|
-
result.research_path = path.join(phaseInfo.directory, researchFile);
|
|
429
|
+
result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
|
|
430
430
|
}
|
|
431
431
|
const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
|
|
432
432
|
if (verificationFile) {
|
|
433
|
-
result.verification_path = path.join(phaseInfo.directory, verificationFile);
|
|
433
|
+
result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
|
|
434
434
|
}
|
|
435
435
|
const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
|
|
436
436
|
if (uatFile) {
|
|
437
|
-
result.uat_path = path.join(phaseInfo.directory, uatFile);
|
|
437
|
+
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
|
|
438
438
|
}
|
|
439
439
|
} catch {}
|
|
440
440
|
}
|
|
@@ -469,7 +469,7 @@ function cmdInitTodos(cwd, area, raw) {
|
|
|
469
469
|
created: createdMatch ? createdMatch[1].trim() : 'unknown',
|
|
470
470
|
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
|
471
471
|
area: todoArea,
|
|
472
|
-
path:
|
|
472
|
+
path: '.planning/todos/pending/' + file,
|
|
473
473
|
});
|
|
474
474
|
} catch {}
|
|
475
475
|
}
|
|
@@ -629,7 +629,7 @@ function cmdInitProgress(cwd, raw) {
|
|
|
629
629
|
const phaseInfo = {
|
|
630
630
|
number: phaseNumber,
|
|
631
631
|
name: phaseName,
|
|
632
|
-
directory:
|
|
632
|
+
directory: '.planning/phases/' + dir,
|
|
633
633
|
status,
|
|
634
634
|
plan_count: plans.length,
|
|
635
635
|
summary_count: summaries.length,
|
|
@@ -92,7 +92,44 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
92
92
|
// Ensure archive directory exists
|
|
93
93
|
fs.mkdirSync(archiveDir, { recursive: true });
|
|
94
94
|
|
|
95
|
-
//
|
|
95
|
+
// Extract milestone phase numbers from ROADMAP.md to scope stats.
|
|
96
|
+
// Only phases listed in the current ROADMAP are counted — phases from
|
|
97
|
+
// prior milestones that remain on disk are excluded.
|
|
98
|
+
//
|
|
99
|
+
// Related upstream PRs (getMilestoneInfo, not milestone complete):
|
|
100
|
+
// #756 — fix(core): detect current milestone correctly in getMilestoneInfo
|
|
101
|
+
// #783 — fix: getMilestoneInfo() returns wrong version after completion
|
|
102
|
+
// Those PRs fix *which* milestone is detected; this fix scopes *stats*
|
|
103
|
+
// and *accomplishments* to only the phases belonging to that milestone.
|
|
104
|
+
const milestonePhaseNums = new Set();
|
|
105
|
+
if (fs.existsSync(roadmapPath)) {
|
|
106
|
+
try {
|
|
107
|
+
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
108
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
|
|
109
|
+
let phaseMatch;
|
|
110
|
+
while ((phaseMatch = phasePattern.exec(roadmapContent)) !== null) {
|
|
111
|
+
milestonePhaseNums.add(phaseMatch[1]);
|
|
112
|
+
}
|
|
113
|
+
} catch {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Pre-normalize phase numbers for O(1) lookup — strip leading zeros
|
|
117
|
+
// and lowercase for case-insensitive matching of letter suffixes (e.g. 3A/3a).
|
|
118
|
+
const normalizedPhaseNums = new Set(
|
|
119
|
+
[...milestonePhaseNums].map(num => (num.replace(/^0+/, '') || '0').toLowerCase())
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Match a phase directory name to the milestone's phase set.
|
|
123
|
+
// Handles: "01-foo" → "1", "3A-bar" → "3a", "3.1-baz" → "3.1"
|
|
124
|
+
// Returns false for non-phase directories (no leading digit).
|
|
125
|
+
function isDirInMilestone(dirName) {
|
|
126
|
+
if (normalizedPhaseNums.size === 0) return true; // no scoping
|
|
127
|
+
const m = dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)/);
|
|
128
|
+
if (!m) return false; // not a phase directory
|
|
129
|
+
return normalizedPhaseNums.has(m[1].toLowerCase());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Gather stats from phases (scoped to current milestone only)
|
|
96
133
|
let phaseCount = 0;
|
|
97
134
|
let totalPlans = 0;
|
|
98
135
|
let totalTasks = 0;
|
|
@@ -103,6 +140,8 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
103
140
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
104
141
|
|
|
105
142
|
for (const dir of dirs) {
|
|
143
|
+
if (!isDirInMilestone(dir)) continue;
|
|
144
|
+
|
|
106
145
|
phaseCount++;
|
|
107
146
|
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
|
108
147
|
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
|
@@ -150,7 +189,16 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
150
189
|
|
|
151
190
|
if (fs.existsSync(milestonesPath)) {
|
|
152
191
|
const existing = fs.readFileSync(milestonesPath, 'utf-8');
|
|
153
|
-
|
|
192
|
+
// Insert after the header line(s) for reverse chronological order (newest first)
|
|
193
|
+
const headerMatch = existing.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
|
|
194
|
+
if (headerMatch) {
|
|
195
|
+
const header = headerMatch[1];
|
|
196
|
+
const rest = existing.slice(header.length);
|
|
197
|
+
fs.writeFileSync(milestonesPath, header + milestoneEntry + rest, 'utf-8');
|
|
198
|
+
} else {
|
|
199
|
+
// No recognizable header — prepend the entry
|
|
200
|
+
fs.writeFileSync(milestonesPath, milestoneEntry + existing, 'utf-8');
|
|
201
|
+
}
|
|
154
202
|
} else {
|
|
155
203
|
fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
|
|
156
204
|
}
|
|
@@ -182,10 +230,13 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
182
230
|
|
|
183
231
|
const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
184
232
|
const phaseDirNames = phaseEntries.filter(e => e.isDirectory()).map(e => e.name);
|
|
233
|
+
let archivedCount = 0;
|
|
185
234
|
for (const dir of phaseDirNames) {
|
|
235
|
+
if (!isDirInMilestone(dir)) continue;
|
|
186
236
|
fs.renameSync(path.join(phasesDir, dir), path.join(phaseArchiveDir, dir));
|
|
237
|
+
archivedCount++;
|
|
187
238
|
}
|
|
188
|
-
phasesArchived =
|
|
239
|
+
phasesArchived = archivedCount > 0;
|
|
189
240
|
} catch {}
|
|
190
241
|
}
|
|
191
242
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, output, error } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, output, error } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
9
|
const { writeStateMd } = require('./state.cjs');
|
|
10
10
|
|
|
@@ -193,6 +193,11 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
function extractObjective(content) {
|
|
197
|
+
const m = content.match(/<objective>\s*\n?\s*(.+)/);
|
|
198
|
+
return m ? m[1].trim() : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
196
201
|
function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
197
202
|
if (!phase) {
|
|
198
203
|
error('phase required for phase-plan-index');
|
|
@@ -242,9 +247,10 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
242
247
|
const content = fs.readFileSync(planPath, 'utf-8');
|
|
243
248
|
const fm = extractFrontmatter(content);
|
|
244
249
|
|
|
245
|
-
// Count tasks (## Task N
|
|
246
|
-
const
|
|
247
|
-
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;
|
|
248
254
|
|
|
249
255
|
// Parse wave as integer
|
|
250
256
|
const wave = parseInt(fm.wave, 10) || 1;
|
|
@@ -259,10 +265,11 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
259
265
|
hasCheckpoints = true;
|
|
260
266
|
}
|
|
261
267
|
|
|
262
|
-
// Parse
|
|
268
|
+
// Parse files_modified (underscore is canonical; also accept hyphenated for compat)
|
|
263
269
|
let filesModified = [];
|
|
264
|
-
|
|
265
|
-
|
|
270
|
+
const fmFiles = fm['files_modified'] || fm['files-modified'];
|
|
271
|
+
if (fmFiles) {
|
|
272
|
+
filesModified = Array.isArray(fmFiles) ? fmFiles : [fmFiles];
|
|
266
273
|
}
|
|
267
274
|
|
|
268
275
|
const hasSummary = completedPlanIds.has(planId);
|
|
@@ -274,7 +281,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
274
281
|
id: planId,
|
|
275
282
|
wave,
|
|
276
283
|
autonomous,
|
|
277
|
-
objective: fm.objective || null,
|
|
284
|
+
objective: extractObjective(content) || fm.objective || null,
|
|
278
285
|
files_modified: filesModified,
|
|
279
286
|
task_count: taskCount,
|
|
280
287
|
has_summary: hasSummary,
|
|
@@ -776,14 +783,19 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
776
783
|
}
|
|
777
784
|
}
|
|
778
785
|
|
|
779
|
-
// 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
|
|
780
789
|
let nextPhaseNum = null;
|
|
781
790
|
let nextPhaseName = null;
|
|
782
791
|
let isLastPhase = true;
|
|
783
792
|
|
|
784
793
|
try {
|
|
794
|
+
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
|
785
795
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
786
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
796
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
797
|
+
.filter(isDirInMilestone)
|
|
798
|
+
.sort((a, b) => comparePhaseNum(a, b));
|
|
787
799
|
|
|
788
800
|
// Find the next phase directory after current
|
|
789
801
|
for (const dir of dirs) {
|
|
@@ -799,6 +811,24 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
799
811
|
}
|
|
800
812
|
} catch {}
|
|
801
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
|
+
|
|
802
832
|
// Update STATE.md
|
|
803
833
|
if (fs.existsSync(statePath)) {
|
|
804
834
|
let stateContent = fs.readFileSync(statePath, 'utf-8');
|