gsd-opencode 1.30.0 → 1.33.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-debugger.md +0 -1
- package/agents/gsd-doc-verifier.md +207 -0
- package/agents/gsd-doc-writer.md +608 -0
- package/agents/gsd-executor.md +22 -1
- package/agents/gsd-phase-researcher.md +41 -0
- package/agents/gsd-plan-checker.md +82 -0
- package/agents/gsd-planner.md +123 -194
- package/agents/gsd-security-auditor.md +129 -0
- package/agents/gsd-ui-auditor.md +40 -0
- package/agents/gsd-user-profiler.md +2 -2
- package/agents/gsd-verifier.md +84 -18
- package/commands/gsd/gsd-add-backlog.md +1 -1
- package/commands/gsd/gsd-analyze-dependencies.md +34 -0
- package/commands/gsd/gsd-autonomous.md +6 -2
- package/commands/gsd/gsd-cleanup.md +5 -0
- package/commands/gsd/gsd-debug.md +24 -21
- package/commands/gsd/gsd-discuss-phase.md +7 -2
- package/commands/gsd/gsd-docs-update.md +48 -0
- package/commands/gsd/gsd-execute-phase.md +4 -0
- package/commands/gsd/gsd-help.md +2 -0
- package/commands/gsd/gsd-join-discord.md +2 -1
- package/commands/gsd/gsd-manager.md +1 -0
- package/commands/gsd/gsd-new-project.md +4 -0
- package/commands/gsd/gsd-plan-phase.md +5 -0
- package/commands/gsd/gsd-quick.md +5 -3
- package/commands/gsd/gsd-reapply-patches.md +171 -39
- package/commands/gsd/gsd-research-phase.md +2 -12
- package/commands/gsd/gsd-review-backlog.md +1 -0
- package/commands/gsd/gsd-review.md +3 -2
- package/commands/gsd/gsd-secure-phase.md +35 -0
- package/commands/gsd/gsd-thread.md +1 -1
- package/commands/gsd/gsd-workstreams.md +7 -2
- package/get-shit-done/bin/gsd-tools.cjs +42 -8
- package/get-shit-done/bin/lib/commands.cjs +68 -14
- package/get-shit-done/bin/lib/config.cjs +18 -10
- package/get-shit-done/bin/lib/core.cjs +383 -80
- package/get-shit-done/bin/lib/docs.cjs +267 -0
- package/get-shit-done/bin/lib/frontmatter.cjs +47 -2
- package/get-shit-done/bin/lib/init.cjs +85 -5
- package/get-shit-done/bin/lib/milestone.cjs +21 -0
- package/get-shit-done/bin/lib/model-profiles.cjs +2 -0
- package/get-shit-done/bin/lib/phase.cjs +232 -189
- package/get-shit-done/bin/lib/profile-output.cjs +97 -1
- package/get-shit-done/bin/lib/roadmap.cjs +137 -113
- package/get-shit-done/bin/lib/schema-detect.cjs +238 -0
- package/get-shit-done/bin/lib/security.cjs +5 -3
- package/get-shit-done/bin/lib/state.cjs +366 -44
- package/get-shit-done/bin/lib/verify.cjs +158 -14
- package/get-shit-done/bin/lib/workstream.cjs +6 -2
- package/get-shit-done/references/agent-contracts.md +79 -0
- package/get-shit-done/references/artifact-types.md +113 -0
- package/get-shit-done/references/context-budget.md +49 -0
- package/get-shit-done/references/continuation-format.md +15 -15
- package/get-shit-done/references/domain-probes.md +125 -0
- package/get-shit-done/references/gate-prompts.md +100 -0
- package/get-shit-done/references/model-profiles.md +2 -2
- package/get-shit-done/references/planner-gap-closure.md +62 -0
- package/get-shit-done/references/planner-reviews.md +39 -0
- package/get-shit-done/references/planner-revision.md +87 -0
- package/get-shit-done/references/planning-config.md +15 -0
- package/get-shit-done/references/revision-loop.md +97 -0
- package/get-shit-done/references/ui-brand.md +2 -2
- package/get-shit-done/references/universal-anti-patterns.md +58 -0
- package/get-shit-done/references/workstream-flag.md +56 -3
- package/get-shit-done/templates/SECURITY.md +61 -0
- package/get-shit-done/templates/VALIDATION.md +3 -3
- package/get-shit-done/templates/claude-md.md +27 -4
- package/get-shit-done/templates/config.json +4 -0
- package/get-shit-done/templates/debug-subagent-prompt.md +2 -6
- package/get-shit-done/templates/planner-subagent-prompt.md +2 -10
- package/get-shit-done/workflows/add-phase.md +2 -2
- package/get-shit-done/workflows/add-todo.md +1 -1
- package/get-shit-done/workflows/analyze-dependencies.md +96 -0
- package/get-shit-done/workflows/audit-milestone.md +8 -12
- package/get-shit-done/workflows/autonomous.md +158 -13
- package/get-shit-done/workflows/check-todos.md +2 -2
- package/get-shit-done/workflows/complete-milestone.md +13 -4
- package/get-shit-done/workflows/diagnose-issues.md +8 -6
- package/get-shit-done/workflows/discovery-phase.md +1 -1
- package/get-shit-done/workflows/discuss-phase-assumptions.md +22 -4
- package/get-shit-done/workflows/discuss-phase-power.md +291 -0
- package/get-shit-done/workflows/discuss-phase.md +149 -11
- package/get-shit-done/workflows/docs-update.md +1093 -0
- package/get-shit-done/workflows/execute-phase.md +362 -66
- package/get-shit-done/workflows/execute-plan.md +1 -1
- package/get-shit-done/workflows/help.md +9 -6
- package/get-shit-done/workflows/insert-phase.md +2 -2
- package/get-shit-done/workflows/manager.md +27 -26
- package/get-shit-done/workflows/map-codebase.md +10 -32
- package/get-shit-done/workflows/new-milestone.md +14 -8
- package/get-shit-done/workflows/new-project.md +48 -25
- package/get-shit-done/workflows/next.md +1 -1
- package/get-shit-done/workflows/note.md +1 -1
- package/get-shit-done/workflows/pause-work.md +73 -10
- package/get-shit-done/workflows/plan-milestone-gaps.md +2 -2
- package/get-shit-done/workflows/plan-phase.md +184 -32
- package/get-shit-done/workflows/progress.md +20 -20
- package/get-shit-done/workflows/quick.md +102 -84
- package/get-shit-done/workflows/research-phase.md +2 -6
- package/get-shit-done/workflows/resume-project.md +4 -4
- package/get-shit-done/workflows/review.md +56 -3
- package/get-shit-done/workflows/secure-phase.md +154 -0
- package/get-shit-done/workflows/settings.md +13 -2
- package/get-shit-done/workflows/ship.md +13 -4
- package/get-shit-done/workflows/transition.md +6 -6
- package/get-shit-done/workflows/ui-phase.md +4 -14
- package/get-shit-done/workflows/ui-review.md +25 -7
- package/get-shit-done/workflows/update.md +165 -16
- package/get-shit-done/workflows/validate-phase.md +1 -11
- package/get-shit-done/workflows/verify-phase.md +127 -6
- package/get-shit-done/workflows/verify-work.md +69 -21
- package/package.json +1 -1
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { escapeRegex, loadConfig, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, planningDir, output, error, readSubdirectories } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, loadConfig, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, planningDir, withPlanningLock, output, error, readSubdirectories, phaseTokenMatches } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
|
-
const { writeStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback } = require('./state.cjs');
|
|
9
|
+
const { writeStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, updatePerformanceMetricsSection } = require('./state.cjs');
|
|
10
10
|
|
|
11
11
|
function cmdPhasesList(cwd, options, raw) {
|
|
12
12
|
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
@@ -41,7 +41,7 @@ function cmdPhasesList(cwd, options, raw) {
|
|
|
41
41
|
// If filtering by phase number
|
|
42
42
|
if (phase) {
|
|
43
43
|
const normalized = normalizePhaseName(phase);
|
|
44
|
-
const match = dirs.find(d => d
|
|
44
|
+
const match = dirs.find(d => phaseTokenMatches(d, normalized));
|
|
45
45
|
if (!match) {
|
|
46
46
|
output({ files: [], count: 0, phase_dir: null, error: 'Phase not found' }, raw, '');
|
|
47
47
|
return;
|
|
@@ -108,7 +108,7 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
|
|
|
108
108
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
109
109
|
|
|
110
110
|
// Check if base phase exists
|
|
111
|
-
const baseExists = dirs.some(d =>
|
|
111
|
+
const baseExists = dirs.some(d => phaseTokenMatches(d, normalized));
|
|
112
112
|
|
|
113
113
|
// Find existing decimal phases for this base
|
|
114
114
|
const decimalPattern = new RegExp(`^${normalized}\\.(\\d+)`);
|
|
@@ -163,13 +163,15 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
163
163
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
164
164
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
165
165
|
|
|
166
|
-
const match = dirs.find(d => d
|
|
166
|
+
const match = dirs.find(d => phaseTokenMatches(d, normalized));
|
|
167
167
|
if (!match) {
|
|
168
168
|
output(notFound, raw, '');
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
// Extract phase number — supports project-code-prefixed (CK-01-name), numeric (01-name), and custom IDs
|
|
173
|
+
const dirMatch = match.match(/^(?:[A-Z]{1,6}-)(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i)
|
|
174
|
+
|| match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
173
175
|
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
|
174
176
|
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
175
177
|
|
|
@@ -212,7 +214,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
212
214
|
try {
|
|
213
215
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
214
216
|
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
215
|
-
const match = dirs.find(d => d
|
|
217
|
+
const match = dirs.find(d => phaseTokenMatches(d, normalized));
|
|
216
218
|
if (match) {
|
|
217
219
|
phaseDir = path.join(phasesDir, match);
|
|
218
220
|
phaseDirName = match;
|
|
@@ -319,53 +321,62 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
319
321
|
error('ROADMAP.md not found');
|
|
320
322
|
}
|
|
321
323
|
|
|
322
|
-
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
323
|
-
const content = extractCurrentMilestone(rawContent, cwd);
|
|
324
324
|
const slug = generateSlugInternal(description);
|
|
325
325
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
// Custom phase naming: use provided ID or generate from description
|
|
331
|
-
newPhaseId = customId || slug.toUpperCase().replace(/-/g, '-');
|
|
332
|
-
if (!newPhaseId) error('--id required when phase_naming is "custom"');
|
|
333
|
-
dirName = `${newPhaseId}-${slug}`;
|
|
334
|
-
} else {
|
|
335
|
-
// Sequential mode: find highest integer phase number (in current milestone only)
|
|
336
|
-
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
|
|
337
|
-
let maxPhase = 0;
|
|
338
|
-
let m;
|
|
339
|
-
while ((m = phasePattern.exec(content)) !== null) {
|
|
340
|
-
const num = parseInt(m[1], 10);
|
|
341
|
-
if (num > maxPhase) maxPhase = num;
|
|
342
|
-
}
|
|
326
|
+
// Wrap entire read-modify-write in lock to prevent concurrent corruption
|
|
327
|
+
const { newPhaseId, dirName } = withPlanningLock(cwd, () => {
|
|
328
|
+
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
329
|
+
const content = extractCurrentMilestone(rawContent, cwd);
|
|
343
330
|
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
}
|
|
331
|
+
// Optional project code prefix (e.g., 'CK' → 'CK-01-foundation')
|
|
332
|
+
const projectCode = config.project_code || '';
|
|
333
|
+
const prefix = projectCode ? `${projectCode}-` : '';
|
|
348
334
|
|
|
349
|
-
|
|
335
|
+
let _newPhaseId;
|
|
336
|
+
let _dirName;
|
|
350
337
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
338
|
+
if (customId || config.phase_naming === 'custom') {
|
|
339
|
+
// Custom phase naming: use provided ID or generate from description
|
|
340
|
+
_newPhaseId = customId || slug.toUpperCase().replace(/-/g, '-');
|
|
341
|
+
if (!_newPhaseId) error('--id required when phase_naming is "custom"');
|
|
342
|
+
_dirName = `${prefix}${_newPhaseId}-${slug}`;
|
|
343
|
+
} else {
|
|
344
|
+
// Sequential mode: find highest integer phase number (in current milestone only)
|
|
345
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
|
|
346
|
+
let maxPhase = 0;
|
|
347
|
+
let m;
|
|
348
|
+
while ((m = phasePattern.exec(content)) !== null) {
|
|
349
|
+
const num = parseInt(m[1], 10);
|
|
350
|
+
if (num > maxPhase) maxPhase = num;
|
|
351
|
+
}
|
|
354
352
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
353
|
+
_newPhaseId = maxPhase + 1;
|
|
354
|
+
const paddedNum = String(_newPhaseId).padStart(2, '0');
|
|
355
|
+
_dirName = `${prefix}${paddedNum}-${slug}`;
|
|
356
|
+
}
|
|
358
357
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
} else {
|
|
365
|
-
updatedContent = rawContent + phaseEntry;
|
|
366
|
-
}
|
|
358
|
+
const dirPath = path.join(planningDir(cwd), 'phases', _dirName);
|
|
359
|
+
|
|
360
|
+
// Create directory with .gitkeep so git tracks empty folders
|
|
361
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
362
|
+
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
|
|
367
363
|
|
|
368
|
-
|
|
364
|
+
// Build phase entry
|
|
365
|
+
const dependsOn = config.phase_naming === 'custom' ? '' : `\n**Depends on:** Phase ${typeof _newPhaseId === 'number' ? _newPhaseId - 1 : 'TBD'}`;
|
|
366
|
+
const phaseEntry = `\n### Phase ${_newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${_newPhaseId} to break down)\n`;
|
|
367
|
+
|
|
368
|
+
// Find insertion point: before last "---" or at end
|
|
369
|
+
let updatedContent;
|
|
370
|
+
const lastSeparator = rawContent.lastIndexOf('\n---');
|
|
371
|
+
if (lastSeparator > 0) {
|
|
372
|
+
updatedContent = rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator);
|
|
373
|
+
} else {
|
|
374
|
+
updatedContent = rawContent + phaseEntry;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
|
|
378
|
+
return { newPhaseId: _newPhaseId, dirName: _dirName };
|
|
379
|
+
});
|
|
369
380
|
|
|
370
381
|
const result = {
|
|
371
382
|
phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
|
|
@@ -389,66 +400,75 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
389
400
|
error('ROADMAP.md not found');
|
|
390
401
|
}
|
|
391
402
|
|
|
392
|
-
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
393
|
-
const content = extractCurrentMilestone(rawContent, cwd);
|
|
394
403
|
const slug = generateSlugInternal(description);
|
|
395
404
|
|
|
396
|
-
//
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
let existingDecimals = [];
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
412
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
413
|
-
const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
|
|
414
|
-
for (const dir of dirs) {
|
|
415
|
-
const dm = dir.match(decimalPattern);
|
|
416
|
-
if (dm) existingDecimals.push(parseInt(dm[1], 10));
|
|
405
|
+
// Wrap entire read-modify-write in lock to prevent concurrent corruption
|
|
406
|
+
const { decimalPhase, dirName } = withPlanningLock(cwd, () => {
|
|
407
|
+
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
408
|
+
const content = extractCurrentMilestone(rawContent, cwd);
|
|
409
|
+
|
|
410
|
+
// Normalize input then strip leading zeros for flexible matching
|
|
411
|
+
const normalizedAfter = normalizePhaseName(afterPhase);
|
|
412
|
+
const unpadded = normalizedAfter.replace(/^0+/, '');
|
|
413
|
+
const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
|
|
414
|
+
const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
|
|
415
|
+
if (!targetPattern.test(content)) {
|
|
416
|
+
error(`Phase ${afterPhase} not found in ROADMAP.md`);
|
|
417
417
|
}
|
|
418
|
-
} catch { /* intentionally empty */ }
|
|
419
418
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
419
|
+
// Calculate next decimal using existing logic
|
|
420
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
421
|
+
const normalizedBase = normalizePhaseName(afterPhase);
|
|
422
|
+
let existingDecimals = [];
|
|
424
423
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
424
|
+
try {
|
|
425
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
426
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
427
|
+
const decimalPattern = new RegExp(`^(?:[A-Z]{1,6}-)?${normalizedBase}\\.(\\d+)`);
|
|
428
|
+
for (const dir of dirs) {
|
|
429
|
+
const dm = dir.match(decimalPattern);
|
|
430
|
+
if (dm) existingDecimals.push(parseInt(dm[1], 10));
|
|
431
|
+
}
|
|
432
|
+
} catch { /* intentionally empty */ }
|
|
431
433
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
434
|
+
const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1;
|
|
435
|
+
const _decimalPhase = `${normalizedBase}.${nextDecimal}`;
|
|
436
|
+
// Optional project code prefix
|
|
437
|
+
const insertConfig = loadConfig(cwd);
|
|
438
|
+
const projectCode = insertConfig.project_code || '';
|
|
439
|
+
const pfx = projectCode ? `${projectCode}-` : '';
|
|
440
|
+
const _dirName = `${pfx}${_decimalPhase}-${slug}`;
|
|
441
|
+
const dirPath = path.join(planningDir(cwd), 'phases', _dirName);
|
|
442
|
+
|
|
443
|
+
// Create directory with .gitkeep so git tracks empty folders
|
|
444
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
445
|
+
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
|
|
446
|
+
|
|
447
|
+
// Build phase entry
|
|
448
|
+
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`;
|
|
449
|
+
|
|
450
|
+
// Insert after the target phase section
|
|
451
|
+
const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
|
|
452
|
+
const headerMatch = rawContent.match(headerPattern);
|
|
453
|
+
if (!headerMatch) {
|
|
454
|
+
error(`Could not find Phase ${afterPhase} header`);
|
|
455
|
+
}
|
|
438
456
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
457
|
+
const headerIdx = rawContent.indexOf(headerMatch[0]);
|
|
458
|
+
const afterHeader = rawContent.slice(headerIdx + headerMatch[0].length);
|
|
459
|
+
const nextPhaseMatch = afterHeader.match(/\n#{2,4}\s+Phase\s+\d/i);
|
|
442
460
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
461
|
+
let insertIdx;
|
|
462
|
+
if (nextPhaseMatch) {
|
|
463
|
+
insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
|
|
464
|
+
} else {
|
|
465
|
+
insertIdx = rawContent.length;
|
|
466
|
+
}
|
|
449
467
|
|
|
450
|
-
|
|
451
|
-
|
|
468
|
+
const updatedContent = rawContent.slice(0, insertIdx) + phaseEntry + rawContent.slice(insertIdx);
|
|
469
|
+
fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
|
|
470
|
+
return { decimalPhase: _decimalPhase, dirName: _dirName };
|
|
471
|
+
});
|
|
452
472
|
|
|
453
473
|
const result = {
|
|
454
474
|
phase_number: decimalPhase,
|
|
@@ -536,29 +556,32 @@ function renameIntegerPhases(phasesDir, removedInt) {
|
|
|
536
556
|
/**
|
|
537
557
|
* Remove a phase section from ROADMAP.md and renumber all subsequent integer phases.
|
|
538
558
|
*/
|
|
539
|
-
function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, removedInt) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
559
|
+
function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, removedInt, cwd) {
|
|
560
|
+
// Wrap entire read-modify-write in lock to prevent concurrent corruption
|
|
561
|
+
withPlanningLock(cwd, () => {
|
|
562
|
+
let content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
563
|
+
const escaped = escapeRegex(targetPhase);
|
|
564
|
+
|
|
565
|
+
content = content.replace(new RegExp(`\\n?#{2,4}\\s*Phase\\s+${escaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`, 'i'), '');
|
|
566
|
+
content = content.replace(new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${escaped}[:\\s][^\\n]*`, 'gi'), '');
|
|
567
|
+
content = content.replace(new RegExp(`\\n?\\|\\s*${escaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi'), '');
|
|
568
|
+
|
|
569
|
+
if (!isDecimal) {
|
|
570
|
+
const MAX_PHASE = 99;
|
|
571
|
+
for (let oldNum = MAX_PHASE; oldNum > removedInt; oldNum--) {
|
|
572
|
+
const newNum = oldNum - 1;
|
|
573
|
+
const oldStr = String(oldNum), newStr = String(newNum);
|
|
574
|
+
const oldPad = oldStr.padStart(2, '0'), newPad = newStr.padStart(2, '0');
|
|
575
|
+
content = content.replace(new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'), `$1${newStr}$2`);
|
|
576
|
+
content = content.replace(new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, 'g'), `$1${newStr}$2`);
|
|
577
|
+
content = content.replace(new RegExp(`${oldPad}-(\\d{2})`, 'g'), `${newPad}-$1`);
|
|
578
|
+
content = content.replace(new RegExp(`(\\|\\s*)${oldStr}\\.\\s`, 'g'), `$1${newStr}. `);
|
|
579
|
+
content = content.replace(new RegExp(`(Depends on:\\*\\*\\s*Phase\\s+)${oldStr}\\b`, 'gi'), `$1${newStr}`);
|
|
580
|
+
}
|
|
558
581
|
}
|
|
559
|
-
}
|
|
560
582
|
|
|
561
|
-
|
|
583
|
+
fs.writeFileSync(roadmapPath, content, 'utf-8');
|
|
584
|
+
});
|
|
562
585
|
}
|
|
563
586
|
|
|
564
587
|
function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
@@ -575,7 +598,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
575
598
|
|
|
576
599
|
// Find target directory
|
|
577
600
|
const targetDir = readSubdirectories(phasesDir, true)
|
|
578
|
-
.find(d =>
|
|
601
|
+
.find(d => phaseTokenMatches(d, normalized)) || null;
|
|
579
602
|
|
|
580
603
|
// Guard against removing executed work
|
|
581
604
|
if (targetDir && !force) {
|
|
@@ -599,7 +622,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
599
622
|
} catch { /* intentionally empty */ }
|
|
600
623
|
|
|
601
624
|
// Update ROADMAP.md
|
|
602
|
-
updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, parseInt(normalized, 10));
|
|
625
|
+
updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, parseInt(normalized, 10), cwd);
|
|
603
626
|
|
|
604
627
|
// Update STATE.md phase count
|
|
605
628
|
const statePath = path.join(planningDir(cwd), 'STATE.md');
|
|
@@ -668,84 +691,101 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
668
691
|
}
|
|
669
692
|
} catch {}
|
|
670
693
|
|
|
671
|
-
// Update ROADMAP.md
|
|
694
|
+
// Update ROADMAP.md and REQUIREMENTS.md atomically under lock
|
|
672
695
|
if (fs.existsSync(roadmapPath)) {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
// Checkbox: - [ ] Phase N: → - [x] Phase N: (...completed DATE)
|
|
676
|
-
const checkboxPattern = new RegExp(
|
|
677
|
-
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
|
|
678
|
-
'i'
|
|
679
|
-
);
|
|
680
|
-
roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
|
|
681
|
-
|
|
682
|
-
// Progress table: update Status to Complete, add date (handles 4 or 5 column tables)
|
|
683
|
-
const phaseEscaped = escapeRegex(phaseNum);
|
|
684
|
-
const tableRowPattern = new RegExp(
|
|
685
|
-
`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
|
|
686
|
-
'im'
|
|
687
|
-
);
|
|
688
|
-
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
|
|
689
|
-
const cells = fullRow.split('|').slice(1, -1);
|
|
690
|
-
if (cells.length === 5) {
|
|
691
|
-
// 5-col: Phase | Milestone | Plans | Status | Completed
|
|
692
|
-
cells[3] = ' Complete ';
|
|
693
|
-
cells[4] = ` ${today} `;
|
|
694
|
-
} else if (cells.length === 4) {
|
|
695
|
-
// 4-col: Phase | Plans | Status | Completed
|
|
696
|
-
cells[2] = ' Complete ';
|
|
697
|
-
cells[3] = ` ${today} `;
|
|
698
|
-
}
|
|
699
|
-
return '|' + cells.join('|') + '|';
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
// Update plan count in phase section
|
|
703
|
-
const planCountPattern = new RegExp(
|
|
704
|
-
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
|
705
|
-
'i'
|
|
706
|
-
);
|
|
707
|
-
roadmapContent = replaceInCurrentMilestone(
|
|
708
|
-
roadmapContent, planCountPattern,
|
|
709
|
-
`$1${summaryCount}/${planCount} plans complete`
|
|
710
|
-
);
|
|
696
|
+
withPlanningLock(cwd, () => {
|
|
697
|
+
let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
711
698
|
|
|
712
|
-
|
|
699
|
+
// Checkbox: - [ ] Phase N: → - [x] Phase N: (...completed DATE)
|
|
700
|
+
const checkboxPattern = new RegExp(
|
|
701
|
+
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
|
|
702
|
+
'i'
|
|
703
|
+
);
|
|
704
|
+
roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
|
|
713
705
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
|
|
720
|
-
const phaseSectionMatch = currentMilestoneRoadmap.match(
|
|
721
|
-
new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i')
|
|
706
|
+
// Progress table: update Status to Complete, add date (handles 4 or 5 column tables)
|
|
707
|
+
const phaseEscaped = escapeRegex(phaseNum);
|
|
708
|
+
const tableRowPattern = new RegExp(
|
|
709
|
+
`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
|
|
710
|
+
'im'
|
|
722
711
|
);
|
|
712
|
+
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
|
|
713
|
+
const cells = fullRow.split('|').slice(1, -1);
|
|
714
|
+
if (cells.length === 5) {
|
|
715
|
+
// 5-col: Phase | Milestone | Plans | Status | Completed
|
|
716
|
+
cells[2] = ` ${summaryCount}/${planCount} `;
|
|
717
|
+
cells[3] = ' Complete ';
|
|
718
|
+
cells[4] = ` ${today} `;
|
|
719
|
+
} else if (cells.length === 4) {
|
|
720
|
+
// 4-col: Phase | Plans | Status | Completed
|
|
721
|
+
cells[1] = ` ${summaryCount}/${planCount} `;
|
|
722
|
+
cells[2] = ' Complete ';
|
|
723
|
+
cells[3] = ` ${today} `;
|
|
724
|
+
}
|
|
725
|
+
return '|' + cells.join('|') + '|';
|
|
726
|
+
});
|
|
723
727
|
|
|
724
|
-
|
|
725
|
-
const
|
|
728
|
+
// Update plan count in phase section
|
|
729
|
+
const planCountPattern = new RegExp(
|
|
730
|
+
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
|
731
|
+
'i'
|
|
732
|
+
);
|
|
733
|
+
roadmapContent = replaceInCurrentMilestone(
|
|
734
|
+
roadmapContent, planCountPattern,
|
|
735
|
+
`$1${summaryCount}/${planCount} plans complete`
|
|
736
|
+
);
|
|
726
737
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
738
|
+
// Mark completed plan checkboxes (safety net for missed per-plan updates)
|
|
739
|
+
// Handles both plain IDs ("- [ ] 01-01-PLAN.md") and bold-wrapped IDs ("- [ ] **01-01**")
|
|
740
|
+
for (const summaryFile of phaseInfo.summaries) {
|
|
741
|
+
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
742
|
+
if (!planId) continue;
|
|
743
|
+
const planEscaped = escapeRegex(planId);
|
|
744
|
+
const planCheckboxPattern = new RegExp(
|
|
745
|
+
`(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`,
|
|
746
|
+
'i'
|
|
747
|
+
);
|
|
748
|
+
roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
|
|
749
|
+
}
|
|
730
750
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
751
|
+
fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
|
|
752
|
+
|
|
753
|
+
// Update REQUIREMENTS.md traceability for this phase's requirements
|
|
754
|
+
const reqPath = path.join(planningDir(cwd), 'REQUIREMENTS.md');
|
|
755
|
+
if (fs.existsSync(reqPath)) {
|
|
756
|
+
// Extract the current phase section from roadmap (scoped to avoid cross-phase matching)
|
|
757
|
+
const phaseEsc = escapeRegex(phaseNum);
|
|
758
|
+
const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
|
|
759
|
+
const phaseSectionMatch = currentMilestoneRoadmap.match(
|
|
760
|
+
new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i')
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
|
|
764
|
+
const reqMatch = sectionText.match(/\*\*Requirements:\*\*\s*([^\n]+)/i);
|
|
765
|
+
|
|
766
|
+
if (reqMatch) {
|
|
767
|
+
const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(r => r.trim()).filter(Boolean);
|
|
768
|
+
let reqContent = fs.readFileSync(reqPath, 'utf-8');
|
|
769
|
+
|
|
770
|
+
for (const reqId of reqIds) {
|
|
771
|
+
const reqEscaped = escapeRegex(reqId);
|
|
772
|
+
// Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
|
|
773
|
+
reqContent = reqContent.replace(
|
|
774
|
+
new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
|
|
775
|
+
'$1x$2'
|
|
776
|
+
);
|
|
777
|
+
// Update traceability table: | REQ-ID | Phase N | Pending/In Progress | → | REQ-ID | Phase N | Complete |
|
|
778
|
+
reqContent = reqContent.replace(
|
|
779
|
+
new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*(?:Pending|In Progress)\\s*(\\|)`, 'gi'),
|
|
780
|
+
'$1 Complete $2'
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
fs.writeFileSync(reqPath, reqContent, 'utf-8');
|
|
785
|
+
requirementsUpdated = true;
|
|
743
786
|
}
|
|
744
|
-
|
|
745
|
-
fs.writeFileSync(reqPath, reqContent, 'utf-8');
|
|
746
|
-
requirementsUpdated = true;
|
|
747
787
|
}
|
|
748
|
-
}
|
|
788
|
+
});
|
|
749
789
|
}
|
|
750
790
|
|
|
751
791
|
// Find next phase — check both filesystem AND roadmap
|
|
@@ -855,6 +895,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
855
895
|
}
|
|
856
896
|
}
|
|
857
897
|
|
|
898
|
+
// Gate 4: Update Performance Metrics section (#1627)
|
|
899
|
+
stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
|
|
900
|
+
|
|
858
901
|
writeStateMd(statePath, stateContent, cwd);
|
|
859
902
|
}
|
|
860
903
|
|