gsd-opencode 1.22.1 → 1.30.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-advisor-researcher.md +112 -0
- package/agents/gsd-assumptions-analyzer.md +110 -0
- package/agents/gsd-codebase-mapper.md +0 -2
- package/agents/gsd-debugger.md +118 -2
- package/agents/gsd-executor.md +24 -4
- package/agents/gsd-integration-checker.md +0 -2
- package/agents/gsd-nyquist-auditor.md +0 -2
- package/agents/gsd-phase-researcher.md +150 -5
- package/agents/gsd-plan-checker.md +70 -5
- package/agents/gsd-planner.md +49 -4
- package/agents/gsd-project-researcher.md +28 -3
- package/agents/gsd-research-synthesizer.md +0 -2
- package/agents/gsd-roadmapper.md +29 -2
- package/agents/gsd-ui-auditor.md +445 -0
- package/agents/gsd-ui-checker.md +305 -0
- package/agents/gsd-ui-researcher.md +368 -0
- package/agents/gsd-user-profiler.md +173 -0
- package/agents/gsd-verifier.md +123 -4
- package/commands/gsd/gsd-add-backlog.md +76 -0
- package/commands/gsd/gsd-audit-uat.md +24 -0
- package/commands/gsd/gsd-autonomous.md +41 -0
- package/commands/gsd/gsd-debug.md +5 -0
- package/commands/gsd/gsd-discuss-phase.md +10 -36
- package/commands/gsd/gsd-do.md +30 -0
- package/commands/gsd/gsd-execute-phase.md +20 -2
- package/commands/gsd/gsd-fast.md +30 -0
- package/commands/gsd/gsd-forensics.md +56 -0
- package/commands/gsd/gsd-list-workspaces.md +19 -0
- package/commands/gsd/gsd-manager.md +39 -0
- package/commands/gsd/gsd-milestone-summary.md +51 -0
- package/commands/gsd/gsd-new-workspace.md +44 -0
- package/commands/gsd/gsd-next.md +24 -0
- package/commands/gsd/gsd-note.md +34 -0
- package/commands/gsd/gsd-plan-phase.md +3 -1
- package/commands/gsd/gsd-plant-seed.md +28 -0
- package/commands/gsd/gsd-pr-branch.md +25 -0
- package/commands/gsd/gsd-profile-user.md +46 -0
- package/commands/gsd/gsd-quick.md +4 -2
- package/commands/gsd/gsd-reapply-patches.md +9 -8
- package/commands/gsd/gsd-remove-workspace.md +26 -0
- package/commands/gsd/gsd-research-phase.md +5 -0
- package/commands/gsd/gsd-review-backlog.md +61 -0
- package/commands/gsd/gsd-review.md +37 -0
- package/commands/gsd/gsd-session-report.md +19 -0
- package/commands/gsd/gsd-set-profile.md +24 -23
- package/commands/gsd/gsd-ship.md +23 -0
- package/commands/gsd/gsd-stats.md +18 -0
- package/commands/gsd/gsd-thread.md +127 -0
- package/commands/gsd/gsd-ui-phase.md +34 -0
- package/commands/gsd/gsd-ui-review.md +32 -0
- package/commands/gsd/gsd-workstreams.md +66 -0
- package/get-shit-done/bin/gsd-tools.cjs +410 -84
- package/get-shit-done/bin/lib/commands.cjs +429 -18
- package/get-shit-done/bin/lib/config.cjs +318 -45
- package/get-shit-done/bin/lib/core.cjs +822 -84
- package/get-shit-done/bin/lib/frontmatter.cjs +78 -41
- package/get-shit-done/bin/lib/init.cjs +836 -104
- package/get-shit-done/bin/lib/milestone.cjs +44 -33
- package/get-shit-done/bin/lib/model-profiles.cjs +68 -0
- package/get-shit-done/bin/lib/phase.cjs +293 -306
- package/get-shit-done/bin/lib/profile-output.cjs +952 -0
- package/get-shit-done/bin/lib/profile-pipeline.cjs +539 -0
- package/get-shit-done/bin/lib/roadmap.cjs +55 -24
- package/get-shit-done/bin/lib/security.cjs +382 -0
- package/get-shit-done/bin/lib/state.cjs +363 -53
- package/get-shit-done/bin/lib/template.cjs +2 -2
- package/get-shit-done/bin/lib/uat.cjs +282 -0
- package/get-shit-done/bin/lib/verify.cjs +104 -36
- package/get-shit-done/bin/lib/workstream.cjs +491 -0
- package/get-shit-done/references/checkpoints.md +12 -10
- package/get-shit-done/references/decimal-phase-calculation.md +2 -3
- package/get-shit-done/references/git-integration.md +47 -0
- package/get-shit-done/references/model-profile-resolution.md +2 -0
- package/get-shit-done/references/model-profiles.md +62 -16
- package/get-shit-done/references/phase-argument-parsing.md +2 -2
- package/get-shit-done/references/planning-config.md +3 -1
- package/get-shit-done/references/user-profiling.md +681 -0
- package/get-shit-done/references/workstream-flag.md +58 -0
- package/get-shit-done/templates/UAT.md +21 -3
- package/get-shit-done/templates/UI-SPEC.md +100 -0
- package/get-shit-done/templates/claude-md.md +122 -0
- package/get-shit-done/templates/config.json +10 -3
- package/get-shit-done/templates/context.md +61 -6
- package/get-shit-done/templates/dev-preferences.md +21 -0
- package/get-shit-done/templates/discussion-log.md +63 -0
- package/get-shit-done/templates/phase-prompt.md +46 -5
- package/get-shit-done/templates/project.md +2 -0
- package/get-shit-done/templates/state.md +2 -2
- package/get-shit-done/templates/user-profile.md +146 -0
- package/get-shit-done/workflows/add-phase.md +2 -2
- package/get-shit-done/workflows/add-tests.md +4 -4
- package/get-shit-done/workflows/add-todo.md +3 -3
- package/get-shit-done/workflows/audit-milestone.md +13 -5
- package/get-shit-done/workflows/audit-uat.md +109 -0
- package/get-shit-done/workflows/autonomous.md +891 -0
- package/get-shit-done/workflows/check-todos.md +2 -2
- package/get-shit-done/workflows/cleanup.md +4 -4
- package/get-shit-done/workflows/complete-milestone.md +9 -6
- package/get-shit-done/workflows/diagnose-issues.md +15 -3
- package/get-shit-done/workflows/discovery-phase.md +3 -3
- package/get-shit-done/workflows/discuss-phase-assumptions.md +653 -0
- package/get-shit-done/workflows/discuss-phase.md +411 -38
- package/get-shit-done/workflows/do.md +104 -0
- package/get-shit-done/workflows/execute-phase.md +405 -18
- package/get-shit-done/workflows/execute-plan.md +77 -12
- package/get-shit-done/workflows/fast.md +105 -0
- package/get-shit-done/workflows/forensics.md +265 -0
- package/get-shit-done/workflows/health.md +28 -6
- package/get-shit-done/workflows/help.md +124 -7
- package/get-shit-done/workflows/insert-phase.md +2 -2
- package/get-shit-done/workflows/list-phase-assumptions.md +2 -2
- package/get-shit-done/workflows/list-workspaces.md +56 -0
- package/get-shit-done/workflows/manager.md +362 -0
- package/get-shit-done/workflows/map-codebase.md +74 -13
- package/get-shit-done/workflows/milestone-summary.md +223 -0
- package/get-shit-done/workflows/new-milestone.md +120 -18
- package/get-shit-done/workflows/new-project.md +178 -39
- package/get-shit-done/workflows/new-workspace.md +237 -0
- package/get-shit-done/workflows/next.md +97 -0
- package/get-shit-done/workflows/node-repair.md +92 -0
- package/get-shit-done/workflows/note.md +156 -0
- package/get-shit-done/workflows/pause-work.md +62 -8
- package/get-shit-done/workflows/plan-milestone-gaps.md +4 -5
- package/get-shit-done/workflows/plan-phase.md +332 -33
- package/get-shit-done/workflows/plant-seed.md +169 -0
- package/get-shit-done/workflows/pr-branch.md +129 -0
- package/get-shit-done/workflows/profile-user.md +450 -0
- package/get-shit-done/workflows/progress.md +145 -20
- package/get-shit-done/workflows/quick.md +205 -49
- package/get-shit-done/workflows/remove-phase.md +2 -2
- package/get-shit-done/workflows/remove-workspace.md +90 -0
- package/get-shit-done/workflows/research-phase.md +11 -3
- package/get-shit-done/workflows/resume-project.md +35 -16
- package/get-shit-done/workflows/review.md +228 -0
- package/get-shit-done/workflows/session-report.md +146 -0
- package/get-shit-done/workflows/set-profile.md +2 -2
- package/get-shit-done/workflows/settings.md +79 -10
- package/get-shit-done/workflows/ship.md +228 -0
- package/get-shit-done/workflows/stats.md +60 -0
- package/get-shit-done/workflows/transition.md +147 -20
- package/get-shit-done/workflows/ui-phase.md +302 -0
- package/get-shit-done/workflows/ui-review.md +165 -0
- package/get-shit-done/workflows/update.md +108 -25
- package/get-shit-done/workflows/validate-phase.md +15 -8
- package/get-shit-done/workflows/verify-phase.md +16 -5
- package/get-shit-done/workflows/verify-work.md +72 -18
- package/package.json +1 -1
- package/skills/gsd-audit-milestone/SKILL.md +29 -0
- package/skills/gsd-cleanup/SKILL.md +19 -0
- package/skills/gsd-complete-milestone/SKILL.md +131 -0
- package/skills/gsd-discuss-phase/SKILL.md +54 -0
- package/skills/gsd-execute-phase/SKILL.md +49 -0
- package/skills/gsd-plan-phase/SKILL.md +37 -0
- package/skills/gsd-ui-phase/SKILL.md +24 -0
- package/skills/gsd-ui-review/SKILL.md +24 -0
- package/skills/gsd-verify-work/SKILL.md +30 -0
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, toPosixPath, output, error } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, loadConfig, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, planningDir, output, error, readSubdirectories } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
|
-
const { writeStateMd } = require('./state.cjs');
|
|
9
|
+
const { writeStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback } = require('./state.cjs');
|
|
10
10
|
|
|
11
11
|
function cmdPhasesList(cwd, options, raw) {
|
|
12
|
-
const phasesDir = path.join(cwd, '
|
|
12
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
13
13
|
const { type, phase, includeArchived } = options;
|
|
14
14
|
|
|
15
15
|
// If no phases directory, return empty
|
|
@@ -85,7 +85,7 @@ function cmdPhasesList(cwd, options, raw) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
function cmdPhaseNextDecimal(cwd, basePhase, raw) {
|
|
88
|
-
const phasesDir = path.join(cwd, '
|
|
88
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
89
89
|
const normalized = normalizePhaseName(basePhase);
|
|
90
90
|
|
|
91
91
|
// Check if phases directory exists
|
|
@@ -154,7 +154,7 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
154
154
|
error('phase identifier required');
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
const phasesDir = path.join(cwd, '
|
|
157
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
158
158
|
const normalized = normalizePhaseName(phase);
|
|
159
159
|
|
|
160
160
|
const notFound = { found: false, directory: null, phase_number: null, phase_name: null, plans: [], summaries: [] };
|
|
@@ -180,7 +180,7 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
180
180
|
|
|
181
181
|
const result = {
|
|
182
182
|
found: true,
|
|
183
|
-
directory: toPosixPath(path.join(
|
|
183
|
+
directory: toPosixPath(path.join(path.relative(cwd, planningDir(cwd)), 'phases', match)),
|
|
184
184
|
phase_number: phaseNumber,
|
|
185
185
|
phase_name: phaseName,
|
|
186
186
|
plans,
|
|
@@ -203,7 +203,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
203
203
|
error('phase required for phase-plan-index');
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
const phasesDir = path.join(cwd, '
|
|
206
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
207
207
|
const normalized = normalizePhaseName(phase);
|
|
208
208
|
|
|
209
209
|
// Find phase directory
|
|
@@ -308,60 +308,75 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
308
308
|
output(result, raw);
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
function cmdPhaseAdd(cwd, description, raw) {
|
|
311
|
+
function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
312
312
|
if (!description) {
|
|
313
313
|
error('description required for phase add');
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
const
|
|
316
|
+
const config = loadConfig(cwd);
|
|
317
|
+
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
|
317
318
|
if (!fs.existsSync(roadmapPath)) {
|
|
318
319
|
error('ROADMAP.md not found');
|
|
319
320
|
}
|
|
320
321
|
|
|
321
|
-
const
|
|
322
|
+
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
323
|
+
const content = extractCurrentMilestone(rawContent, cwd);
|
|
322
324
|
const slug = generateSlugInternal(description);
|
|
323
325
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (
|
|
326
|
+
let newPhaseId;
|
|
327
|
+
let dirName;
|
|
328
|
+
|
|
329
|
+
if (customId || config.phase_naming === 'custom') {
|
|
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
|
+
}
|
|
343
|
+
|
|
344
|
+
newPhaseId = maxPhase + 1;
|
|
345
|
+
const paddedNum = String(newPhaseId).padStart(2, '0');
|
|
346
|
+
dirName = `${paddedNum}-${slug}`;
|
|
331
347
|
}
|
|
332
348
|
|
|
333
|
-
const
|
|
334
|
-
const paddedNum = String(newPhaseNum).padStart(2, '0');
|
|
335
|
-
const dirName = `${paddedNum}-${slug}`;
|
|
336
|
-
const dirPath = path.join(cwd, '.planning', 'phases', dirName);
|
|
349
|
+
const dirPath = path.join(planningDir(cwd), 'phases', dirName);
|
|
337
350
|
|
|
338
351
|
// Create directory with .gitkeep so git tracks empty folders
|
|
339
352
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
340
353
|
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
|
|
341
354
|
|
|
342
355
|
// Build phase entry
|
|
343
|
-
const
|
|
356
|
+
const dependsOn = config.phase_naming === 'custom' ? '' : `\n**Depends on:** Phase ${typeof newPhaseId === 'number' ? newPhaseId - 1 : 'TBD'}`;
|
|
357
|
+
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`;
|
|
344
358
|
|
|
345
359
|
// Find insertion point: before last "---" or at end
|
|
346
360
|
let updatedContent;
|
|
347
|
-
const lastSeparator =
|
|
361
|
+
const lastSeparator = rawContent.lastIndexOf('\n---');
|
|
348
362
|
if (lastSeparator > 0) {
|
|
349
|
-
updatedContent =
|
|
363
|
+
updatedContent = rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator);
|
|
350
364
|
} else {
|
|
351
|
-
updatedContent =
|
|
365
|
+
updatedContent = rawContent + phaseEntry;
|
|
352
366
|
}
|
|
353
367
|
|
|
354
368
|
fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
|
|
355
369
|
|
|
356
370
|
const result = {
|
|
357
|
-
phase_number:
|
|
358
|
-
padded:
|
|
371
|
+
phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
|
|
372
|
+
padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
|
|
359
373
|
name: description,
|
|
360
374
|
slug,
|
|
361
|
-
directory:
|
|
375
|
+
directory: toPosixPath(path.join(path.relative(cwd, planningDir(cwd)), 'phases', dirName)),
|
|
376
|
+
naming_mode: config.phase_naming,
|
|
362
377
|
};
|
|
363
378
|
|
|
364
|
-
output(result, raw,
|
|
379
|
+
output(result, raw, result.padded);
|
|
365
380
|
}
|
|
366
381
|
|
|
367
382
|
function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
@@ -369,12 +384,13 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
369
384
|
error('after-phase and description required for phase insert');
|
|
370
385
|
}
|
|
371
386
|
|
|
372
|
-
const roadmapPath = path.join(cwd, '
|
|
387
|
+
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
|
373
388
|
if (!fs.existsSync(roadmapPath)) {
|
|
374
389
|
error('ROADMAP.md not found');
|
|
375
390
|
}
|
|
376
391
|
|
|
377
|
-
const
|
|
392
|
+
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
393
|
+
const content = extractCurrentMilestone(rawContent, cwd);
|
|
378
394
|
const slug = generateSlugInternal(description);
|
|
379
395
|
|
|
380
396
|
// Normalize input then strip leading zeros for flexible matching
|
|
@@ -387,7 +403,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
387
403
|
}
|
|
388
404
|
|
|
389
405
|
// Calculate next decimal using existing logic
|
|
390
|
-
const phasesDir = path.join(cwd, '
|
|
406
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
391
407
|
const normalizedBase = normalizePhaseName(afterPhase);
|
|
392
408
|
let existingDecimals = [];
|
|
393
409
|
|
|
@@ -399,12 +415,12 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
399
415
|
const dm = dir.match(decimalPattern);
|
|
400
416
|
if (dm) existingDecimals.push(parseInt(dm[1], 10));
|
|
401
417
|
}
|
|
402
|
-
} catch {}
|
|
418
|
+
} catch { /* intentionally empty */ }
|
|
403
419
|
|
|
404
420
|
const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1;
|
|
405
421
|
const decimalPhase = `${normalizedBase}.${nextDecimal}`;
|
|
406
422
|
const dirName = `${decimalPhase}-${slug}`;
|
|
407
|
-
const dirPath = path.join(cwd, '
|
|
423
|
+
const dirPath = path.join(planningDir(cwd), 'phases', dirName);
|
|
408
424
|
|
|
409
425
|
// Create directory with .gitkeep so git tracks empty folders
|
|
410
426
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
@@ -415,23 +431,23 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
415
431
|
|
|
416
432
|
// Insert after the target phase section
|
|
417
433
|
const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
|
|
418
|
-
const headerMatch =
|
|
434
|
+
const headerMatch = rawContent.match(headerPattern);
|
|
419
435
|
if (!headerMatch) {
|
|
420
436
|
error(`Could not find Phase ${afterPhase} header`);
|
|
421
437
|
}
|
|
422
438
|
|
|
423
|
-
const headerIdx =
|
|
424
|
-
const afterHeader =
|
|
439
|
+
const headerIdx = rawContent.indexOf(headerMatch[0]);
|
|
440
|
+
const afterHeader = rawContent.slice(headerIdx + headerMatch[0].length);
|
|
425
441
|
const nextPhaseMatch = afterHeader.match(/\n#{2,4}\s+Phase\s+\d/i);
|
|
426
442
|
|
|
427
443
|
let insertIdx;
|
|
428
444
|
if (nextPhaseMatch) {
|
|
429
445
|
insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
|
|
430
446
|
} else {
|
|
431
|
-
insertIdx =
|
|
447
|
+
insertIdx = rawContent.length;
|
|
432
448
|
}
|
|
433
449
|
|
|
434
|
-
const updatedContent =
|
|
450
|
+
const updatedContent = rawContent.slice(0, insertIdx) + phaseEntry + rawContent.slice(insertIdx);
|
|
435
451
|
fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
|
|
436
452
|
|
|
437
453
|
const result = {
|
|
@@ -439,263 +455,175 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
439
455
|
after_phase: afterPhase,
|
|
440
456
|
name: description,
|
|
441
457
|
slug,
|
|
442
|
-
directory:
|
|
458
|
+
directory: toPosixPath(path.join(path.relative(cwd, planningDir(cwd)), 'phases', dirName)),
|
|
443
459
|
};
|
|
444
460
|
|
|
445
461
|
output(result, raw, decimalPhase);
|
|
446
462
|
}
|
|
447
463
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
464
|
+
/**
|
|
465
|
+
* Renumber sibling decimal phases after a decimal phase is removed.
|
|
466
|
+
* e.g. removing 06.2 → 06.3 becomes 06.2, 06.4 becomes 06.3, etc.
|
|
467
|
+
* Returns { renamedDirs, renamedFiles }.
|
|
468
|
+
*/
|
|
469
|
+
function renameDecimalPhases(phasesDir, baseInt, removedDecimal) {
|
|
470
|
+
const renamedDirs = [], renamedFiles = [];
|
|
471
|
+
const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
|
|
472
|
+
const dirs = readSubdirectories(phasesDir, true);
|
|
473
|
+
const toRename = dirs
|
|
474
|
+
.map(dir => { const m = dir.match(decPattern); return m ? { dir, oldDecimal: parseInt(m[1], 10), slug: m[2] } : null; })
|
|
475
|
+
.filter(item => item && item.oldDecimal > removedDecimal)
|
|
476
|
+
.sort((a, b) => b.oldDecimal - a.oldDecimal); // descending to avoid conflicts
|
|
477
|
+
|
|
478
|
+
for (const item of toRename) {
|
|
479
|
+
const newDecimal = item.oldDecimal - 1;
|
|
480
|
+
const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
|
|
481
|
+
const newPhaseId = `${baseInt}.${newDecimal}`;
|
|
482
|
+
const newDirName = `${baseInt}.${newDecimal}-${item.slug}`;
|
|
483
|
+
fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
|
|
484
|
+
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
485
|
+
for (const f of fs.readdirSync(path.join(phasesDir, newDirName))) {
|
|
486
|
+
if (f.includes(oldPhaseId)) {
|
|
487
|
+
const newFileName = f.replace(oldPhaseId, newPhaseId);
|
|
488
|
+
fs.renameSync(path.join(phasesDir, newDirName, f), path.join(phasesDir, newDirName, newFileName));
|
|
489
|
+
renamedFiles.push({ from: f, to: newFileName });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
451
492
|
}
|
|
493
|
+
return { renamedDirs, renamedFiles };
|
|
494
|
+
}
|
|
452
495
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
496
|
+
/**
|
|
497
|
+
* Renumber all integer phases after removedInt.
|
|
498
|
+
* e.g. removing phase 5 → phase 6 becomes 5, phase 7 becomes 6, etc.
|
|
499
|
+
* Returns { renamedDirs, renamedFiles }.
|
|
500
|
+
*/
|
|
501
|
+
function renameIntegerPhases(phasesDir, removedInt) {
|
|
502
|
+
const renamedDirs = [], renamedFiles = [];
|
|
503
|
+
const dirs = readSubdirectories(phasesDir, true);
|
|
504
|
+
const toRename = dirs
|
|
505
|
+
.map(dir => {
|
|
506
|
+
const m = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
|
|
507
|
+
if (!m) return null;
|
|
508
|
+
const dirInt = parseInt(m[1], 10);
|
|
509
|
+
return dirInt > removedInt ? { dir, oldInt: dirInt, letter: m[2] ? m[2].toUpperCase() : '', decimal: m[3] ? parseInt(m[3], 10) : null, slug: m[4] } : null;
|
|
510
|
+
})
|
|
511
|
+
.filter(Boolean)
|
|
512
|
+
.sort((a, b) => a.oldInt !== b.oldInt ? b.oldInt - a.oldInt : (b.decimal || 0) - (a.decimal || 0));
|
|
513
|
+
|
|
514
|
+
for (const item of toRename) {
|
|
515
|
+
const newInt = item.oldInt - 1;
|
|
516
|
+
const newPadded = String(newInt).padStart(2, '0');
|
|
517
|
+
const oldPadded = String(item.oldInt).padStart(2, '0');
|
|
518
|
+
const letterSuffix = item.letter || '';
|
|
519
|
+
const decimalSuffix = item.decimal !== null ? `.${item.decimal}` : '';
|
|
520
|
+
const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
|
|
521
|
+
const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
|
|
522
|
+
const newDirName = `${newPrefix}-${item.slug}`;
|
|
523
|
+
fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
|
|
524
|
+
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
525
|
+
for (const f of fs.readdirSync(path.join(phasesDir, newDirName))) {
|
|
526
|
+
if (f.startsWith(oldPrefix)) {
|
|
527
|
+
const newFileName = newPrefix + f.slice(oldPrefix.length);
|
|
528
|
+
fs.renameSync(path.join(phasesDir, newDirName, f), path.join(phasesDir, newDirName, newFileName));
|
|
529
|
+
renamedFiles.push({ from: f, to: newFileName });
|
|
530
|
+
}
|
|
531
|
+
}
|
|
459
532
|
}
|
|
533
|
+
return { renamedDirs, renamedFiles };
|
|
534
|
+
}
|
|
460
535
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
536
|
+
/**
|
|
537
|
+
* Remove a phase section from ROADMAP.md and renumber all subsequent integer phases.
|
|
538
|
+
*/
|
|
539
|
+
function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, removedInt) {
|
|
540
|
+
let content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
541
|
+
const escaped = escapeRegex(targetPhase);
|
|
464
542
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
469
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
470
|
-
targetDir = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
|
|
471
|
-
} catch {}
|
|
543
|
+
content = content.replace(new RegExp(`\\n?#{2,4}\\s*Phase\\s+${escaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`, 'i'), '');
|
|
544
|
+
content = content.replace(new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${escaped}[:\\s][^\\n]*`, 'gi'), '');
|
|
545
|
+
content = content.replace(new RegExp(`\\n?\\|\\s*${escaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi'), '');
|
|
472
546
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
547
|
+
if (!isDecimal) {
|
|
548
|
+
const MAX_PHASE = 99;
|
|
549
|
+
for (let oldNum = MAX_PHASE; oldNum > removedInt; oldNum--) {
|
|
550
|
+
const newNum = oldNum - 1;
|
|
551
|
+
const oldStr = String(oldNum), newStr = String(newNum);
|
|
552
|
+
const oldPad = oldStr.padStart(2, '0'), newPad = newStr.padStart(2, '0');
|
|
553
|
+
content = content.replace(new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'), `$1${newStr}$2`);
|
|
554
|
+
content = content.replace(new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, 'g'), `$1${newStr}$2`);
|
|
555
|
+
content = content.replace(new RegExp(`${oldPad}-(\\d{2})`, 'g'), `${newPad}-$1`);
|
|
556
|
+
content = content.replace(new RegExp(`(\\|\\s*)${oldStr}\\.\\s`, 'g'), `$1${newStr}. `);
|
|
557
|
+
content = content.replace(new RegExp(`(Depends on:\\*\\*\\s*Phase\\s+)${oldStr}\\b`, 'gi'), `$1${newStr}`);
|
|
480
558
|
}
|
|
481
559
|
}
|
|
482
560
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
fs.rmSync(path.join(phasesDir, targetDir), { recursive: true, force: true });
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Renumber subsequent phases
|
|
489
|
-
const renamedDirs = [];
|
|
490
|
-
const renamedFiles = [];
|
|
491
|
-
|
|
492
|
-
if (isDecimal) {
|
|
493
|
-
// Decimal removal: renumber sibling decimals (e.g., removing 06.2 → 06.3 becomes 06.2)
|
|
494
|
-
const baseParts = normalized.split('.');
|
|
495
|
-
const baseInt = baseParts[0];
|
|
496
|
-
const removedDecimal = parseInt(baseParts[1], 10);
|
|
497
|
-
|
|
498
|
-
try {
|
|
499
|
-
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
500
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
561
|
+
fs.writeFileSync(roadmapPath, content, 'utf-8');
|
|
562
|
+
}
|
|
501
563
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const toRename = [];
|
|
505
|
-
for (const dir of dirs) {
|
|
506
|
-
const dm = dir.match(decPattern);
|
|
507
|
-
if (dm && parseInt(dm[1], 10) > removedDecimal) {
|
|
508
|
-
toRename.push({ dir, oldDecimal: parseInt(dm[1], 10), slug: dm[2] });
|
|
509
|
-
}
|
|
510
|
-
}
|
|
564
|
+
function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
565
|
+
if (!targetPhase) error('phase number required for phase remove');
|
|
511
566
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
for (const item of toRename) {
|
|
516
|
-
const newDecimal = item.oldDecimal - 1;
|
|
517
|
-
const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
|
|
518
|
-
const newPhaseId = `${baseInt}.${newDecimal}`;
|
|
519
|
-
const newDirName = `${baseInt}.${newDecimal}-${item.slug}`;
|
|
520
|
-
|
|
521
|
-
// Rename directory
|
|
522
|
-
fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
|
|
523
|
-
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
524
|
-
|
|
525
|
-
// Rename files inside
|
|
526
|
-
const dirFiles = fs.readdirSync(path.join(phasesDir, newDirName));
|
|
527
|
-
for (const f of dirFiles) {
|
|
528
|
-
// Files may have phase prefix like "06.2-01-PLAN.md"
|
|
529
|
-
if (f.includes(oldPhaseId)) {
|
|
530
|
-
const newFileName = f.replace(oldPhaseId, newPhaseId);
|
|
531
|
-
fs.renameSync(
|
|
532
|
-
path.join(phasesDir, newDirName, f),
|
|
533
|
-
path.join(phasesDir, newDirName, newFileName)
|
|
534
|
-
);
|
|
535
|
-
renamedFiles.push({ from: f, to: newFileName });
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
} catch {}
|
|
567
|
+
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
|
568
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
540
569
|
|
|
541
|
-
|
|
542
|
-
// Integer removal: renumber all subsequent integer phases
|
|
543
|
-
const removedInt = parseInt(normalized, 10);
|
|
570
|
+
if (!fs.existsSync(roadmapPath)) error('ROADMAP.md not found');
|
|
544
571
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
572
|
+
const normalized = normalizePhaseName(targetPhase);
|
|
573
|
+
const isDecimal = targetPhase.includes('.');
|
|
574
|
+
const force = options.force || false;
|
|
548
575
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const dm = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
|
|
553
|
-
if (!dm) continue;
|
|
554
|
-
const dirInt = parseInt(dm[1], 10);
|
|
555
|
-
if (dirInt > removedInt) {
|
|
556
|
-
toRename.push({
|
|
557
|
-
dir,
|
|
558
|
-
oldInt: dirInt,
|
|
559
|
-
letter: dm[2] ? dm[2].toUpperCase() : '',
|
|
560
|
-
decimal: dm[3] ? parseInt(dm[3], 10) : null,
|
|
561
|
-
slug: dm[4],
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
}
|
|
576
|
+
// Find target directory
|
|
577
|
+
const targetDir = readSubdirectories(phasesDir, true)
|
|
578
|
+
.find(d => d.startsWith(normalized + '-') || d === normalized) || null;
|
|
565
579
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const newInt = item.oldInt - 1;
|
|
574
|
-
const newPadded = String(newInt).padStart(2, '0');
|
|
575
|
-
const oldPadded = String(item.oldInt).padStart(2, '0');
|
|
576
|
-
const letterSuffix = item.letter || '';
|
|
577
|
-
const decimalSuffix = item.decimal !== null ? `.${item.decimal}` : '';
|
|
578
|
-
const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
|
|
579
|
-
const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
|
|
580
|
-
const newDirName = `${newPrefix}-${item.slug}`;
|
|
581
|
-
|
|
582
|
-
// Rename directory
|
|
583
|
-
fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
|
|
584
|
-
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
585
|
-
|
|
586
|
-
// Rename files inside
|
|
587
|
-
const dirFiles = fs.readdirSync(path.join(phasesDir, newDirName));
|
|
588
|
-
for (const f of dirFiles) {
|
|
589
|
-
if (f.startsWith(oldPrefix)) {
|
|
590
|
-
const newFileName = newPrefix + f.slice(oldPrefix.length);
|
|
591
|
-
fs.renameSync(
|
|
592
|
-
path.join(phasesDir, newDirName, f),
|
|
593
|
-
path.join(phasesDir, newDirName, newFileName)
|
|
594
|
-
);
|
|
595
|
-
renamedFiles.push({ from: f, to: newFileName });
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
} catch {}
|
|
580
|
+
// Guard against removing executed work
|
|
581
|
+
if (targetDir && !force) {
|
|
582
|
+
const files = fs.readdirSync(path.join(phasesDir, targetDir));
|
|
583
|
+
const summaries = files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
584
|
+
if (summaries.length > 0) {
|
|
585
|
+
error(`Phase ${targetPhase} has ${summaries.length} executed plan(s). Use --force to remove anyway.`);
|
|
586
|
+
}
|
|
600
587
|
}
|
|
601
588
|
|
|
602
|
-
|
|
603
|
-
let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
604
|
-
|
|
605
|
-
// Remove the target phase section
|
|
606
|
-
const targetEscaped = escapeRegex(targetPhase);
|
|
607
|
-
const sectionPattern = new RegExp(
|
|
608
|
-
`\\n?#{2,4}\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`,
|
|
609
|
-
'i'
|
|
610
|
-
);
|
|
611
|
-
roadmapContent = roadmapContent.replace(sectionPattern, '');
|
|
612
|
-
|
|
613
|
-
// Remove from phase list (checkbox)
|
|
614
|
-
const checkboxPattern = new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${targetEscaped}[:\\s][^\\n]*`, 'gi');
|
|
615
|
-
roadmapContent = roadmapContent.replace(checkboxPattern, '');
|
|
616
|
-
|
|
617
|
-
// Remove from progress table
|
|
618
|
-
const tableRowPattern = new RegExp(`\\n?\\|\\s*${targetEscaped}\\.?\\s[^|]*\\|[^\\n]*`, 'gi');
|
|
619
|
-
roadmapContent = roadmapContent.replace(tableRowPattern, '');
|
|
589
|
+
if (targetDir) fs.rmSync(path.join(phasesDir, targetDir), { recursive: true, force: true });
|
|
620
590
|
|
|
621
|
-
// Renumber
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const newStr = String(newNum);
|
|
631
|
-
const oldPad = oldStr.padStart(2, '0');
|
|
632
|
-
const newPad = newStr.padStart(2, '0');
|
|
633
|
-
|
|
634
|
-
// Phase headings: ## Phase 18: or ### Phase 18: → ## Phase 17: or ### Phase 17:
|
|
635
|
-
roadmapContent = roadmapContent.replace(
|
|
636
|
-
new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'),
|
|
637
|
-
`$1${newStr}$2`
|
|
638
|
-
);
|
|
639
|
-
|
|
640
|
-
// Checkbox items: - [ ] **Phase 18:** → - [ ] **Phase 17:**
|
|
641
|
-
roadmapContent = roadmapContent.replace(
|
|
642
|
-
new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, 'g'),
|
|
643
|
-
`$1${newStr}$2`
|
|
644
|
-
);
|
|
645
|
-
|
|
646
|
-
// Plan references: 18-01 → 17-01
|
|
647
|
-
roadmapContent = roadmapContent.replace(
|
|
648
|
-
new RegExp(`${oldPad}-(\\d{2})`, 'g'),
|
|
649
|
-
`${newPad}-$1`
|
|
650
|
-
);
|
|
651
|
-
|
|
652
|
-
// Table rows: | 18. → | 17.
|
|
653
|
-
roadmapContent = roadmapContent.replace(
|
|
654
|
-
new RegExp(`(\\|\\s*)${oldStr}\\.\\s`, 'g'),
|
|
655
|
-
`$1${newStr}. `
|
|
656
|
-
);
|
|
657
|
-
|
|
658
|
-
// Depends on references
|
|
659
|
-
roadmapContent = roadmapContent.replace(
|
|
660
|
-
new RegExp(`(Depends on:\\*\\*\\s*Phase\\s+)${oldStr}\\b`, 'gi'),
|
|
661
|
-
`$1${newStr}`
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
591
|
+
// Renumber subsequent phases on disk
|
|
592
|
+
let renamedDirs = [], renamedFiles = [];
|
|
593
|
+
try {
|
|
594
|
+
const renamed = isDecimal
|
|
595
|
+
? renameDecimalPhases(phasesDir, normalized.split('.')[0], parseInt(normalized.split('.')[1], 10))
|
|
596
|
+
: renameIntegerPhases(phasesDir, parseInt(normalized, 10));
|
|
597
|
+
renamedDirs = renamed.renamedDirs;
|
|
598
|
+
renamedFiles = renamed.renamedFiles;
|
|
599
|
+
} catch { /* intentionally empty */ }
|
|
665
600
|
|
|
666
|
-
|
|
601
|
+
// Update ROADMAP.md
|
|
602
|
+
updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, parseInt(normalized, 10));
|
|
667
603
|
|
|
668
604
|
// Update STATE.md phase count
|
|
669
|
-
const statePath = path.join(cwd, '
|
|
605
|
+
const statePath = path.join(planningDir(cwd), 'STATE.md');
|
|
670
606
|
if (fs.existsSync(statePath)) {
|
|
671
607
|
let stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if (totalMatch) {
|
|
676
|
-
const oldTotal = parseInt(totalMatch[2], 10);
|
|
677
|
-
stateContent = stateContent.replace(totalPattern, `$1${oldTotal - 1}`);
|
|
608
|
+
const totalRaw = stateExtractField(stateContent, 'Total Phases');
|
|
609
|
+
if (totalRaw) {
|
|
610
|
+
stateContent = stateReplaceField(stateContent, 'Total Phases', String(parseInt(totalRaw, 10) - 1)) || stateContent;
|
|
678
611
|
}
|
|
679
|
-
|
|
680
|
-
const ofPattern = /(\bof\s+)(\d+)(\s*(?:\(|phases?))/i;
|
|
681
|
-
const ofMatch = stateContent.match(ofPattern);
|
|
612
|
+
const ofMatch = stateContent.match(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i);
|
|
682
613
|
if (ofMatch) {
|
|
683
|
-
|
|
684
|
-
stateContent = stateContent.replace(ofPattern, `$1${oldTotal - 1}$3`);
|
|
614
|
+
stateContent = stateContent.replace(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i, `$1${parseInt(ofMatch[2], 10) - 1}$3`);
|
|
685
615
|
}
|
|
686
616
|
writeStateMd(statePath, stateContent, cwd);
|
|
687
617
|
}
|
|
688
618
|
|
|
689
|
-
|
|
619
|
+
output({
|
|
690
620
|
removed: targetPhase,
|
|
691
|
-
directory_deleted: targetDir
|
|
621
|
+
directory_deleted: targetDir,
|
|
692
622
|
renamed_directories: renamedDirs,
|
|
693
623
|
renamed_files: renamedFiles,
|
|
694
624
|
roadmap_updated: true,
|
|
695
625
|
state_updated: fs.existsSync(statePath),
|
|
696
|
-
};
|
|
697
|
-
|
|
698
|
-
output(result, raw);
|
|
626
|
+
}, raw);
|
|
699
627
|
}
|
|
700
628
|
|
|
701
629
|
function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
@@ -703,9 +631,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
703
631
|
error('phase number required for phase complete');
|
|
704
632
|
}
|
|
705
633
|
|
|
706
|
-
const roadmapPath = path.join(cwd, '
|
|
707
|
-
const statePath = path.join(cwd, '
|
|
708
|
-
const phasesDir = path.join(cwd, '
|
|
634
|
+
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
|
635
|
+
const statePath = path.join(planningDir(cwd), 'STATE.md');
|
|
636
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
709
637
|
const normalized = normalizePhaseName(phaseNum);
|
|
710
638
|
const today = new Date().toISOString().split('T')[0];
|
|
711
639
|
|
|
@@ -717,6 +645,28 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
717
645
|
|
|
718
646
|
const planCount = phaseInfo.plans.length;
|
|
719
647
|
const summaryCount = phaseInfo.summaries.length;
|
|
648
|
+
let requirementsUpdated = false;
|
|
649
|
+
|
|
650
|
+
// Check for unresolved verification debt (non-blocking warnings)
|
|
651
|
+
const warnings = [];
|
|
652
|
+
try {
|
|
653
|
+
const phaseFullDir = path.join(cwd, phaseInfo.directory);
|
|
654
|
+
const phaseFiles = fs.readdirSync(phaseFullDir);
|
|
655
|
+
|
|
656
|
+
for (const file of phaseFiles.filter(f => f.includes('-UAT') && f.endsWith('.md'))) {
|
|
657
|
+
const content = fs.readFileSync(path.join(phaseFullDir, file), 'utf-8');
|
|
658
|
+
if (/result: pending/.test(content)) warnings.push(`${file}: has pending tests`);
|
|
659
|
+
if (/result: blocked/.test(content)) warnings.push(`${file}: has blocked tests`);
|
|
660
|
+
if (/status: partial/.test(content)) warnings.push(`${file}: testing incomplete (partial)`);
|
|
661
|
+
if (/status: diagnosed/.test(content)) warnings.push(`${file}: has diagnosed gaps`);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
for (const file of phaseFiles.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
|
|
665
|
+
const content = fs.readFileSync(path.join(phaseFullDir, file), 'utf-8');
|
|
666
|
+
if (/status: human_needed/.test(content)) warnings.push(`${file}: needs human verification`);
|
|
667
|
+
if (/status: gaps_found/.test(content)) warnings.push(`${file}: has unresolved gaps`);
|
|
668
|
+
}
|
|
669
|
+
} catch {}
|
|
720
670
|
|
|
721
671
|
// Update ROADMAP.md: mark phase complete
|
|
722
672
|
if (fs.existsSync(roadmapPath)) {
|
|
@@ -727,39 +677,53 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
727
677
|
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
|
|
728
678
|
'i'
|
|
729
679
|
);
|
|
730
|
-
roadmapContent = roadmapContent
|
|
680
|
+
roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
|
|
731
681
|
|
|
732
|
-
// Progress table: update Status to Complete, add date
|
|
682
|
+
// Progress table: update Status to Complete, add date (handles 4 or 5 column tables)
|
|
733
683
|
const phaseEscaped = escapeRegex(phaseNum);
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
'
|
|
737
|
-
);
|
|
738
|
-
roadmapContent = roadmapContent.replace(
|
|
739
|
-
tablePattern,
|
|
740
|
-
`$1 Complete $2 ${today} $3`
|
|
684
|
+
const tableRowPattern = new RegExp(
|
|
685
|
+
`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
|
|
686
|
+
'im'
|
|
741
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
|
+
});
|
|
742
701
|
|
|
743
702
|
// Update plan count in phase section
|
|
744
703
|
const planCountPattern = new RegExp(
|
|
745
704
|
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
|
746
705
|
'i'
|
|
747
706
|
);
|
|
748
|
-
roadmapContent =
|
|
749
|
-
planCountPattern,
|
|
707
|
+
roadmapContent = replaceInCurrentMilestone(
|
|
708
|
+
roadmapContent, planCountPattern,
|
|
750
709
|
`$1${summaryCount}/${planCount} plans complete`
|
|
751
710
|
);
|
|
752
711
|
|
|
753
712
|
fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
|
|
754
713
|
|
|
755
714
|
// Update REQUIREMENTS.md traceability for this phase's requirements
|
|
756
|
-
const reqPath = path.join(cwd, '
|
|
715
|
+
const reqPath = path.join(planningDir(cwd), 'REQUIREMENTS.md');
|
|
757
716
|
if (fs.existsSync(reqPath)) {
|
|
758
|
-
// Extract
|
|
759
|
-
const
|
|
760
|
-
|
|
717
|
+
// Extract the current phase section from roadmap (scoped to avoid cross-phase matching)
|
|
718
|
+
const phaseEsc = escapeRegex(phaseNum);
|
|
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')
|
|
761
722
|
);
|
|
762
723
|
|
|
724
|
+
const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
|
|
725
|
+
const reqMatch = sectionText.match(/\*\*Requirements:\*\*\s*([^\n]+)/i);
|
|
726
|
+
|
|
763
727
|
if (reqMatch) {
|
|
764
728
|
const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(r => r.trim()).filter(Boolean);
|
|
765
729
|
let reqContent = fs.readFileSync(reqPath, 'utf-8');
|
|
@@ -771,14 +735,15 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
771
735
|
new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
|
|
772
736
|
'$1x$2'
|
|
773
737
|
);
|
|
774
|
-
// Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete |
|
|
738
|
+
// Update traceability table: | REQ-ID | Phase N | Pending/In Progress | → | REQ-ID | Phase N | Complete |
|
|
775
739
|
reqContent = reqContent.replace(
|
|
776
|
-
new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
|
|
740
|
+
new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*(?:Pending|In Progress)\\s*(\\|)`, 'gi'),
|
|
777
741
|
'$1 Complete $2'
|
|
778
742
|
);
|
|
779
743
|
}
|
|
780
744
|
|
|
781
745
|
fs.writeFileSync(reqPath, reqContent, 'utf-8');
|
|
746
|
+
requirementsUpdated = true;
|
|
782
747
|
}
|
|
783
748
|
}
|
|
784
749
|
}
|
|
@@ -809,13 +774,13 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
809
774
|
}
|
|
810
775
|
}
|
|
811
776
|
}
|
|
812
|
-
} catch {}
|
|
777
|
+
} catch { /* intentionally empty */ }
|
|
813
778
|
|
|
814
779
|
// Fallback: if filesystem found no next phase, check ROADMAP.md
|
|
815
780
|
// for phases that are defined but not yet planned (no directory on disk)
|
|
816
781
|
if (isLastPhase && fs.existsSync(roadmapPath)) {
|
|
817
782
|
try {
|
|
818
|
-
const roadmapForPhases = fs.readFileSync(roadmapPath, 'utf-8');
|
|
783
|
+
const roadmapForPhases = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
|
|
819
784
|
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
820
785
|
let pm;
|
|
821
786
|
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
|
|
@@ -826,50 +791,69 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
826
791
|
break;
|
|
827
792
|
}
|
|
828
793
|
}
|
|
829
|
-
} catch {}
|
|
794
|
+
} catch { /* intentionally empty */ }
|
|
830
795
|
}
|
|
831
796
|
|
|
832
|
-
// Update STATE.md
|
|
797
|
+
// Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats
|
|
833
798
|
if (fs.existsSync(statePath)) {
|
|
834
799
|
let stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
835
800
|
|
|
836
|
-
// Update Current Phase
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
);
|
|
801
|
+
// Update Current Phase — preserve "X of Y (Name)" compound format
|
|
802
|
+
const phaseValue = nextPhaseNum || phaseNum;
|
|
803
|
+
const existingPhaseField = stateExtractField(stateContent, 'Current Phase')
|
|
804
|
+
|| stateExtractField(stateContent, 'Phase');
|
|
805
|
+
let newPhaseValue = String(phaseValue);
|
|
806
|
+
if (existingPhaseField) {
|
|
807
|
+
const totalMatch = existingPhaseField.match(/of\s+(\d+)/);
|
|
808
|
+
const nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
|
|
809
|
+
if (totalMatch) {
|
|
810
|
+
const total = totalMatch[1];
|
|
811
|
+
const nameStr = nextPhaseName ? ` (${nextPhaseName.replace(/-/g, ' ')})` : (nameMatch ? ` (${nameMatch[1]})` : '');
|
|
812
|
+
newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
|
|
841
816
|
|
|
842
817
|
// Update Current Phase Name
|
|
843
818
|
if (nextPhaseName) {
|
|
844
|
-
stateContent = stateContent.replace(
|
|
845
|
-
/(\*\*Current Phase Name:\*\*\s*).*/,
|
|
846
|
-
`$1${nextPhaseName.replace(/-/g, ' ')}`
|
|
847
|
-
);
|
|
819
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase Name', null, nextPhaseName.replace(/-/g, ' '));
|
|
848
820
|
}
|
|
849
821
|
|
|
850
822
|
// Update Status
|
|
851
|
-
stateContent = stateContent
|
|
852
|
-
|
|
853
|
-
`$1${isLastPhase ? 'Milestone complete' : 'Ready to plan'}`
|
|
854
|
-
);
|
|
823
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null,
|
|
824
|
+
isLastPhase ? 'Milestone complete' : 'Ready to plan');
|
|
855
825
|
|
|
856
826
|
// Update Current Plan
|
|
857
|
-
stateContent = stateContent
|
|
858
|
-
/(\*\*Current Plan:\*\*\s*).*/,
|
|
859
|
-
`$1Not started`
|
|
860
|
-
);
|
|
827
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
|
|
861
828
|
|
|
862
829
|
// Update Last Activity
|
|
863
|
-
stateContent = stateContent
|
|
864
|
-
/(\*\*Last Activity:\*\*\s*).*/,
|
|
865
|
-
`$1${today}`
|
|
866
|
-
);
|
|
830
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
|
|
867
831
|
|
|
868
832
|
// Update Last Activity Description
|
|
869
|
-
stateContent = stateContent
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
)
|
|
833
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null,
|
|
834
|
+
`Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`);
|
|
835
|
+
|
|
836
|
+
// Increment Completed Phases counter (#956)
|
|
837
|
+
const completedRaw = stateExtractField(stateContent, 'Completed Phases');
|
|
838
|
+
if (completedRaw) {
|
|
839
|
+
const newCompleted = parseInt(completedRaw, 10) + 1;
|
|
840
|
+
stateContent = stateReplaceField(stateContent, 'Completed Phases', String(newCompleted)) || stateContent;
|
|
841
|
+
|
|
842
|
+
// Recalculate percent based on completed / total (#956)
|
|
843
|
+
const totalRaw = stateExtractField(stateContent, 'Total Phases');
|
|
844
|
+
if (totalRaw) {
|
|
845
|
+
const totalPhases = parseInt(totalRaw, 10);
|
|
846
|
+
if (totalPhases > 0) {
|
|
847
|
+
const newPercent = Math.round((newCompleted / totalPhases) * 100);
|
|
848
|
+
stateContent = stateReplaceField(stateContent, 'Progress', `${newPercent}%`) || stateContent;
|
|
849
|
+
// Also update percent field if it exists separately
|
|
850
|
+
stateContent = stateContent.replace(
|
|
851
|
+
/(percent:\s*)\d+/,
|
|
852
|
+
`$1${newPercent}`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
873
857
|
|
|
874
858
|
writeStateMd(statePath, stateContent, cwd);
|
|
875
859
|
}
|
|
@@ -884,6 +868,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
884
868
|
date: today,
|
|
885
869
|
roadmap_updated: fs.existsSync(roadmapPath),
|
|
886
870
|
state_updated: fs.existsSync(statePath),
|
|
871
|
+
requirements_updated: requirementsUpdated,
|
|
872
|
+
warnings,
|
|
873
|
+
has_warnings: warnings.length > 0,
|
|
887
874
|
};
|
|
888
875
|
|
|
889
876
|
output(result, raw);
|