gsd-opencode 1.22.1 → 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-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 +117 -2
- package/agents/gsd-doc-verifier.md +207 -0
- package/agents/gsd-doc-writer.md +608 -0
- package/agents/gsd-executor.md +45 -4
- package/agents/gsd-integration-checker.md +0 -2
- package/agents/gsd-nyquist-auditor.md +0 -2
- package/agents/gsd-phase-researcher.md +191 -5
- package/agents/gsd-plan-checker.md +152 -5
- package/agents/gsd-planner.md +131 -157
- 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-security-auditor.md +129 -0
- package/agents/gsd-ui-auditor.md +485 -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 +207 -22
- package/commands/gsd/gsd-add-backlog.md +76 -0
- package/commands/gsd/gsd-analyze-dependencies.md +34 -0
- package/commands/gsd/gsd-audit-uat.md +24 -0
- package/commands/gsd/gsd-autonomous.md +45 -0
- package/commands/gsd/gsd-cleanup.md +5 -0
- package/commands/gsd/gsd-debug.md +29 -21
- package/commands/gsd/gsd-discuss-phase.md +15 -36
- package/commands/gsd/gsd-do.md +30 -0
- package/commands/gsd/gsd-docs-update.md +48 -0
- package/commands/gsd/gsd-execute-phase.md +24 -2
- package/commands/gsd/gsd-fast.md +30 -0
- package/commands/gsd/gsd-forensics.md +56 -0
- package/commands/gsd/gsd-help.md +2 -0
- package/commands/gsd/gsd-join-discord.md +2 -1
- package/commands/gsd/gsd-list-workspaces.md +19 -0
- package/commands/gsd/gsd-manager.md +40 -0
- package/commands/gsd/gsd-milestone-summary.md +51 -0
- package/commands/gsd/gsd-new-project.md +4 -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 +8 -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 +7 -3
- package/commands/gsd/gsd-reapply-patches.md +178 -45
- package/commands/gsd/gsd-remove-workspace.md +26 -0
- package/commands/gsd/gsd-research-phase.md +7 -12
- package/commands/gsd/gsd-review-backlog.md +62 -0
- package/commands/gsd/gsd-review.md +38 -0
- package/commands/gsd/gsd-secure-phase.md +35 -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 +71 -0
- package/get-shit-done/bin/gsd-tools.cjs +450 -90
- package/get-shit-done/bin/lib/commands.cjs +489 -24
- package/get-shit-done/bin/lib/config.cjs +329 -48
- package/get-shit-done/bin/lib/core.cjs +1143 -102
- package/get-shit-done/bin/lib/docs.cjs +267 -0
- package/get-shit-done/bin/lib/frontmatter.cjs +125 -43
- package/get-shit-done/bin/lib/init.cjs +918 -106
- package/get-shit-done/bin/lib/milestone.cjs +65 -33
- package/get-shit-done/bin/lib/model-profiles.cjs +70 -0
- package/get-shit-done/bin/lib/phase.cjs +434 -404
- package/get-shit-done/bin/lib/profile-output.cjs +1048 -0
- package/get-shit-done/bin/lib/profile-pipeline.cjs +539 -0
- package/get-shit-done/bin/lib/roadmap.cjs +156 -101
- package/get-shit-done/bin/lib/schema-detect.cjs +238 -0
- package/get-shit-done/bin/lib/security.cjs +384 -0
- package/get-shit-done/bin/lib/state.cjs +711 -79
- 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 +254 -42
- package/get-shit-done/bin/lib/workstream.cjs +495 -0
- 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/checkpoints.md +12 -10
- 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/decimal-phase-calculation.md +2 -3
- 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/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/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 +18 -1
- 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/user-profiling.md +681 -0
- package/get-shit-done/references/workstream-flag.md +111 -0
- package/get-shit-done/templates/SECURITY.md +61 -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/VALIDATION.md +3 -3
- package/get-shit-done/templates/claude-md.md +145 -0
- package/get-shit-done/templates/config.json +14 -3
- package/get-shit-done/templates/context.md +61 -6
- package/get-shit-done/templates/debug-subagent-prompt.md +2 -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/planner-subagent-prompt.md +2 -10
- 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 +4 -4
- package/get-shit-done/workflows/add-tests.md +4 -4
- package/get-shit-done/workflows/add-todo.md +4 -4
- package/get-shit-done/workflows/analyze-dependencies.md +96 -0
- package/get-shit-done/workflows/audit-milestone.md +20 -16
- package/get-shit-done/workflows/audit-uat.md +109 -0
- package/get-shit-done/workflows/autonomous.md +1036 -0
- package/get-shit-done/workflows/check-todos.md +4 -4
- package/get-shit-done/workflows/cleanup.md +4 -4
- package/get-shit-done/workflows/complete-milestone.md +22 -10
- package/get-shit-done/workflows/diagnose-issues.md +21 -7
- package/get-shit-done/workflows/discovery-phase.md +2 -2
- package/get-shit-done/workflows/discuss-phase-assumptions.md +671 -0
- package/get-shit-done/workflows/discuss-phase-power.md +291 -0
- package/get-shit-done/workflows/discuss-phase.md +558 -47
- package/get-shit-done/workflows/do.md +104 -0
- package/get-shit-done/workflows/docs-update.md +1093 -0
- package/get-shit-done/workflows/execute-phase.md +741 -58
- 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 +127 -7
- package/get-shit-done/workflows/insert-phase.md +4 -4
- 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 +363 -0
- package/get-shit-done/workflows/map-codebase.md +83 -44
- package/get-shit-done/workflows/milestone-summary.md +223 -0
- package/get-shit-done/workflows/new-milestone.md +133 -25
- package/get-shit-done/workflows/new-project.md +216 -54
- 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 +132 -15
- package/get-shit-done/workflows/plan-milestone-gaps.md +6 -7
- package/get-shit-done/workflows/plan-phase.md +513 -62
- 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 +154 -29
- package/get-shit-done/workflows/quick.md +285 -111
- 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 +13 -9
- package/get-shit-done/workflows/resume-project.md +37 -18
- package/get-shit-done/workflows/review.md +281 -0
- package/get-shit-done/workflows/secure-phase.md +154 -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 +91 -11
- package/get-shit-done/workflows/ship.md +237 -0
- package/get-shit-done/workflows/stats.md +60 -0
- package/get-shit-done/workflows/transition.md +150 -23
- package/get-shit-done/workflows/ui-phase.md +292 -0
- package/get-shit-done/workflows/ui-review.md +183 -0
- package/get-shit-done/workflows/update.md +262 -30
- package/get-shit-done/workflows/validate-phase.md +14 -17
- package/get-shit-done/workflows/verify-phase.md +143 -11
- package/get-shit-done/workflows/verify-work.md +141 -39
- 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, withPlanningLock, output, error, readSubdirectories, phaseTokenMatches } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
|
-
const { writeStateMd } = require('./state.cjs');
|
|
9
|
+
const { writeStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, updatePerformanceMetricsSection } = 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
|
|
@@ -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;
|
|
@@ -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
|
|
@@ -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+)`);
|
|
@@ -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: [] };
|
|
@@ -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
|
|
|
@@ -180,7 +182,7 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
180
182
|
|
|
181
183
|
const result = {
|
|
182
184
|
found: true,
|
|
183
|
-
directory: toPosixPath(path.join(
|
|
185
|
+
directory: toPosixPath(path.join(path.relative(cwd, planningDir(cwd)), 'phases', match)),
|
|
184
186
|
phase_number: phaseNumber,
|
|
185
187
|
phase_name: phaseName,
|
|
186
188
|
plans,
|
|
@@ -203,7 +205,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
203
205
|
error('phase required for phase-plan-index');
|
|
204
206
|
}
|
|
205
207
|
|
|
206
|
-
const phasesDir = path.join(cwd, '
|
|
208
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
207
209
|
const normalized = normalizePhaseName(phase);
|
|
208
210
|
|
|
209
211
|
// Find phase directory
|
|
@@ -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;
|
|
@@ -308,60 +310,84 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
308
310
|
output(result, raw);
|
|
309
311
|
}
|
|
310
312
|
|
|
311
|
-
function cmdPhaseAdd(cwd, description, raw) {
|
|
313
|
+
function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
312
314
|
if (!description) {
|
|
313
315
|
error('description required for phase add');
|
|
314
316
|
}
|
|
315
317
|
|
|
316
|
-
const
|
|
318
|
+
const config = loadConfig(cwd);
|
|
319
|
+
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
|
317
320
|
if (!fs.existsSync(roadmapPath)) {
|
|
318
321
|
error('ROADMAP.md not found');
|
|
319
322
|
}
|
|
320
323
|
|
|
321
|
-
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
322
324
|
const slug = generateSlugInternal(description);
|
|
323
325
|
|
|
324
|
-
//
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
while ((m = phasePattern.exec(content)) !== null) {
|
|
329
|
-
const num = parseInt(m[1], 10);
|
|
330
|
-
if (num > maxPhase) maxPhase = num;
|
|
331
|
-
}
|
|
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);
|
|
332
330
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
331
|
+
// Optional project code prefix (e.g., 'CK' → 'CK-01-foundation')
|
|
332
|
+
const projectCode = config.project_code || '';
|
|
333
|
+
const prefix = projectCode ? `${projectCode}-` : '';
|
|
334
|
+
|
|
335
|
+
let _newPhaseId;
|
|
336
|
+
let _dirName;
|
|
337
|
+
|
|
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
|
+
}
|
|
352
|
+
|
|
353
|
+
_newPhaseId = maxPhase + 1;
|
|
354
|
+
const paddedNum = String(_newPhaseId).padStart(2, '0');
|
|
355
|
+
_dirName = `${prefix}${paddedNum}-${slug}`;
|
|
356
|
+
}
|
|
357
|
+
|
|
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'), '');
|
|
353
363
|
|
|
354
|
-
|
|
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
|
+
});
|
|
355
380
|
|
|
356
381
|
const result = {
|
|
357
|
-
phase_number:
|
|
358
|
-
padded:
|
|
382
|
+
phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
|
|
383
|
+
padded: typeof newPhaseId === 'number' ? String(newPhaseId).padStart(2, '0') : String(newPhaseId),
|
|
359
384
|
name: description,
|
|
360
385
|
slug,
|
|
361
|
-
directory:
|
|
386
|
+
directory: toPosixPath(path.join(path.relative(cwd, planningDir(cwd)), 'phases', dirName)),
|
|
387
|
+
naming_mode: config.phase_naming,
|
|
362
388
|
};
|
|
363
389
|
|
|
364
|
-
output(result, raw,
|
|
390
|
+
output(result, raw, result.padded);
|
|
365
391
|
}
|
|
366
392
|
|
|
367
393
|
function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
@@ -369,333 +395,258 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
369
395
|
error('after-phase and description required for phase insert');
|
|
370
396
|
}
|
|
371
397
|
|
|
372
|
-
const roadmapPath = path.join(cwd, '
|
|
398
|
+
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
|
373
399
|
if (!fs.existsSync(roadmapPath)) {
|
|
374
400
|
error('ROADMAP.md not found');
|
|
375
401
|
}
|
|
376
402
|
|
|
377
|
-
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
378
403
|
const slug = generateSlugInternal(description);
|
|
379
404
|
|
|
380
|
-
//
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
let existingDecimals = [];
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
396
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
397
|
-
const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
|
|
398
|
-
for (const dir of dirs) {
|
|
399
|
-
const dm = dir.match(decimalPattern);
|
|
400
|
-
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`);
|
|
401
417
|
}
|
|
402
|
-
} catch {}
|
|
403
|
-
|
|
404
|
-
const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1;
|
|
405
|
-
const decimalPhase = `${normalizedBase}.${nextDecimal}`;
|
|
406
|
-
const dirName = `${decimalPhase}-${slug}`;
|
|
407
|
-
const dirPath = path.join(cwd, '.planning', 'phases', dirName);
|
|
408
|
-
|
|
409
|
-
// Create directory with .gitkeep so git tracks empty folders
|
|
410
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
411
|
-
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
|
|
412
418
|
|
|
413
|
-
|
|
414
|
-
|
|
419
|
+
// Calculate next decimal using existing logic
|
|
420
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
421
|
+
const normalizedBase = normalizePhaseName(afterPhase);
|
|
422
|
+
let existingDecimals = [];
|
|
415
423
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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 */ }
|
|
433
|
+
|
|
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
|
+
}
|
|
422
456
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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);
|
|
426
460
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
461
|
+
let insertIdx;
|
|
462
|
+
if (nextPhaseMatch) {
|
|
463
|
+
insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
|
|
464
|
+
} else {
|
|
465
|
+
insertIdx = rawContent.length;
|
|
466
|
+
}
|
|
433
467
|
|
|
434
|
-
|
|
435
|
-
|
|
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
|
+
});
|
|
436
472
|
|
|
437
473
|
const result = {
|
|
438
474
|
phase_number: decimalPhase,
|
|
439
475
|
after_phase: afterPhase,
|
|
440
476
|
name: description,
|
|
441
477
|
slug,
|
|
442
|
-
directory:
|
|
478
|
+
directory: toPosixPath(path.join(path.relative(cwd, planningDir(cwd)), 'phases', dirName)),
|
|
443
479
|
};
|
|
444
480
|
|
|
445
481
|
output(result, raw, decimalPhase);
|
|
446
482
|
}
|
|
447
483
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
484
|
+
/**
|
|
485
|
+
* Renumber sibling decimal phases after a decimal phase is removed.
|
|
486
|
+
* e.g. removing 06.2 → 06.3 becomes 06.2, 06.4 becomes 06.3, etc.
|
|
487
|
+
* Returns { renamedDirs, renamedFiles }.
|
|
488
|
+
*/
|
|
489
|
+
function renameDecimalPhases(phasesDir, baseInt, removedDecimal) {
|
|
490
|
+
const renamedDirs = [], renamedFiles = [];
|
|
491
|
+
const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
|
|
492
|
+
const dirs = readSubdirectories(phasesDir, true);
|
|
493
|
+
const toRename = dirs
|
|
494
|
+
.map(dir => { const m = dir.match(decPattern); return m ? { dir, oldDecimal: parseInt(m[1], 10), slug: m[2] } : null; })
|
|
495
|
+
.filter(item => item && item.oldDecimal > removedDecimal)
|
|
496
|
+
.sort((a, b) => b.oldDecimal - a.oldDecimal); // descending to avoid conflicts
|
|
497
|
+
|
|
498
|
+
for (const item of toRename) {
|
|
499
|
+
const newDecimal = item.oldDecimal - 1;
|
|
500
|
+
const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
|
|
501
|
+
const newPhaseId = `${baseInt}.${newDecimal}`;
|
|
502
|
+
const newDirName = `${baseInt}.${newDecimal}-${item.slug}`;
|
|
503
|
+
fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
|
|
504
|
+
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
505
|
+
for (const f of fs.readdirSync(path.join(phasesDir, newDirName))) {
|
|
506
|
+
if (f.includes(oldPhaseId)) {
|
|
507
|
+
const newFileName = f.replace(oldPhaseId, newPhaseId);
|
|
508
|
+
fs.renameSync(path.join(phasesDir, newDirName, f), path.join(phasesDir, newDirName, newFileName));
|
|
509
|
+
renamedFiles.push({ from: f, to: newFileName });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
451
512
|
}
|
|
513
|
+
return { renamedDirs, renamedFiles };
|
|
514
|
+
}
|
|
452
515
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
516
|
+
/**
|
|
517
|
+
* Renumber all integer phases after removedInt.
|
|
518
|
+
* e.g. removing phase 5 → phase 6 becomes 5, phase 7 becomes 6, etc.
|
|
519
|
+
* Returns { renamedDirs, renamedFiles }.
|
|
520
|
+
*/
|
|
521
|
+
function renameIntegerPhases(phasesDir, removedInt) {
|
|
522
|
+
const renamedDirs = [], renamedFiles = [];
|
|
523
|
+
const dirs = readSubdirectories(phasesDir, true);
|
|
524
|
+
const toRename = dirs
|
|
525
|
+
.map(dir => {
|
|
526
|
+
const m = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
|
|
527
|
+
if (!m) return null;
|
|
528
|
+
const dirInt = parseInt(m[1], 10);
|
|
529
|
+
return dirInt > removedInt ? { dir, oldInt: dirInt, letter: m[2] ? m[2].toUpperCase() : '', decimal: m[3] ? parseInt(m[3], 10) : null, slug: m[4] } : null;
|
|
530
|
+
})
|
|
531
|
+
.filter(Boolean)
|
|
532
|
+
.sort((a, b) => a.oldInt !== b.oldInt ? b.oldInt - a.oldInt : (b.decimal || 0) - (a.decimal || 0));
|
|
533
|
+
|
|
534
|
+
for (const item of toRename) {
|
|
535
|
+
const newInt = item.oldInt - 1;
|
|
536
|
+
const newPadded = String(newInt).padStart(2, '0');
|
|
537
|
+
const oldPadded = String(item.oldInt).padStart(2, '0');
|
|
538
|
+
const letterSuffix = item.letter || '';
|
|
539
|
+
const decimalSuffix = item.decimal !== null ? `.${item.decimal}` : '';
|
|
540
|
+
const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
|
|
541
|
+
const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
|
|
542
|
+
const newDirName = `${newPrefix}-${item.slug}`;
|
|
543
|
+
fs.renameSync(path.join(phasesDir, item.dir), path.join(phasesDir, newDirName));
|
|
544
|
+
renamedDirs.push({ from: item.dir, to: newDirName });
|
|
545
|
+
for (const f of fs.readdirSync(path.join(phasesDir, newDirName))) {
|
|
546
|
+
if (f.startsWith(oldPrefix)) {
|
|
547
|
+
const newFileName = newPrefix + f.slice(oldPrefix.length);
|
|
548
|
+
fs.renameSync(path.join(phasesDir, newDirName, f), path.join(phasesDir, newDirName, newFileName));
|
|
549
|
+
renamedFiles.push({ from: f, to: newFileName });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
459
552
|
}
|
|
553
|
+
return { renamedDirs, renamedFiles };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Remove a phase section from ROADMAP.md and renumber all subsequent integer phases.
|
|
558
|
+
*/
|
|
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
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
fs.writeFileSync(roadmapPath, content, 'utf-8');
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
588
|
+
if (!targetPhase) error('phase number required for phase remove');
|
|
589
|
+
|
|
590
|
+
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
|
591
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
592
|
+
|
|
593
|
+
if (!fs.existsSync(roadmapPath)) error('ROADMAP.md not found');
|
|
460
594
|
|
|
461
|
-
// Normalize the target
|
|
462
595
|
const normalized = normalizePhaseName(targetPhase);
|
|
463
596
|
const isDecimal = targetPhase.includes('.');
|
|
597
|
+
const force = options.force || false;
|
|
464
598
|
|
|
465
|
-
// Find
|
|
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 {}
|
|
599
|
+
// Find target directory
|
|
600
|
+
const targetDir = readSubdirectories(phasesDir, true)
|
|
601
|
+
.find(d => phaseTokenMatches(d, normalized)) || null;
|
|
472
602
|
|
|
473
|
-
//
|
|
603
|
+
// Guard against removing executed work
|
|
474
604
|
if (targetDir && !force) {
|
|
475
|
-
const
|
|
476
|
-
const files = fs.readdirSync(targetPath);
|
|
605
|
+
const files = fs.readdirSync(path.join(phasesDir, targetDir));
|
|
477
606
|
const summaries = files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
478
607
|
if (summaries.length > 0) {
|
|
479
608
|
error(`Phase ${targetPhase} has ${summaries.length} executed plan(s). Use --force to remove anyway.`);
|
|
480
609
|
}
|
|
481
610
|
}
|
|
482
611
|
|
|
483
|
-
|
|
484
|
-
if (targetDir) {
|
|
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));
|
|
501
|
-
|
|
502
|
-
// Find sibling decimals with higher numbers
|
|
503
|
-
const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
|
|
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
|
-
}
|
|
511
|
-
|
|
512
|
-
// Sort descending to avoid conflicts
|
|
513
|
-
toRename.sort((a, b) => b.oldDecimal - a.oldDecimal);
|
|
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 {}
|
|
540
|
-
|
|
541
|
-
} else {
|
|
542
|
-
// Integer removal: renumber all subsequent integer phases
|
|
543
|
-
const removedInt = parseInt(normalized, 10);
|
|
544
|
-
|
|
545
|
-
try {
|
|
546
|
-
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
547
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
|
612
|
+
if (targetDir) fs.rmSync(path.join(phasesDir, targetDir), { recursive: true, force: true });
|
|
548
613
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
}
|
|
565
|
-
|
|
566
|
-
// Sort descending to avoid conflicts
|
|
567
|
-
toRename.sort((a, b) => {
|
|
568
|
-
if (a.oldInt !== b.oldInt) return b.oldInt - a.oldInt;
|
|
569
|
-
return (b.decimal || 0) - (a.decimal || 0);
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
for (const item of toRename) {
|
|
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 {}
|
|
600
|
-
}
|
|
614
|
+
// Renumber subsequent phases on disk
|
|
615
|
+
let renamedDirs = [], renamedFiles = [];
|
|
616
|
+
try {
|
|
617
|
+
const renamed = isDecimal
|
|
618
|
+
? renameDecimalPhases(phasesDir, normalized.split('.')[0], parseInt(normalized.split('.')[1], 10))
|
|
619
|
+
: renameIntegerPhases(phasesDir, parseInt(normalized, 10));
|
|
620
|
+
renamedDirs = renamed.renamedDirs;
|
|
621
|
+
renamedFiles = renamed.renamedFiles;
|
|
622
|
+
} catch { /* intentionally empty */ }
|
|
601
623
|
|
|
602
624
|
// Update ROADMAP.md
|
|
603
|
-
|
|
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, '');
|
|
620
|
-
|
|
621
|
-
// Renumber references in ROADMAP for subsequent phases
|
|
622
|
-
if (!isDecimal) {
|
|
623
|
-
const removedInt = parseInt(normalized, 10);
|
|
624
|
-
|
|
625
|
-
// Collect all integer phases > removedInt
|
|
626
|
-
const maxPhase = 99; // reasonable upper bound
|
|
627
|
-
for (let oldNum = maxPhase; oldNum > removedInt; oldNum--) {
|
|
628
|
-
const newNum = oldNum - 1;
|
|
629
|
-
const oldStr = String(oldNum);
|
|
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
|
-
}
|
|
665
|
-
|
|
666
|
-
fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
|
|
625
|
+
updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, parseInt(normalized, 10), cwd);
|
|
667
626
|
|
|
668
627
|
// Update STATE.md phase count
|
|
669
|
-
const statePath = path.join(cwd, '
|
|
628
|
+
const statePath = path.join(planningDir(cwd), 'STATE.md');
|
|
670
629
|
if (fs.existsSync(statePath)) {
|
|
671
630
|
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}`);
|
|
631
|
+
const totalRaw = stateExtractField(stateContent, 'Total Phases');
|
|
632
|
+
if (totalRaw) {
|
|
633
|
+
stateContent = stateReplaceField(stateContent, 'Total Phases', String(parseInt(totalRaw, 10) - 1)) || stateContent;
|
|
678
634
|
}
|
|
679
|
-
|
|
680
|
-
const ofPattern = /(\bof\s+)(\d+)(\s*(?:\(|phases?))/i;
|
|
681
|
-
const ofMatch = stateContent.match(ofPattern);
|
|
635
|
+
const ofMatch = stateContent.match(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i);
|
|
682
636
|
if (ofMatch) {
|
|
683
|
-
|
|
684
|
-
stateContent = stateContent.replace(ofPattern, `$1${oldTotal - 1}$3`);
|
|
637
|
+
stateContent = stateContent.replace(/(\bof\s+)(\d+)(\s*(?:\(|phases?))/i, `$1${parseInt(ofMatch[2], 10) - 1}$3`);
|
|
685
638
|
}
|
|
686
639
|
writeStateMd(statePath, stateContent, cwd);
|
|
687
640
|
}
|
|
688
641
|
|
|
689
|
-
|
|
642
|
+
output({
|
|
690
643
|
removed: targetPhase,
|
|
691
|
-
directory_deleted: targetDir
|
|
644
|
+
directory_deleted: targetDir,
|
|
692
645
|
renamed_directories: renamedDirs,
|
|
693
646
|
renamed_files: renamedFiles,
|
|
694
647
|
roadmap_updated: true,
|
|
695
648
|
state_updated: fs.existsSync(statePath),
|
|
696
|
-
};
|
|
697
|
-
|
|
698
|
-
output(result, raw);
|
|
649
|
+
}, raw);
|
|
699
650
|
}
|
|
700
651
|
|
|
701
652
|
function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
@@ -703,9 +654,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
703
654
|
error('phase number required for phase complete');
|
|
704
655
|
}
|
|
705
656
|
|
|
706
|
-
const roadmapPath = path.join(cwd, '
|
|
707
|
-
const statePath = path.join(cwd, '
|
|
708
|
-
const phasesDir = path.join(cwd, '
|
|
657
|
+
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
|
658
|
+
const statePath = path.join(planningDir(cwd), 'STATE.md');
|
|
659
|
+
const phasesDir = path.join(planningDir(cwd), 'phases');
|
|
709
660
|
const normalized = normalizePhaseName(phaseNum);
|
|
710
661
|
const today = new Date().toISOString().split('T')[0];
|
|
711
662
|
|
|
@@ -717,70 +668,124 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
717
668
|
|
|
718
669
|
const planCount = phaseInfo.plans.length;
|
|
719
670
|
const summaryCount = phaseInfo.summaries.length;
|
|
671
|
+
let requirementsUpdated = false;
|
|
720
672
|
|
|
721
|
-
//
|
|
722
|
-
|
|
723
|
-
|
|
673
|
+
// Check for unresolved verification debt (non-blocking warnings)
|
|
674
|
+
const warnings = [];
|
|
675
|
+
try {
|
|
676
|
+
const phaseFullDir = path.join(cwd, phaseInfo.directory);
|
|
677
|
+
const phaseFiles = fs.readdirSync(phaseFullDir);
|
|
678
|
+
|
|
679
|
+
for (const file of phaseFiles.filter(f => f.includes('-UAT') && f.endsWith('.md'))) {
|
|
680
|
+
const content = fs.readFileSync(path.join(phaseFullDir, file), 'utf-8');
|
|
681
|
+
if (/result: pending/.test(content)) warnings.push(`${file}: has pending tests`);
|
|
682
|
+
if (/result: blocked/.test(content)) warnings.push(`${file}: has blocked tests`);
|
|
683
|
+
if (/status: partial/.test(content)) warnings.push(`${file}: testing incomplete (partial)`);
|
|
684
|
+
if (/status: diagnosed/.test(content)) warnings.push(`${file}: has diagnosed gaps`);
|
|
685
|
+
}
|
|
724
686
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
687
|
+
for (const file of phaseFiles.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
|
|
688
|
+
const content = fs.readFileSync(path.join(phaseFullDir, file), 'utf-8');
|
|
689
|
+
if (/status: human_needed/.test(content)) warnings.push(`${file}: needs human verification`);
|
|
690
|
+
if (/status: gaps_found/.test(content)) warnings.push(`${file}: has unresolved gaps`);
|
|
691
|
+
}
|
|
692
|
+
} catch {}
|
|
731
693
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
'i'
|
|
737
|
-
);
|
|
738
|
-
roadmapContent = roadmapContent.replace(
|
|
739
|
-
tablePattern,
|
|
740
|
-
`$1 Complete $2 ${today} $3`
|
|
741
|
-
);
|
|
694
|
+
// Update ROADMAP.md and REQUIREMENTS.md atomically under lock
|
|
695
|
+
if (fs.existsSync(roadmapPath)) {
|
|
696
|
+
withPlanningLock(cwd, () => {
|
|
697
|
+
let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
742
698
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
planCountPattern,
|
|
750
|
-
`$1${summaryCount}/${planCount} plans complete`
|
|
751
|
-
);
|
|
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})`);
|
|
752
705
|
|
|
753
|
-
|
|
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'
|
|
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
|
+
});
|
|
754
727
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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`
|
|
761
736
|
);
|
|
762
737
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
+
}
|
|
766
750
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
+
}
|
|
780
783
|
|
|
781
|
-
|
|
784
|
+
fs.writeFileSync(reqPath, reqContent, 'utf-8');
|
|
785
|
+
requirementsUpdated = true;
|
|
786
|
+
}
|
|
782
787
|
}
|
|
783
|
-
}
|
|
788
|
+
});
|
|
784
789
|
}
|
|
785
790
|
|
|
786
791
|
// Find next phase — check both filesystem AND roadmap
|
|
@@ -809,13 +814,13 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
809
814
|
}
|
|
810
815
|
}
|
|
811
816
|
}
|
|
812
|
-
} catch {}
|
|
817
|
+
} catch { /* intentionally empty */ }
|
|
813
818
|
|
|
814
819
|
// Fallback: if filesystem found no next phase, check ROADMAP.md
|
|
815
820
|
// for phases that are defined but not yet planned (no directory on disk)
|
|
816
821
|
if (isLastPhase && fs.existsSync(roadmapPath)) {
|
|
817
822
|
try {
|
|
818
|
-
const roadmapForPhases = fs.readFileSync(roadmapPath, 'utf-8');
|
|
823
|
+
const roadmapForPhases = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
|
|
819
824
|
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
|
|
820
825
|
let pm;
|
|
821
826
|
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
|
|
@@ -826,50 +831,72 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
826
831
|
break;
|
|
827
832
|
}
|
|
828
833
|
}
|
|
829
|
-
} catch {}
|
|
834
|
+
} catch { /* intentionally empty */ }
|
|
830
835
|
}
|
|
831
836
|
|
|
832
|
-
// Update STATE.md
|
|
837
|
+
// Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats
|
|
833
838
|
if (fs.existsSync(statePath)) {
|
|
834
839
|
let stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
835
840
|
|
|
836
|
-
// Update Current Phase
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
);
|
|
841
|
+
// Update Current Phase — preserve "X of Y (Name)" compound format
|
|
842
|
+
const phaseValue = nextPhaseNum || phaseNum;
|
|
843
|
+
const existingPhaseField = stateExtractField(stateContent, 'Current Phase')
|
|
844
|
+
|| stateExtractField(stateContent, 'Phase');
|
|
845
|
+
let newPhaseValue = String(phaseValue);
|
|
846
|
+
if (existingPhaseField) {
|
|
847
|
+
const totalMatch = existingPhaseField.match(/of\s+(\d+)/);
|
|
848
|
+
const nameMatch = existingPhaseField.match(/\(([^)]+)\)/);
|
|
849
|
+
if (totalMatch) {
|
|
850
|
+
const total = totalMatch[1];
|
|
851
|
+
const nameStr = nextPhaseName ? ` (${nextPhaseName.replace(/-/g, ' ')})` : (nameMatch ? ` (${nameMatch[1]})` : '');
|
|
852
|
+
newPhaseValue = `${phaseValue} of ${total}${nameStr}`;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase', 'Phase', newPhaseValue);
|
|
841
856
|
|
|
842
857
|
// Update Current Phase Name
|
|
843
858
|
if (nextPhaseName) {
|
|
844
|
-
stateContent = stateContent.replace(
|
|
845
|
-
/(\*\*Current Phase Name:\*\*\s*).*/,
|
|
846
|
-
`$1${nextPhaseName.replace(/-/g, ' ')}`
|
|
847
|
-
);
|
|
859
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Phase Name', null, nextPhaseName.replace(/-/g, ' '));
|
|
848
860
|
}
|
|
849
861
|
|
|
850
862
|
// Update Status
|
|
851
|
-
stateContent = stateContent
|
|
852
|
-
|
|
853
|
-
`$1${isLastPhase ? 'Milestone complete' : 'Ready to plan'}`
|
|
854
|
-
);
|
|
863
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null,
|
|
864
|
+
isLastPhase ? 'Milestone complete' : 'Ready to plan');
|
|
855
865
|
|
|
856
866
|
// Update Current Plan
|
|
857
|
-
stateContent = stateContent
|
|
858
|
-
/(\*\*Current Plan:\*\*\s*).*/,
|
|
859
|
-
`$1Not started`
|
|
860
|
-
);
|
|
867
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Current Plan', 'Plan', 'Not started');
|
|
861
868
|
|
|
862
869
|
// Update Last Activity
|
|
863
|
-
stateContent = stateContent
|
|
864
|
-
/(\*\*Last Activity:\*\*\s*).*/,
|
|
865
|
-
`$1${today}`
|
|
866
|
-
);
|
|
870
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
|
|
867
871
|
|
|
868
872
|
// Update Last Activity Description
|
|
869
|
-
stateContent = stateContent
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
)
|
|
873
|
+
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null,
|
|
874
|
+
`Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`);
|
|
875
|
+
|
|
876
|
+
// Increment Completed Phases counter (#956)
|
|
877
|
+
const completedRaw = stateExtractField(stateContent, 'Completed Phases');
|
|
878
|
+
if (completedRaw) {
|
|
879
|
+
const newCompleted = parseInt(completedRaw, 10) + 1;
|
|
880
|
+
stateContent = stateReplaceField(stateContent, 'Completed Phases', String(newCompleted)) || stateContent;
|
|
881
|
+
|
|
882
|
+
// Recalculate percent based on completed / total (#956)
|
|
883
|
+
const totalRaw = stateExtractField(stateContent, 'Total Phases');
|
|
884
|
+
if (totalRaw) {
|
|
885
|
+
const totalPhases = parseInt(totalRaw, 10);
|
|
886
|
+
if (totalPhases > 0) {
|
|
887
|
+
const newPercent = Math.round((newCompleted / totalPhases) * 100);
|
|
888
|
+
stateContent = stateReplaceField(stateContent, 'Progress', `${newPercent}%`) || stateContent;
|
|
889
|
+
// Also update percent field if it exists separately
|
|
890
|
+
stateContent = stateContent.replace(
|
|
891
|
+
/(percent:\s*)\d+/,
|
|
892
|
+
`$1${newPercent}`
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Gate 4: Update Performance Metrics section (#1627)
|
|
899
|
+
stateContent = updatePerformanceMetricsSection(stateContent, cwd, phaseNum, planCount, summaryCount);
|
|
873
900
|
|
|
874
901
|
writeStateMd(statePath, stateContent, cwd);
|
|
875
902
|
}
|
|
@@ -884,6 +911,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|
|
884
911
|
date: today,
|
|
885
912
|
roadmap_updated: fs.existsSync(roadmapPath),
|
|
886
913
|
state_updated: fs.existsSync(statePath),
|
|
914
|
+
requirements_updated: requirementsUpdated,
|
|
915
|
+
warnings,
|
|
916
|
+
has_warnings: warnings.length > 0,
|
|
887
917
|
};
|
|
888
918
|
|
|
889
919
|
output(result, raw);
|