@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.
- package/AGENTS.md +29 -29
- package/README.md +102 -30
- package/agents/gsd-debugger.md +53 -8
- package/agents/gsd-planner.md +86 -5
- package/agents/gsd-verifier.md +15 -0
- package/bin/install.js +524 -37
- package/commands/gsd/add-tests.md +41 -0
- package/commands/gsd/debug.md +3 -0
- package/commands/gsd/join-discord.md +1 -1
- package/commands/gsd/plan-phase.md +2 -1
- package/get-shit-done/bin/gsd-tools.cjs +39 -4
- package/get-shit-done/bin/lib/commands.cjs +5 -8
- package/get-shit-done/bin/lib/core.cjs +22 -9
- package/get-shit-done/bin/lib/init.cjs +17 -1
- package/get-shit-done/bin/lib/milestone.cjs +2 -1
- package/get-shit-done/bin/lib/phase.cjs +18 -20
- package/get-shit-done/bin/lib/roadmap.cjs +7 -7
- package/get-shit-done/bin/lib/state.cjs +216 -27
- package/get-shit-done/bin/lib/verify.cjs +9 -8
- package/get-shit-done/templates/DEBUG.md +7 -2
- package/get-shit-done/templates/VALIDATION.md +18 -46
- package/get-shit-done/templates/retrospective.md +54 -0
- package/get-shit-done/workflows/add-tests.md +350 -0
- package/get-shit-done/workflows/complete-milestone.md +63 -0
- package/get-shit-done/workflows/discuss-phase.md +2 -0
- package/get-shit-done/workflows/help.md +3 -0
- package/package.json +2 -1
|
@@ -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, `$
|
|
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
|
-
|
|
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, `$
|
|
129
|
-
|
|
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, `$
|
|
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
|
-
|
|
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
|
-
|
|
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, `${
|
|
213
|
-
|
|
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, `$
|
|
250
|
-
|
|
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
|
-
|
|
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 || '?'}]: ${
|
|
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, `${
|
|
277
|
-
|
|
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
|
-
|
|
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 = `- ${
|
|
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, `${
|
|
300
|
-
|
|
301
|
-
output({ added: true, blocker:
|
|
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, `${
|
|
332
|
-
|
|
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
|
-
|
|
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+)
|
|
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+)
|
|
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+)
|
|
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+)
|
|
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+)
|
|
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+)
|
|
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
|
-
|
|
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
|
-
>
|
|
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
|
|
24
|
-
| **Quick run command** | `{
|
|
25
|
-
| **Full suite command** | `{
|
|
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
|
-
##
|
|
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
|
-
- **
|
|
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 | `
|
|
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
|
|
43
|
+
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
|
51
44
|
|
|
52
45
|
---
|
|
53
46
|
|
|
54
47
|
## Wave 0 Requirements
|
|
55
48
|
|
|
56
|
-
|
|
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
|
|
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
|
|
61
|
+
| {behavior} | REQ-{XX} | {reason} | {steps} |
|
|
74
62
|
|
|
75
|
-
*If none: "All phase behaviors have automated verification
|
|
63
|
+
*If none: "All phase behaviors have automated verification."*
|
|
76
64
|
|
|
77
65
|
---
|
|
78
66
|
|
|
79
67
|
## Validation Sign-Off
|
|
80
68
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
- [ ]
|
|
84
|
-
- [ ] No
|
|
85
|
-
- [ ]
|
|
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
|
-
**
|
|
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}
|