@undeemed/get-shit-done-codex 1.20.8 → 1.20.10

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.
@@ -110,6 +110,9 @@ Task(
110
110
  **If `## CHECKPOINT REACHED`:**
111
111
  - Present checkpoint details to user
112
112
  - Get user response
113
+ - If checkpoint type is `human-verify`:
114
+ - If user confirms fixed: continue so agent can finalize/resolve/archive
115
+ - If user reports issues: continue so agent returns to investigation/fixing
113
116
  - Spawn continuation agent (see step 5)
114
117
 
115
118
  **If `## INVESTIGATION INCONCLUSIVE`:**
@@ -12,7 +12,7 @@ Display the Discord invite link for the GSD community server.
12
12
 
13
13
  Connect with other GSD users, get help, share what you're building, and stay updated.
14
14
 
15
- **Invite link:** https://discord.gg/5JJgD5svVS
15
+ **Invite link:** https://discord.gg/gsd
16
16
 
17
17
  Click the link or paste it into your browser to join.
18
18
  </output>
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: gsd:plan-phase
3
3
  description: Create detailed phase plan (PLAN.md) with verification loop
4
- argument-hint: "[phase] [--auto] [--research] [--skip-research] [--gaps] [--skip-verify]"
4
+ argument-hint: "[phase] [--auto] [--research] [--skip-research] [--gaps] [--skip-verify] [--prd <file>]"
5
5
  agent: gsd-planner
6
6
  allowed-tools:
7
7
  - Read
@@ -34,6 +34,7 @@ Phase number: $ARGUMENTS (optional — auto-detects next unplanned phase if omit
34
34
  - `--skip-research` — Skip research, go straight to planning
35
35
  - `--gaps` — Gap closure mode (reads VERIFICATION.md, skips research)
36
36
  - `--skip-verify` — Skip verification loop
37
+ - `--prd <file>` — Use a PRD/acceptance criteria file instead of discuss-phase. Parses requirements into CONTEXT.md automatically. Skips discuss-phase entirely.
37
38
 
38
39
  Normalize phase input in step 2 before any directory lookups.
39
40
  </context>
@@ -10,6 +10,7 @@
10
10
  *
11
11
  * Atomic Commands:
12
12
  * state load Load project config + state
13
+ * state json Output STATE.md frontmatter as JSON
13
14
  * state update <field> <value> Update a STATE.md field
14
15
  * state get [section] Get STATE.md content or section
15
16
  * state patch --field val ... Batch update STATE.md fields
@@ -102,7 +103,9 @@
102
103
  * state update-progress Recalculate progress bar
103
104
  * state add-decision --summary "..." Add decision to STATE.md
104
105
  * [--phase N] [--rationale "..."]
106
+ * [--summary-file path] [--rationale-file path]
105
107
  * state add-blocker --text "..." Add blocker
108
+ * [--text-file path]
106
109
  * state resolve-blocker --text "..." Remove blocker
107
110
  * state record-session Update session continuity
108
111
  * --stopped-at "..."
@@ -123,6 +126,8 @@
123
126
  * init progress All context for progress workflow
124
127
  */
125
128
 
129
+ const fs = require('fs');
130
+ const path = require('path');
126
131
  const { error } = require('./lib/core.cjs');
127
132
  const state = require('./lib/state.cjs');
128
133
  const phase = require('./lib/phase.cjs');
@@ -139,21 +144,43 @@ const frontmatter = require('./lib/frontmatter.cjs');
139
144
 
140
145
  async function main() {
141
146
  const args = process.argv.slice(2);
147
+
148
+ // Optional cwd override for sandboxed subagents running outside project root.
149
+ let cwd = process.cwd();
150
+ const cwdEqArg = args.find(arg => arg.startsWith('--cwd='));
151
+ const cwdIdx = args.indexOf('--cwd');
152
+ if (cwdEqArg) {
153
+ const value = cwdEqArg.slice('--cwd='.length).trim();
154
+ if (!value) error('Missing value for --cwd');
155
+ args.splice(args.indexOf(cwdEqArg), 1);
156
+ cwd = path.resolve(value);
157
+ } else if (cwdIdx !== -1) {
158
+ const value = args[cwdIdx + 1];
159
+ if (!value || value.startsWith('--')) error('Missing value for --cwd');
160
+ args.splice(cwdIdx, 2);
161
+ cwd = path.resolve(value);
162
+ }
163
+
164
+ if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
165
+ error(`Invalid --cwd: ${cwd}`);
166
+ }
167
+
142
168
  const rawIndex = args.indexOf('--raw');
143
169
  const raw = rawIndex !== -1;
144
170
  if (rawIndex !== -1) args.splice(rawIndex, 1);
145
171
 
146
172
  const command = args[0];
147
- const cwd = process.cwd();
148
173
 
149
174
  if (!command) {
150
- error('Usage: gsd-tools <command> [args] [--raw]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init');
175
+ error('Usage: gsd-tools <command> [args] [--raw] [--cwd <path>]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init');
151
176
  }
152
177
 
153
178
  switch (command) {
154
179
  case 'state': {
155
180
  const subcommand = args[1];
156
- if (subcommand === 'update') {
181
+ if (subcommand === 'json') {
182
+ state.cmdStateJson(cwd, raw);
183
+ } else if (subcommand === 'update') {
157
184
  state.cmdStateUpdate(cwd, args[2], args[3]);
158
185
  } else if (subcommand === 'get') {
159
186
  state.cmdStateGet(cwd, args[2], raw);
@@ -187,15 +214,23 @@ async function main() {
187
214
  } else if (subcommand === 'add-decision') {
188
215
  const phaseIdx = args.indexOf('--phase');
189
216
  const summaryIdx = args.indexOf('--summary');
217
+ const summaryFileIdx = args.indexOf('--summary-file');
190
218
  const rationaleIdx = args.indexOf('--rationale');
219
+ const rationaleFileIdx = args.indexOf('--rationale-file');
191
220
  state.cmdStateAddDecision(cwd, {
192
221
  phase: phaseIdx !== -1 ? args[phaseIdx + 1] : null,
193
222
  summary: summaryIdx !== -1 ? args[summaryIdx + 1] : null,
223
+ summary_file: summaryFileIdx !== -1 ? args[summaryFileIdx + 1] : null,
194
224
  rationale: rationaleIdx !== -1 ? args[rationaleIdx + 1] : '',
225
+ rationale_file: rationaleFileIdx !== -1 ? args[rationaleFileIdx + 1] : null,
195
226
  }, raw);
196
227
  } else if (subcommand === 'add-blocker') {
197
228
  const textIdx = args.indexOf('--text');
198
- state.cmdStateAddBlocker(cwd, textIdx !== -1 ? args[textIdx + 1] : null, raw);
229
+ const textFileIdx = args.indexOf('--text-file');
230
+ state.cmdStateAddBlocker(cwd, {
231
+ text: textIdx !== -1 ? args[textIdx + 1] : null,
232
+ text_file: textFileIdx !== -1 ? args[textFileIdx + 1] : null,
233
+ }, raw);
199
234
  } else if (subcommand === 'resolve-blocker') {
200
235
  const textIdx = args.indexOf('--text');
201
236
  state.cmdStateResolveBlocker(cwd, textIdx !== -1 ? args[textIdx + 1] : null, raw);
@@ -4,7 +4,7 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { execSync } = require('child_process');
7
- const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, MODEL_PROFILES, output, error, findPhaseInternal } = require('./core.cjs');
7
+ const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, MODEL_PROFILES, output, error, findPhaseInternal } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
9
 
10
10
  function cmdGenerateSlug(text, raw) {
@@ -304,6 +304,7 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
304
304
  tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [],
305
305
  patterns: fm['patterns-established'] || [],
306
306
  decisions: parseDecisions(fm['key-decisions']),
307
+ requirements_completed: fm['requirements-completed'] || [],
307
308
  };
308
309
 
309
310
  // If fields specified, filter to only those fields
@@ -394,14 +395,10 @@ function cmdProgressRender(cwd, format, raw) {
394
395
 
395
396
  try {
396
397
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
397
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => {
398
- const aNum = parseFloat(a.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
399
- const bNum = parseFloat(b.match(/^(\d+(?:\.\d+)?)/)?.[1] || '0');
400
- return aNum - bNum;
401
- });
398
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
402
399
 
403
400
  for (const dir of dirs) {
404
- const dm = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/);
401
+ const dm = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
405
402
  const phaseNum = dm ? dm[1] : dir;
406
403
  const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
407
404
  const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
@@ -421,7 +418,7 @@ function cmdProgressRender(cwd, format, raw) {
421
418
  }
422
419
  } catch {}
423
420
 
424
- const percent = totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0;
421
+ const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
425
422
 
426
423
  if (format === 'table') {
427
424
  // Render markdown table
@@ -147,8 +147,12 @@ function execGit(cwd, args) {
147
147
 
148
148
  // ─── Phase utilities ──────────────────────────────────────────────────────────
149
149
 
150
+ function escapeRegex(value) {
151
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
152
+ }
153
+
150
154
  function normalizePhaseName(phase) {
151
- const match = phase.match(/^(\d+)([A-Z])?(\.\d+)?/i);
155
+ const match = String(phase).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
152
156
  if (!match) return phase;
153
157
  const padded = match[1].padStart(2, '0');
154
158
  const letter = match[2] ? match[2].toUpperCase() : '';
@@ -157,8 +161,8 @@ function normalizePhaseName(phase) {
157
161
  }
158
162
 
159
163
  function comparePhaseNum(a, b) {
160
- const pa = String(a).match(/^(\d+)([A-Z])?(\.\d+)?/i);
161
- const pb = String(b).match(/^(\d+)([A-Z])?(\.\d+)?/i);
164
+ const pa = String(a).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
165
+ const pb = String(b).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
162
166
  if (!pa || !pb) return String(a).localeCompare(String(b));
163
167
  const intDiff = parseInt(pa[1], 10) - parseInt(pb[1], 10);
164
168
  if (intDiff !== 0) return intDiff;
@@ -170,10 +174,18 @@ function comparePhaseNum(a, b) {
170
174
  if (!lb) return 1;
171
175
  return la < lb ? -1 : 1;
172
176
  }
173
- // No decimal sorts before decimal: 12A < 12A.1 < 12A.2
174
- const da = pa[3] ? parseFloat(pa[3]) : -1;
175
- const db = pb[3] ? parseFloat(pb[3]) : -1;
176
- return da - db;
177
+ // Segment-by-segment decimal comparison: 12A < 12A.1 < 12A.1.2 < 12A.2
178
+ const aDecParts = pa[3] ? pa[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
179
+ const bDecParts = pb[3] ? pb[3].slice(1).split('.').map(p => parseInt(p, 10)) : [];
180
+ const maxLen = Math.max(aDecParts.length, bDecParts.length);
181
+ if (aDecParts.length === 0 && bDecParts.length > 0) return -1;
182
+ if (bDecParts.length === 0 && aDecParts.length > 0) return 1;
183
+ for (let i = 0; i < maxLen; i++) {
184
+ const av = Number.isFinite(aDecParts[i]) ? aDecParts[i] : 0;
185
+ const bv = Number.isFinite(bDecParts[i]) ? bDecParts[i] : 0;
186
+ if (av !== bv) return av - bv;
187
+ }
188
+ return 0;
177
189
  }
178
190
 
179
191
  function searchPhaseInDir(baseDir, relBase, normalized) {
@@ -183,7 +195,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
183
195
  const match = dirs.find(d => d.startsWith(normalized));
184
196
  if (!match) return null;
185
197
 
186
- const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)?)-?(.*)/i);
198
+ const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
187
199
  const phaseNumber = dirMatch ? dirMatch[1] : normalized;
188
200
  const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
189
201
  const phaseDir = path.join(baseDir, match);
@@ -302,7 +314,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
302
314
 
303
315
  try {
304
316
  const content = fs.readFileSync(roadmapPath, 'utf-8');
305
- const escapedPhase = phaseNum.toString().replace(/\./g, '\\.');
317
+ const escapedPhase = escapeRegex(phaseNum.toString());
306
318
  const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i');
307
319
  const headerMatch = content.match(phasePattern);
308
320
  if (!headerMatch) return null;
@@ -385,6 +397,7 @@ module.exports = {
385
397
  loadConfig,
386
398
  isGitIgnored,
387
399
  execGit,
400
+ escapeRegex,
388
401
  normalizePhaseName,
389
402
  comparePhaseNum,
390
403
  searchPhaseInDir,
@@ -16,6 +16,13 @@ function cmdInitExecutePhase(cwd, phase, raw) {
16
16
  const phaseInfo = findPhaseInternal(cwd, phase);
17
17
  const milestone = getMilestoneInfo(cwd);
18
18
 
19
+ const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
20
+ const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
21
+ const reqExtracted = reqMatch
22
+ ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
23
+ : null;
24
+ const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
25
+
19
26
  const result = {
20
27
  // Models
21
28
  executor_model: resolveModelInternal(cwd, 'gsd-executor'),
@@ -35,6 +42,7 @@ function cmdInitExecutePhase(cwd, phase, raw) {
35
42
  phase_number: phaseInfo?.phase_number || null,
36
43
  phase_name: phaseInfo?.phase_name || null,
37
44
  phase_slug: phaseInfo?.phase_slug || null,
45
+ phase_req_ids,
38
46
 
39
47
  // Plan inventory
40
48
  plans: phaseInfo?.plans || [],
@@ -80,6 +88,13 @@ function cmdInitPlanPhase(cwd, phase, raw) {
80
88
  const config = loadConfig(cwd);
81
89
  const phaseInfo = findPhaseInternal(cwd, phase);
82
90
 
91
+ const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
92
+ const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
93
+ const reqExtracted = reqMatch
94
+ ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
95
+ : null;
96
+ const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
97
+
83
98
  const result = {
84
99
  // Models
85
100
  researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
@@ -99,6 +114,7 @@ function cmdInitPlanPhase(cwd, phase, raw) {
99
114
  phase_name: phaseInfo?.phase_name || null,
100
115
  phase_slug: phaseInfo?.phase_slug || null,
101
116
  padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null,
117
+ phase_req_ids,
102
118
 
103
119
  // Existing artifacts
104
120
  has_research: phaseInfo?.has_research || false,
@@ -595,7 +611,7 @@ function cmdInitProgress(cwd, raw) {
595
611
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
596
612
 
597
613
  for (const dir of dirs) {
598
- const match = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/);
614
+ const match = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
599
615
  const phaseNumber = match ? match[1] : dir;
600
616
  const phaseName = match && match[2] ? match[2] : null;
601
617
 
@@ -6,6 +6,7 @@ const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { output, error } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
+ const { writeStateMd } = require('./state.cjs');
9
10
 
10
11
  function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
11
12
  if (!reqIdsRaw || reqIdsRaw.length === 0) {
@@ -169,7 +170,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
169
170
  /(\*\*Last Activity Description:\*\*\s*).*/,
170
171
  `$1${version} milestone completed and archived`
171
172
  );
172
- fs.writeFileSync(statePath, stateContent, 'utf-8');
173
+ writeStateMd(statePath, stateContent, cwd);
173
174
  }
174
175
 
175
176
  // Archive phase directories if requested
@@ -4,8 +4,9 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, output, error } = require('./core.cjs');
7
+ const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, output, error } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
+ const { writeStateMd } = require('./state.cjs');
9
10
 
10
11
  function cmdPhasesList(cwd, options, raw) {
11
12
  const phasesDir = path.join(cwd, '.planning', 'phases');
@@ -70,7 +71,7 @@ function cmdPhasesList(cwd, options, raw) {
70
71
  const result = {
71
72
  files,
72
73
  count: files.length,
73
- phase_dir: phase ? dirs[0].replace(/^\d+(?:\.\d+)?-?/, '') : null,
74
+ phase_dir: phase ? dirs[0].replace(/^\d+(?:\.\d+)*-?/, '') : null,
74
75
  };
75
76
  output(result, raw, files.join('\n'));
76
77
  return;
@@ -121,11 +122,7 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
121
122
  }
122
123
 
123
124
  // Sort numerically
124
- existingDecimals.sort((a, b) => {
125
- const aNum = parseFloat(a);
126
- const bNum = parseFloat(b);
127
- return aNum - bNum;
128
- });
125
+ existingDecimals.sort((a, b) => comparePhaseNum(a, b));
129
126
 
130
127
  // Calculate next decimal
131
128
  let nextDecimal;
@@ -172,7 +169,7 @@ function cmdFindPhase(cwd, phase, raw) {
172
169
  return;
173
170
  }
174
171
 
175
- const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)?)-?(.*)/i);
172
+ const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
176
173
  const phaseNumber = dirMatch ? dirMatch[1] : normalized;
177
174
  const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
178
175
 
@@ -318,7 +315,7 @@ function cmdPhaseAdd(cwd, description, raw) {
318
315
  const slug = generateSlugInternal(description);
319
316
 
320
317
  // Find highest integer phase number
321
- const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)?:/gi;
318
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
322
319
  let maxPhase = 0;
323
320
  let m;
324
321
  while ((m = phasePattern.exec(content)) !== null) {
@@ -336,7 +333,7 @@ function cmdPhaseAdd(cwd, description, raw) {
336
333
  fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
337
334
 
338
335
  // Build phase entry
339
- const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${newPhaseNum} to break down)\n`;
336
+ const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${newPhaseNum} to break down)\n`;
340
337
 
341
338
  // Find insertion point: before last "---" or at end
342
339
  let updatedContent;
@@ -407,7 +404,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
407
404
  fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
408
405
 
409
406
  // Build phase entry
410
- const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${decimalPhase} to break down)\n`;
407
+ 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`;
411
408
 
412
409
  // Insert after the target phase section
413
410
  const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
@@ -599,7 +596,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
599
596
  let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
600
597
 
601
598
  // Remove the target phase section
602
- const targetEscaped = targetPhase.replace(/\./g, '\\.');
599
+ const targetEscaped = escapeRegex(targetPhase);
603
600
  const sectionPattern = new RegExp(
604
601
  `\\n?#{2,4}\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`,
605
602
  'i'
@@ -679,7 +676,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
679
676
  const oldTotal = parseInt(ofMatch[2], 10);
680
677
  stateContent = stateContent.replace(ofPattern, `$1${oldTotal - 1}$3`);
681
678
  }
682
- fs.writeFileSync(statePath, stateContent, 'utf-8');
679
+ writeStateMd(statePath, stateContent, cwd);
683
680
  }
684
681
 
685
682
  const result = {
@@ -720,13 +717,13 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
720
717
 
721
718
  // Checkbox: - [ ] Phase N: → - [x] Phase N: (...completed DATE)
722
719
  const checkboxPattern = new RegExp(
723
- `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseNum.replace('.', '\\.')}[:\\s][^\\n]*)`,
720
+ `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`,
724
721
  'i'
725
722
  );
726
723
  roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
727
724
 
728
725
  // Progress table: update Status to Complete, add date
729
- const phaseEscaped = phaseNum.replace('.', '\\.');
726
+ const phaseEscaped = escapeRegex(phaseNum);
730
727
  const tablePattern = new RegExp(
731
728
  `(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
732
729
  'i'
@@ -753,7 +750,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
753
750
  if (fs.existsSync(reqPath)) {
754
751
  // Extract Requirements line from roadmap for this phase
755
752
  const reqMatch = roadmapContent.match(
756
- new RegExp(`Phase\\s+${phaseNum.replace('.', '\\.')}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
753
+ new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
757
754
  );
758
755
 
759
756
  if (reqMatch) {
@@ -761,14 +758,15 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
761
758
  let reqContent = fs.readFileSync(reqPath, 'utf-8');
762
759
 
763
760
  for (const reqId of reqIds) {
761
+ const reqEscaped = escapeRegex(reqId);
764
762
  // Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
765
763
  reqContent = reqContent.replace(
766
- new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqId}\\*\\*)`, 'gi'),
764
+ new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
767
765
  '$1x$2'
768
766
  );
769
767
  // Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete |
770
768
  reqContent = reqContent.replace(
771
- new RegExp(`(\\|\\s*${reqId}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
769
+ new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
772
770
  '$1 Complete $2'
773
771
  );
774
772
  }
@@ -789,7 +787,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
789
787
 
790
788
  // Find the next phase directory after current
791
789
  for (const dir of dirs) {
792
- const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)?)-?(.*)/i);
790
+ const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
793
791
  if (dm) {
794
792
  if (comparePhaseNum(dm[1], phaseNum) > 0) {
795
793
  nextPhaseNum = dm[1];
@@ -843,7 +841,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
843
841
  `$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`
844
842
  );
845
843
 
846
- fs.writeFileSync(statePath, stateContent, 'utf-8');
844
+ writeStateMd(statePath, stateContent, cwd);
847
845
  }
848
846
 
849
847
  const result = {
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs');
7
+ const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs');
8
8
 
9
9
  function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
10
10
  const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
@@ -18,7 +18,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
18
18
  const content = fs.readFileSync(roadmapPath, 'utf-8');
19
19
 
20
20
  // Escape special regex chars in phase number, handle decimal
21
- const escapedPhase = phaseNum.replace(/\./g, '\\.');
21
+ const escapedPhase = escapeRegex(phaseNum);
22
22
 
23
23
  // Match "## Phase X:", "### Phase X:", or "#### Phase X:" with optional name
24
24
  const phasePattern = new RegExp(
@@ -102,7 +102,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
102
102
  const phasesDir = path.join(cwd, '.planning', 'phases');
103
103
 
104
104
  // Extract all phase headings: ## Phase N: Name or ### Phase N: Name
105
- const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)?)\s*:\s*([^\n]+)/gi;
105
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
106
106
  const phases = [];
107
107
  let match;
108
108
 
@@ -153,7 +153,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
153
153
  } catch {}
154
154
 
155
155
  // Check ROADMAP checkbox status
156
- const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum.replace('.', '\\.')}`, 'i');
156
+ const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}`, 'i');
157
157
  const checkboxMatch = content.match(checkboxPattern);
158
158
  const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
159
159
 
@@ -192,7 +192,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
192
192
  const completedPhases = phases.filter(p => p.disk_status === 'complete').length;
193
193
 
194
194
  // Detect phases in summary list without detail sections (malformed ROADMAP)
195
- const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:\.\d+)?)/gi;
195
+ const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:\.\d+)*)/gi;
196
196
  const checklistPhases = new Set();
197
197
  let checklistMatch;
198
198
  while ((checklistMatch = checklistPattern.exec(content)) !== null) {
@@ -208,7 +208,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
208
208
  completed_phases: completedPhases,
209
209
  total_plans: totalPlans,
210
210
  total_summaries: totalSummaries,
211
- progress_percent: totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0,
211
+ progress_percent: totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0,
212
212
  current_phase: currentPhase ? currentPhase.number : null,
213
213
  next_phase: nextPhase ? nextPhase.number : null,
214
214
  missing_phase_details: missingDetails.length > 0 ? missingDetails : null,
@@ -247,7 +247,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
247
247
  }
248
248
 
249
249
  let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
250
- const phaseEscaped = phaseNum.replace('.', '\\.');
250
+ const phaseEscaped = escapeRegex(phaseNum);
251
251
 
252
252
  // Progress table row: update Plans column (summaries/plans) and Status column
253
253
  const tablePattern = new RegExp(