@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.
@@ -4,7 +4,8 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { loadConfig, output, error } = require('./core.cjs');
7
+ const { loadConfig, getMilestoneInfo, output, error } = require('./core.cjs');
8
+ const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
8
9
 
9
10
  function cmdStateLoad(cwd, raw) {
10
11
  const config = loadConfig(cwd);
@@ -86,6 +87,17 @@ function cmdStateGet(cwd, section, raw) {
86
87
  }
87
88
  }
88
89
 
90
+ function readTextArgOrFile(cwd, value, filePath, label) {
91
+ if (!filePath) return value;
92
+
93
+ const resolvedPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
94
+ try {
95
+ return fs.readFileSync(resolvedPath, 'utf-8').trimEnd();
96
+ } catch {
97
+ throw new Error(`${label} file not found: ${filePath}`);
98
+ }
99
+ }
100
+
89
101
  function cmdStatePatch(cwd, patches, raw) {
90
102
  const statePath = path.join(cwd, '.planning', 'STATE.md');
91
103
  try {
@@ -97,7 +109,7 @@ function cmdStatePatch(cwd, patches, raw) {
97
109
  const pattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
98
110
 
99
111
  if (pattern.test(content)) {
100
- content = content.replace(pattern, `$1${value}`);
112
+ content = content.replace(pattern, (_match, prefix) => `${prefix}${value}`);
101
113
  results.updated.push(field);
102
114
  } else {
103
115
  results.failed.push(field);
@@ -105,7 +117,7 @@ function cmdStatePatch(cwd, patches, raw) {
105
117
  }
106
118
 
107
119
  if (results.updated.length > 0) {
108
- fs.writeFileSync(statePath, content, 'utf-8');
120
+ writeStateMd(statePath, content, cwd);
109
121
  }
110
122
 
111
123
  output(results, raw, results.updated.length > 0 ? 'true' : 'false');
@@ -125,8 +137,8 @@ function cmdStateUpdate(cwd, field, value) {
125
137
  const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
126
138
  const pattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
127
139
  if (pattern.test(content)) {
128
- content = content.replace(pattern, `$1${value}`);
129
- fs.writeFileSync(statePath, content, 'utf-8');
140
+ content = content.replace(pattern, (_match, prefix) => `${prefix}${value}`);
141
+ writeStateMd(statePath, content, cwd);
130
142
  output({ updated: true });
131
143
  } else {
132
144
  output({ updated: false, reason: `Field "${field}" not found in STATE.md` });
@@ -148,7 +160,7 @@ function stateReplaceField(content, fieldName, newValue) {
148
160
  const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
149
161
  const pattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
150
162
  if (pattern.test(content)) {
151
- return content.replace(pattern, `$1${newValue}`);
163
+ return content.replace(pattern, (_match, prefix) => `${prefix}${newValue}`);
152
164
  }
153
165
  return null;
154
166
  }
@@ -170,14 +182,14 @@ function cmdStateAdvancePlan(cwd, raw) {
170
182
  if (currentPlan >= totalPlans) {
171
183
  content = stateReplaceField(content, 'Status', 'Phase complete — ready for verification') || content;
172
184
  content = stateReplaceField(content, 'Last Activity', today) || content;
173
- fs.writeFileSync(statePath, content, 'utf-8');
185
+ writeStateMd(statePath, content, cwd);
174
186
  output({ advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' }, raw, 'false');
175
187
  } else {
176
188
  const newPlan = currentPlan + 1;
177
189
  content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
178
190
  content = stateReplaceField(content, 'Status', 'Ready to execute') || content;
179
191
  content = stateReplaceField(content, 'Last Activity', today) || content;
180
- fs.writeFileSync(statePath, content, 'utf-8');
192
+ writeStateMd(statePath, content, cwd);
181
193
  output({ advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans }, raw, 'true');
182
194
  }
183
195
  }
@@ -199,7 +211,6 @@ function cmdStateRecordMetric(cwd, options, raw) {
199
211
  const metricsMatch = content.match(metricsPattern);
200
212
 
201
213
  if (metricsMatch) {
202
- const tableHeader = metricsMatch[1];
203
214
  let tableBody = metricsMatch[2].trimEnd();
204
215
  const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
205
216
 
@@ -209,8 +220,8 @@ function cmdStateRecordMetric(cwd, options, raw) {
209
220
  tableBody = tableBody + '\n' + newRow;
210
221
  }
211
222
 
212
- content = content.replace(metricsPattern, `${tableHeader}${tableBody}\n`);
213
- fs.writeFileSync(statePath, content, 'utf-8');
223
+ content = content.replace(metricsPattern, (_match, header) => `${header}${tableBody}\n`);
224
+ writeStateMd(statePath, content, cwd);
214
225
  output({ recorded: true, phase, plan, duration }, raw, 'true');
215
226
  } else {
216
227
  output({ recorded: false, reason: 'Performance Metrics section not found in STATE.md' }, raw, 'false');
@@ -238,7 +249,7 @@ function cmdStateUpdateProgress(cwd, raw) {
238
249
  }
239
250
  }
240
251
 
241
- const percent = totalPlans > 0 ? Math.round(totalSummaries / totalPlans * 100) : 0;
252
+ const percent = totalPlans > 0 ? Math.min(100, Math.round(totalSummaries / totalPlans * 100)) : 0;
242
253
  const barWidth = 10;
243
254
  const filled = Math.round(percent / 100 * barWidth);
244
255
  const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
@@ -246,8 +257,8 @@ function cmdStateUpdateProgress(cwd, raw) {
246
257
 
247
258
  const progressPattern = /(\*\*Progress:\*\*\s*).*/i;
248
259
  if (progressPattern.test(content)) {
249
- content = content.replace(progressPattern, `$1${progressStr}`);
250
- fs.writeFileSync(statePath, content, 'utf-8');
260
+ content = content.replace(progressPattern, (_match, prefix) => `${prefix}${progressStr}`);
261
+ writeStateMd(statePath, content, cwd);
251
262
  output({ updated: true, percent, completed: totalSummaries, total: totalPlans, bar: progressStr }, raw, progressStr);
252
263
  } else {
253
264
  output({ updated: false, reason: 'Progress field not found in STATE.md' }, raw, 'false');
@@ -258,11 +269,22 @@ function cmdStateAddDecision(cwd, options, raw) {
258
269
  const statePath = path.join(cwd, '.planning', 'STATE.md');
259
270
  if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
260
271
 
261
- const { phase, summary, rationale } = options;
262
- if (!summary) { output({ error: 'summary required' }, raw); return; }
272
+ const { phase, summary, summary_file, rationale, rationale_file } = options;
273
+ let summaryText = null;
274
+ let rationaleText = '';
275
+
276
+ try {
277
+ summaryText = readTextArgOrFile(cwd, summary, summary_file, 'summary');
278
+ rationaleText = readTextArgOrFile(cwd, rationale || '', rationale_file, 'rationale');
279
+ } catch (err) {
280
+ output({ added: false, reason: err.message }, raw, 'false');
281
+ return;
282
+ }
283
+
284
+ if (!summaryText) { output({ error: 'summary required' }, raw); return; }
263
285
 
264
286
  let content = fs.readFileSync(statePath, 'utf-8');
265
- const entry = `- [Phase ${phase || '?'}]: ${summary}${rationale ? ` — ${rationale}` : ''}`;
287
+ const entry = `- [Phase ${phase || '?'}]: ${summaryText}${rationaleText ? ` — ${rationaleText}` : ''}`;
266
288
 
267
289
  // Find Decisions section (various heading patterns)
268
290
  const sectionPattern = /(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
@@ -273,8 +295,8 @@ function cmdStateAddDecision(cwd, options, raw) {
273
295
  // Remove placeholders
274
296
  sectionBody = sectionBody.replace(/None yet\.?\s*\n?/gi, '').replace(/No decisions yet\.?\s*\n?/gi, '');
275
297
  sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
276
- content = content.replace(sectionPattern, `${match[1]}${sectionBody}`);
277
- fs.writeFileSync(statePath, content, 'utf-8');
298
+ content = content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
299
+ writeStateMd(statePath, content, cwd);
278
300
  output({ added: true, decision: entry }, raw, 'true');
279
301
  } else {
280
302
  output({ added: false, reason: 'Decisions section not found in STATE.md' }, raw, 'false');
@@ -284,10 +306,20 @@ function cmdStateAddDecision(cwd, options, raw) {
284
306
  function cmdStateAddBlocker(cwd, text, raw) {
285
307
  const statePath = path.join(cwd, '.planning', 'STATE.md');
286
308
  if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
287
- if (!text) { output({ error: 'text required' }, raw); return; }
309
+ const blockerOptions = typeof text === 'object' && text !== null ? text : { text };
310
+ let blockerText = null;
311
+
312
+ try {
313
+ blockerText = readTextArgOrFile(cwd, blockerOptions.text, blockerOptions.text_file, 'blocker');
314
+ } catch (err) {
315
+ output({ added: false, reason: err.message }, raw, 'false');
316
+ return;
317
+ }
318
+
319
+ if (!blockerText) { output({ error: 'text required' }, raw); return; }
288
320
 
289
321
  let content = fs.readFileSync(statePath, 'utf-8');
290
- const entry = `- ${text}`;
322
+ const entry = `- ${blockerText}`;
291
323
 
292
324
  const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
293
325
  const match = content.match(sectionPattern);
@@ -296,9 +328,9 @@ function cmdStateAddBlocker(cwd, text, raw) {
296
328
  let sectionBody = match[2];
297
329
  sectionBody = sectionBody.replace(/None\.?\s*\n?/gi, '').replace(/None yet\.?\s*\n?/gi, '');
298
330
  sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
299
- content = content.replace(sectionPattern, `${match[1]}${sectionBody}`);
300
- fs.writeFileSync(statePath, content, 'utf-8');
301
- output({ added: true, blocker: text }, raw, 'true');
331
+ content = content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
332
+ writeStateMd(statePath, content, cwd);
333
+ output({ added: true, blocker: blockerText }, raw, 'true');
302
334
  } else {
303
335
  output({ added: false, reason: 'Blockers section not found in STATE.md' }, raw, 'false');
304
336
  }
@@ -328,8 +360,8 @@ function cmdStateResolveBlocker(cwd, text, raw) {
328
360
  newBody = 'None\n';
329
361
  }
330
362
 
331
- content = content.replace(sectionPattern, `${match[1]}${newBody}`);
332
- fs.writeFileSync(statePath, content, 'utf-8');
363
+ content = content.replace(sectionPattern, (_match, header) => `${header}${newBody}`);
364
+ writeStateMd(statePath, content, cwd);
333
365
  output({ resolved: true, blocker: text }, raw, 'true');
334
366
  } else {
335
367
  output({ resolved: false, reason: 'Blockers section not found in STATE.md' }, raw, 'false');
@@ -364,7 +396,7 @@ function cmdStateRecordSession(cwd, options, raw) {
364
396
  if (result) { content = result; updated.push('Resume File'); }
365
397
 
366
398
  if (updated.length > 0) {
367
- fs.writeFileSync(statePath, content, 'utf-8');
399
+ writeStateMd(statePath, content, cwd);
368
400
  output({ recorded: true, updated }, raw, 'true');
369
401
  } else {
370
402
  output({ recorded: false, reason: 'No session fields found in STATE.md' }, raw, 'false');
@@ -472,9 +504,165 @@ function cmdStateSnapshot(cwd, raw) {
472
504
  output(result, raw);
473
505
  }
474
506
 
507
+ // ─── State Frontmatter Sync ──────────────────────────────────────────────────
508
+
509
+ /**
510
+ * Extract machine-readable fields from STATE.md markdown body and build
511
+ * a YAML frontmatter object. Allows hooks and scripts to read state
512
+ * reliably via `state json` instead of fragile regex parsing.
513
+ */
514
+ function buildStateFrontmatter(bodyContent, cwd) {
515
+ const extractField = (fieldName) => {
516
+ const pattern = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*(.+)`, 'i');
517
+ const match = bodyContent.match(pattern);
518
+ return match ? match[1].trim() : null;
519
+ };
520
+
521
+ const currentPhase = extractField('Current Phase');
522
+ const currentPhaseName = extractField('Current Phase Name');
523
+ const currentPlan = extractField('Current Plan');
524
+ const totalPhasesRaw = extractField('Total Phases');
525
+ const totalPlansRaw = extractField('Total Plans in Phase');
526
+ const status = extractField('Status');
527
+ const progressRaw = extractField('Progress');
528
+ const lastActivity = extractField('Last Activity');
529
+ const stoppedAt = extractField('Stopped At') || extractField('Stopped at');
530
+ const pausedAt = extractField('Paused At');
531
+
532
+ let milestone = null;
533
+ let milestoneName = null;
534
+ if (cwd) {
535
+ try {
536
+ const info = getMilestoneInfo(cwd);
537
+ milestone = info.version;
538
+ milestoneName = info.name;
539
+ } catch {}
540
+ }
541
+
542
+ let totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
543
+ let completedPhases = null;
544
+ let totalPlans = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
545
+ let completedPlans = null;
546
+
547
+ if (cwd) {
548
+ try {
549
+ const phasesDir = path.join(cwd, '.planning', 'phases');
550
+ if (fs.existsSync(phasesDir)) {
551
+ const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
552
+ .filter(e => e.isDirectory()).map(e => e.name);
553
+ let diskTotalPlans = 0;
554
+ let diskTotalSummaries = 0;
555
+ let diskCompletedPhases = 0;
556
+
557
+ for (const dir of phaseDirs) {
558
+ const files = fs.readdirSync(path.join(phasesDir, dir));
559
+ const plans = files.filter(f => f.match(/-PLAN\.md$/i)).length;
560
+ const summaries = files.filter(f => f.match(/-SUMMARY\.md$/i)).length;
561
+ diskTotalPlans += plans;
562
+ diskTotalSummaries += summaries;
563
+ if (plans > 0 && summaries >= plans) diskCompletedPhases++;
564
+ }
565
+ if (totalPhases === null) totalPhases = phaseDirs.length;
566
+ completedPhases = diskCompletedPhases;
567
+ totalPlans = diskTotalPlans;
568
+ completedPlans = diskTotalSummaries;
569
+ }
570
+ } catch {}
571
+ }
572
+
573
+ let progressPercent = null;
574
+ if (progressRaw) {
575
+ const pctMatch = progressRaw.match(/(\d+)%/);
576
+ if (pctMatch) progressPercent = parseInt(pctMatch[1], 10);
577
+ }
578
+
579
+ // Normalize status to one of: planning, discussing, executing, verifying, paused, completed, unknown
580
+ let normalizedStatus = status || 'unknown';
581
+ const statusLower = (status || '').toLowerCase();
582
+ if (statusLower.includes('paused') || statusLower.includes('stopped') || pausedAt) {
583
+ normalizedStatus = 'paused';
584
+ } else if (statusLower.includes('executing') || statusLower.includes('in progress')) {
585
+ normalizedStatus = 'executing';
586
+ } else if (statusLower.includes('planning') || statusLower.includes('ready to plan')) {
587
+ normalizedStatus = 'planning';
588
+ } else if (statusLower.includes('discussing')) {
589
+ normalizedStatus = 'discussing';
590
+ } else if (statusLower.includes('verif')) {
591
+ normalizedStatus = 'verifying';
592
+ } else if (statusLower.includes('complete') || statusLower.includes('done')) {
593
+ normalizedStatus = 'completed';
594
+ } else if (statusLower.includes('ready to execute')) {
595
+ normalizedStatus = 'executing';
596
+ }
597
+
598
+ const fm = { gsd_state_version: '1.0' };
599
+
600
+ if (milestone) fm.milestone = milestone;
601
+ if (milestoneName) fm.milestone_name = milestoneName;
602
+ if (currentPhase) fm.current_phase = currentPhase;
603
+ if (currentPhaseName) fm.current_phase_name = currentPhaseName;
604
+ if (currentPlan) fm.current_plan = currentPlan;
605
+ fm.status = normalizedStatus;
606
+ if (stoppedAt) fm.stopped_at = stoppedAt;
607
+ if (pausedAt) fm.paused_at = pausedAt;
608
+ fm.last_updated = new Date().toISOString();
609
+ if (lastActivity) fm.last_activity = lastActivity;
610
+
611
+ const progress = {};
612
+ if (totalPhases !== null) progress.total_phases = totalPhases;
613
+ if (completedPhases !== null) progress.completed_phases = completedPhases;
614
+ if (totalPlans !== null) progress.total_plans = totalPlans;
615
+ if (completedPlans !== null) progress.completed_plans = completedPlans;
616
+ if (progressPercent !== null) progress.percent = progressPercent;
617
+ if (Object.keys(progress).length > 0) fm.progress = progress;
618
+
619
+ return fm;
620
+ }
621
+
622
+ function stripFrontmatter(content) {
623
+ return content.replace(/^---\n[\s\S]*?\n---\n*/, '');
624
+ }
625
+
626
+ function syncStateFrontmatter(content, cwd) {
627
+ const body = stripFrontmatter(content);
628
+ const fm = buildStateFrontmatter(body, cwd);
629
+ const yamlStr = reconstructFrontmatter(fm);
630
+ return `---\n${yamlStr}\n---\n\n${body}`;
631
+ }
632
+
633
+ /**
634
+ * Write STATE.md with synchronized YAML frontmatter.
635
+ * All STATE.md writes should use this instead of raw writeFileSync.
636
+ */
637
+ function writeStateMd(statePath, content, cwd) {
638
+ const synced = syncStateFrontmatter(content, cwd);
639
+ fs.writeFileSync(statePath, synced, 'utf-8');
640
+ }
641
+
642
+ function cmdStateJson(cwd, raw) {
643
+ const statePath = path.join(cwd, '.planning', 'STATE.md');
644
+ if (!fs.existsSync(statePath)) {
645
+ output({ error: 'STATE.md not found' }, raw, 'STATE.md not found');
646
+ return;
647
+ }
648
+
649
+ const content = fs.readFileSync(statePath, 'utf-8');
650
+ const fm = extractFrontmatter(content);
651
+
652
+ if (!fm || Object.keys(fm).length === 0) {
653
+ const body = stripFrontmatter(content);
654
+ const built = buildStateFrontmatter(body, cwd);
655
+ output(built, raw, JSON.stringify(built, null, 2));
656
+ return;
657
+ }
658
+
659
+ output(fm, raw, JSON.stringify(fm, null, 2));
660
+ }
661
+
475
662
  module.exports = {
476
663
  stateExtractField,
477
664
  stateReplaceField,
665
+ writeStateMd,
478
666
  cmdStateLoad,
479
667
  cmdStateGet,
480
668
  cmdStatePatch,
@@ -487,4 +675,5 @@ module.exports = {
487
675
  cmdStateResolveBlocker,
488
676
  cmdStateRecordSession,
489
677
  cmdStateSnapshot,
678
+ cmdStateJson,
490
679
  };
@@ -6,6 +6,7 @@ const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, output, error } = require('./core.cjs');
8
8
  const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
9
+ const { writeStateMd } = require('./state.cjs');
9
10
 
10
11
  function cmdVerifySummary(cwd, summaryPath, checkFileCount, raw) {
11
12
  if (!summaryPath) {
@@ -410,7 +411,7 @@ function cmdValidateConsistency(cwd, raw) {
410
411
 
411
412
  // Extract phases from ROADMAP
412
413
  const roadmapPhases = new Set();
413
- const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)?)\s*:/gi;
414
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
414
415
  let m;
415
416
  while ((m = phasePattern.exec(roadmapContent)) !== null) {
416
417
  roadmapPhases.add(m[1]);
@@ -422,7 +423,7 @@ function cmdValidateConsistency(cwd, raw) {
422
423
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
423
424
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
424
425
  for (const dir of dirs) {
425
- const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)?)/i);
426
+ const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
426
427
  if (dm) diskPhases.add(dm[1]);
427
428
  }
428
429
  } catch {}
@@ -572,14 +573,14 @@ function cmdValidateHealth(cwd, options, raw) {
572
573
  } else {
573
574
  const stateContent = fs.readFileSync(statePath, 'utf-8');
574
575
  // Extract phase references from STATE.md
575
- const phaseRefs = [...stateContent.matchAll(/[Pp]hase\s+(\d+(?:\.\d+)?)/g)].map(m => m[1]);
576
+ const phaseRefs = [...stateContent.matchAll(/[Pp]hase\s+(\d+(?:\.\d+)*)/g)].map(m => m[1]);
576
577
  // Get disk phases
577
578
  const diskPhases = new Set();
578
579
  try {
579
580
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
580
581
  for (const e of entries) {
581
582
  if (e.isDirectory()) {
582
- const m = e.name.match(/^(\d+(?:\.\d+)?)/);
583
+ const m = e.name.match(/^(\d+(?:\.\d+)*)/);
583
584
  if (m) diskPhases.add(m[1]);
584
585
  }
585
586
  }
@@ -620,7 +621,7 @@ function cmdValidateHealth(cwd, options, raw) {
620
621
  try {
621
622
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
622
623
  for (const e of entries) {
623
- if (e.isDirectory() && !e.name.match(/^\d{2}(?:\.\d+)?-[\w-]+$/)) {
624
+ if (e.isDirectory() && !e.name.match(/^\d{2}(?:\.\d+)*-[\w-]+$/)) {
624
625
  addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
625
626
  }
626
627
  }
@@ -650,7 +651,7 @@ function cmdValidateHealth(cwd, options, raw) {
650
651
  if (fs.existsSync(roadmapPath)) {
651
652
  const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
652
653
  const roadmapPhases = new Set();
653
- const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)?)\s*:/gi;
654
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
654
655
  let m;
655
656
  while ((m = phasePattern.exec(roadmapContent)) !== null) {
656
657
  roadmapPhases.add(m[1]);
@@ -661,7 +662,7 @@ function cmdValidateHealth(cwd, options, raw) {
661
662
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
662
663
  for (const e of entries) {
663
664
  if (e.isDirectory()) {
664
- const dm = e.name.match(/^(\d+[A-Z]?(?:\.\d+)?)/i);
665
+ const dm = e.name.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
665
666
  if (dm) diskPhases.add(dm[1]);
666
667
  }
667
668
  }
@@ -725,7 +726,7 @@ function cmdValidateHealth(cwd, options, raw) {
725
726
  stateContent += `**Status:** Resuming\n\n`;
726
727
  stateContent += `## Session Log\n\n`;
727
728
  stateContent += `- ${new Date().toISOString().split('T')[0]}: STATE.md regenerated by /gsd:health --repair\n`;
728
- fs.writeFileSync(statePath, stateContent, 'utf-8');
729
+ writeStateMd(statePath, stateContent, cwd);
729
730
  repairActions.push({ action: repair, success: true, path: 'STATE.md' });
730
731
  break;
731
732
  }
@@ -8,7 +8,7 @@ Template for `.planning/debug/[slug].md` — active debug session tracking.
8
8
 
9
9
  ```markdown
10
10
  ---
11
- status: gathering | investigating | fixing | verifying | resolved
11
+ status: gathering | investigating | fixing | verifying | awaiting_human_verify | resolved
12
12
  trigger: "[verbatim user input]"
13
13
  created: [ISO timestamp]
14
14
  updated: [ISO timestamp]
@@ -127,9 +127,14 @@ files_changed: []
127
127
  - Update Resolution.verification with results
128
128
  - If verification fails: status → "investigating", try again
129
129
 
130
+ **After self-verification passes:**
131
+ - status -> "awaiting_human_verify"
132
+ - Request explicit user confirmation in a checkpoint
133
+ - Do NOT move file to resolved yet
134
+
130
135
  **On resolution:**
131
136
  - status → "resolved"
132
- - Move file to .planning/debug/resolved/
137
+ - Move file to .planning/debug/resolved/ (only after user confirms fix)
133
138
 
134
139
  </lifecycle>
135
140
 
@@ -9,9 +9,7 @@ created: {date}
9
9
 
10
10
  # Phase {N} — Validation Strategy
11
11
 
12
- > Generated by `gsd-phase-researcher` during `/gsd:plan-phase {N}`.
13
- > Updated by `gsd-plan-checker` after plan approval.
14
- > Governs feedback sampling during `/gsd:execute-phase {N}`.
12
+ > Per-phase validation contract for feedback sampling during execution.
15
13
 
16
14
  ---
17
15
 
@@ -20,22 +18,19 @@ created: {date}
20
18
  | Property | Value |
21
19
  |----------|-------|
22
20
  | **Framework** | {pytest 7.x / jest 29.x / vitest / go test / other} |
23
- | **Config file** | {path/to/pytest.ini or "none — Wave 0 installs"} |
24
- | **Quick run command** | `{e.g., pytest -x --tb=short}` |
25
- | **Full suite command** | `{e.g., pytest tests/ --tb=short}` |
21
+ | **Config file** | {path or "none — Wave 0 installs"} |
22
+ | **Quick run command** | `{quick command}` |
23
+ | **Full suite command** | `{full command}` |
26
24
  | **Estimated runtime** | ~{N} seconds |
27
- | **CI pipeline** | {.github/workflows/test.yml — exists / needs creation} |
28
25
 
29
26
  ---
30
27
 
31
- ## Nyquist Sampling Rate
32
-
33
- > The minimum feedback frequency required to reliably catch errors in this phase.
28
+ ## Sampling Rate
34
29
 
35
30
  - **After every task commit:** Run `{quick run command}`
36
31
  - **After every plan wave:** Run `{full suite command}`
37
32
  - **Before `/gsd:verify-work`:** Full suite must be green
38
- - **Maximum acceptable task feedback latency:** {N} seconds
33
+ - **Max feedback latency:** {N} seconds
39
34
 
40
35
  ---
41
36
 
@@ -43,62 +38,39 @@ created: {date}
43
38
 
44
39
  | Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
45
40
  |---------|------|------|-------------|-----------|-------------------|-------------|--------|
46
- | {N}-01-01 | 01 | 1 | REQ-{XX} | unit | `pytest tests/test_{module}.py::test_{name} -x` | ✅ / ❌ W0 | ⬜ pending |
47
- | {N}-01-02 | 01 | 1 | REQ-{XX} | integration | `pytest tests/test_{flow}.py -x` | ✅ / ❌ W0 | ⬜ pending |
48
- | {N}-02-01 | 02 | 2 | REQ-{XX} | smoke | `curl -s {endpoint} \| grep {expected}` | ✅ N/A | ⬜ pending |
41
+ | {N}-01-01 | 01 | 1 | REQ-{XX} | unit | `{command}` | ✅ / ❌ W0 | ⬜ pending |
49
42
 
50
- *Status values: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
43
+ *Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
51
44
 
52
45
  ---
53
46
 
54
47
  ## Wave 0 Requirements
55
48
 
56
- > Test scaffolding committed BEFORE any implementation task. Executor runs Wave 0 first.
57
-
58
- - [ ] `{tests/test_file.py}` — stubs for REQ-{XX}, REQ-{XX}
49
+ - [ ] `{tests/test_file.py}` stubs for REQ-{XX}
59
50
  - [ ] `{tests/conftest.py}` — shared fixtures
60
51
  - [ ] `{framework install}` — if no framework detected
61
52
 
62
- *If none required: "Existing infrastructure covers all phase requirements — no Wave 0 test tasks needed."*
53
+ *If none: "Existing infrastructure covers all phase requirements."*
63
54
 
64
55
  ---
65
56
 
66
57
  ## Manual-Only Verifications
67
58
 
68
- > Behaviors that genuinely cannot be automated, with justification.
69
- > These are surfaced during `/gsd:verify-work` UAT.
70
-
71
59
  | Behavior | Requirement | Why Manual | Test Instructions |
72
60
  |----------|-------------|------------|-------------------|
73
- | {behavior} | REQ-{XX} | {reason: visual, third-party auth, physical device...} | {step-by-step} |
61
+ | {behavior} | REQ-{XX} | {reason} | {steps} |
74
62
 
75
- *If none: "All phase behaviors have automated verification coverage."*
63
+ *If none: "All phase behaviors have automated verification."*
76
64
 
77
65
  ---
78
66
 
79
67
  ## Validation Sign-Off
80
68
 
81
- Updated by `gsd-plan-checker` when plans are approved:
82
-
83
- - [ ] All tasks have `<automated>` verify commands or Wave 0 dependencies
84
- - [ ] No 3 consecutive implementation tasks without automated verify (sampling continuity)
85
- - [ ] Wave 0 test files cover all MISSING references
86
- - [ ] No watch-mode flags in any automated command
87
- - [ ] Feedback latency per task: < {N}s ✅
69
+ - [ ] All tasks have `<automated>` verify or Wave 0 dependencies
70
+ - [ ] Sampling continuity: no 3 consecutive tasks without automated verify
71
+ - [ ] Wave 0 covers all MISSING references
72
+ - [ ] No watch-mode flags
73
+ - [ ] Feedback latency < {N}s
88
74
  - [ ] `nyquist_compliant: true` set in frontmatter
89
75
 
90
- **Plan-checker approval:** {pending / approved on YYYY-MM-DD}
91
-
92
- ---
93
-
94
- ## Execution Tracking
95
-
96
- Updated during `/gsd:execute-phase {N}`:
97
-
98
- | Wave | Tasks | Tests Run | Pass | Fail | Sampling Status |
99
- |------|-------|-----------|------|------|-----------------|
100
- | 0 | {N} | — | — | — | scaffold |
101
- | 1 | {N} | {command} | {N} | {N} | ✅ sampled |
102
- | 2 | {N} | {command} | {N} | {N} | ✅ sampled |
103
-
104
- **Phase validation complete:** {pending / YYYY-MM-DD HH:MM}
76
+ **Approval:** {pending / approved YYYY-MM-DD}
@@ -0,0 +1,54 @@
1
+ # Project Retrospective
2
+
3
+ *A living document updated after each milestone. Lessons feed forward into future planning.*
4
+
5
+ ## Milestone: v{version} — {name}
6
+
7
+ **Shipped:** {date}
8
+ **Phases:** {count} | **Plans:** {count} | **Sessions:** {count}
9
+
10
+ ### What Was Built
11
+ - {Key deliverable 1}
12
+ - {Key deliverable 2}
13
+ - {Key deliverable 3}
14
+
15
+ ### What Worked
16
+ - {Efficiency win or successful pattern}
17
+ - {What went smoothly}
18
+
19
+ ### What Was Inefficient
20
+ - {Missed opportunity}
21
+ - {What took longer than expected}
22
+
23
+ ### Patterns Established
24
+ - {New pattern or convention that should persist}
25
+
26
+ ### Key Lessons
27
+ 1. {Specific, actionable lesson}
28
+ 2. {Another lesson}
29
+
30
+ ### Cost Observations
31
+ - Model mix: {X}% opus, {Y}% sonnet, {Z}% haiku
32
+ - Sessions: {count}
33
+ - Notable: {efficiency observation}
34
+
35
+ ---
36
+
37
+ ## Cross-Milestone Trends
38
+
39
+ ### Process Evolution
40
+
41
+ | Milestone | Sessions | Phases | Key Change |
42
+ |-----------|----------|--------|------------|
43
+ | v{X} | {N} | {M} | {What changed in process} |
44
+
45
+ ### Cumulative Quality
46
+
47
+ | Milestone | Tests | Coverage | Zero-Dep Additions |
48
+ |-----------|-------|----------|-------------------|
49
+ | v{X} | {N} | {Y}% | {count} |
50
+
51
+ ### Top Lessons (Verified Across Milestones)
52
+
53
+ 1. {Lesson verified by multiple milestones}
54
+ 2. {Another cross-validated lesson}