gsd-opencode 1.30.0 → 1.33.1

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