gsdd-cli 0.16.1 → 0.18.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 (50) hide show
  1. package/README.md +111 -61
  2. package/agents/README.md +1 -1
  3. package/bin/adapters/claude.mjs +8 -1
  4. package/bin/adapters/codex.mjs +5 -1
  5. package/bin/adapters/opencode.mjs +7 -1
  6. package/bin/gsdd.mjs +25 -20
  7. package/bin/lib/evidence-contract.mjs +112 -0
  8. package/bin/lib/health-truth.mjs +29 -31
  9. package/bin/lib/health.mjs +61 -106
  10. package/bin/lib/init-flow.mjs +91 -34
  11. package/bin/lib/init-runtime.mjs +46 -25
  12. package/bin/lib/init.mjs +3 -7
  13. package/bin/lib/lifecycle-preflight.mjs +333 -0
  14. package/bin/lib/lifecycle-state.mjs +476 -0
  15. package/bin/lib/manifest.mjs +24 -20
  16. package/bin/lib/phase.mjs +81 -26
  17. package/bin/lib/provenance.mjs +295 -54
  18. package/bin/lib/rendering.mjs +70 -12
  19. package/bin/lib/runtime-freshness.mjs +264 -0
  20. package/bin/lib/session-fingerprint.mjs +106 -0
  21. package/distilled/DESIGN.md +707 -13
  22. package/distilled/EVIDENCE-INDEX.md +166 -1
  23. package/distilled/README.md +44 -28
  24. package/distilled/SKILL.md +4 -4
  25. package/distilled/templates/agents.block.md +1 -1
  26. package/distilled/workflows/audit-milestone.md +50 -0
  27. package/distilled/workflows/complete-milestone.md +38 -2
  28. package/distilled/workflows/execute.md +16 -3
  29. package/distilled/workflows/map-codebase.md +15 -4
  30. package/distilled/workflows/new-milestone.md +53 -24
  31. package/distilled/workflows/new-project.md +44 -25
  32. package/distilled/workflows/pause.md +1 -1
  33. package/distilled/workflows/plan.md +187 -74
  34. package/distilled/workflows/progress.md +135 -31
  35. package/distilled/workflows/quick.md +20 -12
  36. package/distilled/workflows/resume.md +152 -65
  37. package/distilled/workflows/verify.md +55 -20
  38. package/docs/BROWNFIELD-PROOF.md +95 -0
  39. package/docs/RUNTIME-SUPPORT.md +77 -0
  40. package/docs/USER-GUIDE.md +443 -0
  41. package/docs/VERIFICATION-DISCIPLINE.md +59 -0
  42. package/docs/claude/context-monitor.md +98 -0
  43. package/docs/proof/consumer-node-cli/README.md +37 -0
  44. package/docs/proof/consumer-node-cli/ROADMAP.md +14 -0
  45. package/docs/proof/consumer-node-cli/SPEC.md +17 -0
  46. package/docs/proof/consumer-node-cli/brief.md +9 -0
  47. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -0
  48. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -0
  49. package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -0
  50. package/package.json +26 -20
@@ -0,0 +1,476 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ const BROWNFIELD_CHANGE_DIR = 'brownfield-change';
5
+
6
+ const PHASE_LINE_RE = /^\s*[-*]\s*\[([ x-])\]\s*\*\*Phase\s+(\d+(?:\.\d+)*[a-z]?):\s*(.+?)\*\*(?:\s+—\s+\[([^\]]+)])?/i;
7
+ const ACTIVE_MILESTONE_HEADING_RE = /^###\s+(v[^\s]+)\s+(.+)$/im;
8
+ const MILESTONE_LEDGER_HEADING_RE = /^##\s+(?:✅\s+)?(v[^\s]+)\s*(?:—|-)?\s*(.*)$/i;
9
+
10
+ export function evaluateLifecycleState({ planningDir, provenance = null } = {}) {
11
+ if (!planningDir) {
12
+ throw new Error('planningDir is required');
13
+ }
14
+
15
+ const specPath = join(planningDir, 'SPEC.md');
16
+ const roadmapPath = join(planningDir, 'ROADMAP.md');
17
+ const milestonesPath = join(planningDir, 'MILESTONES.md');
18
+ const phasesDir = join(planningDir, 'phases');
19
+
20
+ const spec = readTextIfExists(specPath);
21
+ const roadmap = readTextIfExists(roadmapPath);
22
+ const milestones = readTextIfExists(milestonesPath);
23
+ const brownfieldChange = readBrownfieldChangeState(planningDir);
24
+ const nonPhaseState = deriveNonPhaseState({
25
+ planningDir,
26
+ hasSpec: Boolean(spec.trim()),
27
+ hasRoadmap: Boolean(roadmap.trim()),
28
+ brownfieldChange,
29
+ });
30
+
31
+ const phases = parseActiveRoadmapPhases(roadmap);
32
+ const phaseArtifacts = collectPhaseArtifacts(phasesDir);
33
+ const enrichedPhases = phases.map((phase) => {
34
+ const matchingArtifacts = phaseArtifacts.filter((artifact) => artifact.phaseToken === phase.number);
35
+ const hasPlan = matchingArtifacts.some((artifact) => artifact.kind === 'plan');
36
+ const hasSummary = matchingArtifacts.some((artifact) => artifact.kind === 'summary');
37
+ return {
38
+ ...phase,
39
+ hasArtifacts: matchingArtifacts.length > 0,
40
+ hasPlan,
41
+ hasSummary,
42
+ artifacts: matchingArtifacts,
43
+ };
44
+ });
45
+
46
+ const incompletePlans = phaseArtifacts.filter((artifact) => {
47
+ if (artifact.kind !== 'plan') return false;
48
+ return !phaseArtifacts.some((candidate) =>
49
+ candidate.dir === artifact.dir &&
50
+ candidate.baseId === artifact.baseId &&
51
+ candidate.kind === 'summary'
52
+ );
53
+ });
54
+
55
+ const counts = {
56
+ total: enrichedPhases.length,
57
+ completed: enrichedPhases.filter((phase) => phase.status === 'done').length,
58
+ inProgress: enrichedPhases.filter((phase) => phase.status === 'in_progress').length,
59
+ notStarted: enrichedPhases.filter((phase) => phase.status === 'not_started').length,
60
+ };
61
+
62
+ const milestoneLedger = parseMilestoneLedger(milestones);
63
+ const currentMilestone = deriveCurrentMilestone({
64
+ roadmap,
65
+ planningDir,
66
+ milestoneLedger,
67
+ counts,
68
+ });
69
+
70
+ return {
71
+ paths: {
72
+ specPath,
73
+ roadmapPath,
74
+ milestonesPath,
75
+ phasesDir,
76
+ },
77
+ currentMilestone,
78
+ phases: enrichedPhases,
79
+ currentPhase: enrichedPhases.find((phase) => phase.status === 'in_progress') || null,
80
+ nextPhase: enrichedPhases.find((phase) => phase.status === 'not_started') || null,
81
+ counts,
82
+ phaseArtifacts,
83
+ incompletePlans,
84
+ brownfieldChange,
85
+ nonPhaseState,
86
+ requirementAlignment: evaluateRequirementAlignment(spec, enrichedPhases),
87
+ provenance: provenance
88
+ ? {
89
+ provided: true,
90
+ ...provenance,
91
+ }
92
+ : { provided: false },
93
+ };
94
+ }
95
+
96
+ function readTextIfExists(filePath) {
97
+ return existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
98
+ }
99
+
100
+ function normalizeContent(content) {
101
+ return String(content || '').replace(/\r\n/g, '\n');
102
+ }
103
+
104
+ function escapeRegExp(value) {
105
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
106
+ }
107
+
108
+ export function normalizePhaseToken(value) {
109
+ const raw = String(value || '').trim().toLowerCase();
110
+ const match = raw.match(/^(\d+(?:\.\d+)*)([a-z]?)$/i);
111
+ if (!match) return raw;
112
+
113
+ const numericSegments = match[1]
114
+ .split('.')
115
+ .map((segment) => String(parseInt(segment, 10)));
116
+ return `${numericSegments.join('.')}${match[2] || ''}`;
117
+ }
118
+
119
+ function normalizePhaseStatus(marker) {
120
+ const normalized = String(marker || '').trim().toLowerCase();
121
+ if (normalized === 'x') return 'done';
122
+ if (normalized === '-') return 'in_progress';
123
+ return 'not_started';
124
+ }
125
+
126
+ function parseActiveRoadmapPhases(roadmap) {
127
+ if (!roadmap) return [];
128
+
129
+ const phases = [];
130
+ let inDetails = false;
131
+
132
+ for (const line of normalizeContent(roadmap).split('\n')) {
133
+ if (line.includes('<details>') && !line.includes('</details>')) {
134
+ inDetails = true;
135
+ continue;
136
+ }
137
+ if (line.includes('</details>')) {
138
+ inDetails = false;
139
+ continue;
140
+ }
141
+ if (inDetails) continue;
142
+
143
+ const match = line.match(PHASE_LINE_RE);
144
+ if (!match) continue;
145
+
146
+ phases.push({
147
+ number: normalizePhaseToken(match[2]),
148
+ title: match[3].trim(),
149
+ status: normalizePhaseStatus(match[1]),
150
+ requirements: splitRequirementList(match[4]),
151
+ });
152
+ }
153
+
154
+ return phases;
155
+ }
156
+
157
+ export function readBrownfieldChangeState(planningDir) {
158
+ const dir = join(planningDir, BROWNFIELD_CHANGE_DIR);
159
+ const changePath = join(dir, 'CHANGE.md');
160
+ const handoffPath = join(dir, 'HANDOFF.md');
161
+
162
+ if (!existsSync(changePath)) {
163
+ return {
164
+ exists: false,
165
+ dir,
166
+ changePath,
167
+ handoffPath,
168
+ title: null,
169
+ changeId: null,
170
+ currentStatus: null,
171
+ currentIntegrationSurface: null,
172
+ currentOwnerRuntime: null,
173
+ nextAction: null,
174
+ declaredOwnedPaths: [],
175
+ handoff: null,
176
+ };
177
+ }
178
+
179
+ const changeArtifact = readMarkdownArtifact(changePath);
180
+ const handoffArtifact = existsSync(handoffPath) ? readMarkdownArtifact(handoffPath) : null;
181
+ const currentStatusSection = extractMarkdownSection(changeArtifact.body, 'Current Status');
182
+ const nextActionSection = extractMarkdownSection(changeArtifact.body, 'Next Action');
183
+ const sliceSection = extractMarkdownSection(changeArtifact.body, 'PR Slice Ownership');
184
+
185
+ return {
186
+ exists: true,
187
+ dir,
188
+ changePath,
189
+ handoffPath,
190
+ title: extractMarkdownHeading(changeArtifact.body),
191
+ changeId: changeArtifact.frontmatter.change || null,
192
+ currentStatus: changeArtifact.frontmatter.status || extractBulletLabel(currentStatusSection, 'Current posture'),
193
+ currentIntegrationSurface: extractBulletLabel(currentStatusSection, 'Current branch / integration surface'),
194
+ currentOwnerRuntime: extractBulletLabel(currentStatusSection, 'Current owner / runtime'),
195
+ nextAction: collapseMarkdownSection(nextActionSection),
196
+ declaredOwnedPaths: parseOwnedPathHints(sliceSection),
197
+ handoff: handoffArtifact
198
+ ? {
199
+ updated: handoffArtifact.frontmatter.updated || null,
200
+ activeConstraints: collapseMarkdownSection(extractMarkdownSection(handoffArtifact.body, 'Active Constraints')),
201
+ unresolvedUncertainty: collapseMarkdownSection(extractMarkdownSection(handoffArtifact.body, 'Unresolved Uncertainty')),
202
+ decisionPosture: collapseMarkdownSection(extractMarkdownSection(handoffArtifact.body, 'Decision Posture')),
203
+ antiRegression: collapseMarkdownSection(extractMarkdownSection(handoffArtifact.body, 'Anti-Regression')),
204
+ nextActionContext: collapseMarkdownSection(extractMarkdownSection(handoffArtifact.body, 'Next Action')),
205
+ }
206
+ : null,
207
+ };
208
+ }
209
+
210
+ export function deriveNonPhaseState({ planningDir, hasSpec, hasRoadmap, brownfieldChange } = {}) {
211
+ if (brownfieldChange?.exists) return 'active_brownfield_change';
212
+ if (hasRoadmap) return null;
213
+ if (hasSpec) return 'between_milestones';
214
+ if (hasSubstantiveCodebaseMaps(planningDir)) return 'codebase_only';
215
+ if (hasQuickLaneArtifacts(planningDir)) return 'quick_lane';
216
+ return null;
217
+ }
218
+
219
+ function splitRequirementList(rawRequirements = '') {
220
+ return String(rawRequirements)
221
+ .split(',')
222
+ .map((entry) => entry.trim())
223
+ .filter(Boolean);
224
+ }
225
+
226
+ function collectPhaseArtifacts(phasesDir) {
227
+ if (!existsSync(phasesDir)) return [];
228
+
229
+ const artifacts = [];
230
+ for (const entry of readdirSync(phasesDir, { withFileTypes: true })) {
231
+ const entryPath = join(phasesDir, entry.name);
232
+ if (entry.isFile()) {
233
+ const artifact = classifyPhaseArtifact('', entry.name);
234
+ if (artifact) artifacts.push(artifact);
235
+ continue;
236
+ }
237
+
238
+ if (!entry.isDirectory()) continue;
239
+
240
+ for (const child of readdirSync(entryPath, { withFileTypes: true })) {
241
+ if (!child.isFile()) continue;
242
+ const artifact = classifyPhaseArtifact(entry.name, child.name);
243
+ if (artifact) artifacts.push(artifact);
244
+ }
245
+ }
246
+
247
+ return artifacts;
248
+ }
249
+
250
+ function classifyPhaseArtifact(dir, name) {
251
+ const baseIdMatch = name.match(/^(\d+(?:\.\d+)*[a-z]?(?:-\d+)?)/i);
252
+ const phaseTokenMatch = (dir ? dir.match(/^(\d+(?:\.\d+)*[a-z]?)-/i) : null)
253
+ || name.match(/^(\d+(?:\.\d+)*[a-z]?)/i);
254
+
255
+ if (!baseIdMatch || !phaseTokenMatch) return null;
256
+
257
+ let kind = 'other';
258
+ if (name.includes('PLAN')) kind = 'plan';
259
+ else if (name.includes('SUMMARY')) kind = 'summary';
260
+ else if (name.includes('VERIFICATION')) kind = 'verification';
261
+ else if (name.includes('APPROACH')) kind = 'approach';
262
+
263
+ return {
264
+ dir,
265
+ name,
266
+ displayPath: dir ? `${dir}/${name}` : name,
267
+ baseId: baseIdMatch[1],
268
+ phaseToken: normalizePhaseToken(phaseTokenMatch[1]),
269
+ kind,
270
+ };
271
+ }
272
+
273
+ function parseMilestoneLedger(content) {
274
+ if (!content) return [];
275
+
276
+ const entries = [];
277
+ let current = null;
278
+
279
+ for (const line of normalizeContent(content).split('\n')) {
280
+ const headingMatch = line.match(MILESTONE_LEDGER_HEADING_RE);
281
+ if (headingMatch) {
282
+ if (current) entries.push(current);
283
+ current = {
284
+ version: headingMatch[1],
285
+ title: headingMatch[2].trim(),
286
+ lines: [],
287
+ };
288
+ continue;
289
+ }
290
+
291
+ if (current) {
292
+ current.lines.push(line);
293
+ }
294
+ }
295
+
296
+ if (current) entries.push(current);
297
+
298
+ return entries.map((entry) => {
299
+ const section = entry.lines.join('\n');
300
+ const statusMatch = section.match(/^- Status:\s*(.+)$/im);
301
+ const shipped = /(^- Status:\s*shipped$)|(^- Shipped:\s+)/im.test(section) || /^✅/.test(entry.title);
302
+ return {
303
+ version: entry.version,
304
+ title: entry.title.replace(/^✅\s*/, ''),
305
+ status: statusMatch ? statusMatch[1].trim().toLowerCase() : shipped ? 'shipped' : 'unknown',
306
+ shipped,
307
+ };
308
+ });
309
+ }
310
+
311
+ function deriveCurrentMilestone({ roadmap, planningDir, milestoneLedger, counts }) {
312
+ const normalizedRoadmap = normalizeContent(roadmap);
313
+ const headingMatch = normalizedRoadmap.match(ACTIVE_MILESTONE_HEADING_RE);
314
+ if (!headingMatch) {
315
+ return {
316
+ version: null,
317
+ title: null,
318
+ shippedInLedger: false,
319
+ hasMatchingAudit: false,
320
+ archiveState: 'unknown',
321
+ counts,
322
+ };
323
+ }
324
+
325
+ const version = headingMatch[1];
326
+ const title = headingMatch[2].trim();
327
+ const ledgerEntry = milestoneLedger.find((entry) => entry.version === version) || null;
328
+ const auditPath = join(planningDir, `${version}-MILESTONE-AUDIT.md`);
329
+ const hasMatchingAudit = existsSync(auditPath);
330
+ const shippedInLedger = Boolean(ledgerEntry?.shipped);
331
+
332
+ return {
333
+ version,
334
+ title,
335
+ shippedInLedger,
336
+ hasMatchingAudit,
337
+ archiveState: shippedInLedger && hasMatchingAudit ? 'archived' : 'active',
338
+ counts,
339
+ };
340
+ }
341
+
342
+ function evaluateRequirementAlignment(spec, phases) {
343
+ const checkedRequirements = new Set(
344
+ [...normalizeContent(spec).matchAll(/- \[x\] \*\*\[([A-Z0-9-]+)]\*\*/g)].map((result) => result[1])
345
+ );
346
+
347
+ const roadmapRequirements = new Map();
348
+ for (const phase of phases) {
349
+ for (const requirementId of phase.requirements) {
350
+ roadmapRequirements.set(requirementId, phase.status === 'done');
351
+ }
352
+ }
353
+
354
+ const mismatches = [];
355
+ for (const [requirementId, roadmapStatus] of roadmapRequirements.entries()) {
356
+ const specChecked = checkedRequirements.has(requirementId);
357
+ if (roadmapStatus && !specChecked) {
358
+ mismatches.push(`${requirementId} phase complete but SPEC unchecked`);
359
+ }
360
+ if (!roadmapStatus && specChecked) {
361
+ mismatches.push(`${requirementId} SPEC checked but phase incomplete`);
362
+ }
363
+ }
364
+
365
+ return {
366
+ checkedRequirements,
367
+ roadmapRequirements,
368
+ mismatches,
369
+ };
370
+ }
371
+
372
+ function readMarkdownArtifact(filePath) {
373
+ const raw = readTextIfExists(filePath);
374
+ const normalized = normalizeContent(raw);
375
+ const match = normalized.match(/^---\n([\s\S]*?)\n---\n?/);
376
+ if (!match) {
377
+ return {
378
+ raw: normalized,
379
+ frontmatter: {},
380
+ body: normalized,
381
+ };
382
+ }
383
+
384
+ return {
385
+ raw: normalized,
386
+ frontmatter: parseFrontmatter(match[1]),
387
+ body: normalized.slice(match[0].length),
388
+ };
389
+ }
390
+
391
+ function parseFrontmatter(content) {
392
+ const data = {};
393
+ for (const line of normalizeContent(content).split('\n')) {
394
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.+)$/);
395
+ if (!match) continue;
396
+ data[match[1]] = match[2].trim();
397
+ }
398
+ return data;
399
+ }
400
+
401
+ function extractMarkdownHeading(content) {
402
+ return normalizeContent(content).match(/^#\s+(.+)$/m)?.[1]?.trim() || null;
403
+ }
404
+
405
+ function extractMarkdownSection(content, heading) {
406
+ const normalized = normalizeContent(content);
407
+ const headingMatch = new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`, 'm').exec(normalized);
408
+ if (!headingMatch) return '';
409
+
410
+ const start = headingMatch.index + headingMatch[0].length;
411
+ const remainder = normalized.slice(start).replace(/^\n/, '');
412
+ const nextHeading = /\n##\s+/.exec(remainder);
413
+ const end = nextHeading ? nextHeading.index : remainder.length;
414
+ return remainder.slice(0, end).trim();
415
+ }
416
+
417
+ function extractBulletLabel(section, label) {
418
+ const match = normalizeContent(section).match(
419
+ new RegExp(`^[-*]\\s+${escapeRegExp(label)}\\s*:\\s*(.+)$`, 'im')
420
+ );
421
+ return match ? match[1].trim() : null;
422
+ }
423
+
424
+ function collapseMarkdownSection(section) {
425
+ return normalizeContent(section)
426
+ .split('\n')
427
+ .map((line) => line.replace(/^[-*]\s+/, '').trim())
428
+ .filter(Boolean)
429
+ .join(' ');
430
+ }
431
+
432
+ function parseOwnedPathHints(section) {
433
+ const lines = normalizeContent(section)
434
+ .split('\n')
435
+ .map((line) => line.trim())
436
+ .filter((line) => line.startsWith('|'));
437
+ const paths = [];
438
+
439
+ for (const line of lines.slice(2)) {
440
+ const columns = line.split('|').map((column) => column.trim());
441
+ const owned = columns[3];
442
+ if (!owned) continue;
443
+
444
+ for (const candidate of owned.split(',')) {
445
+ const normalized = normalizeOwnedPathHint(candidate);
446
+ if (normalized) paths.push(normalized);
447
+ }
448
+ }
449
+
450
+ return [...new Set(paths)];
451
+ }
452
+
453
+ function normalizeOwnedPathHint(value) {
454
+ const normalized = String(value || '')
455
+ .replace(/[`[\]]/g, '')
456
+ .trim()
457
+ .replace(/\\/g, '/');
458
+ if (!normalized) return null;
459
+ if (/^(disjoint write set|owned files \/ modules|what this slice does|planned)$/i.test(normalized)) {
460
+ return null;
461
+ }
462
+ if (!/[/.\\*]/.test(normalized)) return null;
463
+ return normalized;
464
+ }
465
+
466
+ function hasSubstantiveCodebaseMaps(planningDir) {
467
+ const dir = join(planningDir, 'codebase');
468
+ if (!existsSync(dir)) return false;
469
+ return readdirSync(dir).some((entry) => entry.toLowerCase().endsWith('.md'));
470
+ }
471
+
472
+ function hasQuickLaneArtifacts(planningDir) {
473
+ const dir = join(planningDir, 'quick');
474
+ if (!existsSync(dir)) return false;
475
+ return readdirSync(dir).length > 0;
476
+ }
@@ -39,9 +39,10 @@ export function hashDirectory(dir, baseDir = dir) {
39
39
  /**
40
40
  * Build a full manifest snapshot from installed project files.
41
41
  */
42
- export function buildManifest({ planningDir, frameworkVersion }) {
43
- const templatesDir = join(planningDir, 'templates');
44
- const rolesDir = join(templatesDir, 'roles');
42
+ export function buildManifest({ planningDir, frameworkVersion }) {
43
+ const templatesDir = join(planningDir, 'templates');
44
+ const rolesDir = join(templatesDir, 'roles');
45
+ const runtimeHelpersDir = join(planningDir, 'bin');
45
46
 
46
47
  // Template subcategories
47
48
  const delegatesHashes = hashDirectory(join(templatesDir, 'delegates'), join(templatesDir, 'delegates'));
@@ -59,28 +60,31 @@ export function buildManifest({ planningDir, frameworkVersion }) {
59
60
  }
60
61
  }
61
62
 
62
- // Role contracts
63
- const rolesHashes = {};
64
- if (existsSync(rolesDir)) {
65
- for (const entry of readdirSync(rolesDir)) {
66
- if (entry.endsWith('.md')) {
67
- rolesHashes[entry] = fileHash(join(rolesDir, entry));
68
- }
69
- }
70
- }
71
-
72
- return {
73
- frameworkVersion,
74
- generatedAt: new Date().toISOString(),
63
+ // Role contracts
64
+ const rolesHashes = {};
65
+ if (existsSync(rolesDir)) {
66
+ for (const entry of readdirSync(rolesDir)) {
67
+ if (entry.endsWith('.md')) {
68
+ rolesHashes[entry] = fileHash(join(rolesDir, entry));
69
+ }
70
+ }
71
+ }
72
+
73
+ const runtimeHelpersHashes = hashDirectory(runtimeHelpersDir, planningDir);
74
+
75
+ return {
76
+ frameworkVersion,
77
+ generatedAt: new Date().toISOString(),
75
78
  templates: {
76
79
  delegates: delegatesHashes,
77
80
  research: researchHashes,
78
81
  codebase: codebaseHashes,
79
82
  root: rootHashes,
80
- },
81
- roles: rolesHashes,
82
- };
83
- }
83
+ },
84
+ roles: rolesHashes,
85
+ runtimeHelpers: runtimeHelpersHashes,
86
+ };
87
+ }
84
88
 
85
89
  /**
86
90
  * Read existing manifest from planningDir, or return null if missing/corrupt.
package/bin/lib/phase.mjs CHANGED
@@ -6,6 +6,7 @@
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
7
7
  import { join, basename } from 'path';
8
8
  import { output } from './cli-utils.mjs';
9
+ import { writeFingerprint } from './session-fingerprint.mjs';
9
10
 
10
11
  const PHASE_STATUS_MARKERS = {
11
12
  not_started: '[ ]',
@@ -27,7 +28,60 @@ const ROADMAP_PHASE_STATUS_RE = new RegExp(
27
28
 
28
29
  function findFiles(dir, prefix) {
29
30
  if (!existsSync(dir)) return [];
30
- return readdirSync(dir).filter((f) => f.startsWith(prefix) || f.startsWith(prefix.replace(/^0+/, '')));
31
+
32
+ const phaseArtifactPrefix = String(prefix).match(/^(\d+(?:\.\d+)*[a-z]?)-(PLAN|SUMMARY)$/i);
33
+ if (!phaseArtifactPrefix) {
34
+ return readdirSync(dir).filter((f) => f.startsWith(prefix) || f.startsWith(prefix.replace(/^0+/, '')));
35
+ }
36
+
37
+ const targetPhase = normalizePhaseToken(phaseArtifactPrefix[1]);
38
+ const targetKind = phaseArtifactPrefix[2].toUpperCase();
39
+
40
+ return listPhaseArtifacts(dir)
41
+ .filter((artifact) => artifact.phaseToken === targetPhase && artifact.kind === targetKind)
42
+ .map((artifact) => artifact.displayPath);
43
+ }
44
+
45
+ function listPhaseArtifacts(dir) {
46
+ if (!existsSync(dir)) return [];
47
+
48
+ const artifacts = [];
49
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
50
+ if (entry.isFile()) {
51
+ const artifact = classifyPhaseArtifact('', entry.name);
52
+ if (artifact) artifacts.push(artifact);
53
+ continue;
54
+ }
55
+
56
+ if (!entry.isDirectory()) continue;
57
+
58
+ const entryPath = join(dir, entry.name);
59
+ for (const child of readdirSync(entryPath, { withFileTypes: true })) {
60
+ if (!child.isFile()) continue;
61
+ const artifact = classifyPhaseArtifact(entry.name, child.name);
62
+ if (artifact) artifacts.push(artifact);
63
+ }
64
+ }
65
+
66
+ return artifacts;
67
+ }
68
+
69
+ function classifyPhaseArtifact(dir, name) {
70
+ const dirMatch = dir ? dir.match(/^(\d+(?:\.\d+)*[a-z]?)-/i) : null;
71
+ const nameMatch = name.match(/^(\d+(?:\.\d+)*[a-z]?)/i);
72
+ const phaseToken = normalizePhaseToken((dirMatch || nameMatch)?.[1] || '');
73
+
74
+ let kind = 'OTHER';
75
+ if (name.includes('PLAN')) kind = 'PLAN';
76
+ else if (name.includes('SUMMARY')) kind = 'SUMMARY';
77
+
78
+ return {
79
+ dir,
80
+ name,
81
+ displayPath: dir ? `${dir}/${name}` : name,
82
+ phaseToken,
83
+ kind,
84
+ };
31
85
  }
32
86
 
33
87
  function padPhase(n) {
@@ -97,23 +151,23 @@ export function updateRoadmapPhaseStatus(roadmap, phaseNumber, status) {
97
151
  return updated;
98
152
  }
99
153
 
100
- export function cmdPhaseStatus(...args) {
101
- const cwd = process.cwd();
102
- const planningDir = join(cwd, '.planning');
103
- const roadmapPath = join(planningDir, 'ROADMAP.md');
104
- const [phaseNumber, status] = args;
105
-
106
- if (!phaseNumber || !status) {
107
- console.error('Usage: gsdd phase-status <phase-number> <not_started|todo|in_progress|done>');
108
- process.exitCode = 1;
109
- return;
110
- }
111
-
112
- if (!existsSync(roadmapPath)) {
113
- console.error('No ROADMAP.md found. Run the new-project workflow first.');
114
- process.exitCode = 1;
115
- return;
116
- }
154
+ export function cmdPhaseStatus(...args) {
155
+ const cwd = process.cwd();
156
+ const planningDir = join(cwd, '.planning');
157
+ const roadmapPath = join(planningDir, 'ROADMAP.md');
158
+ const [phaseNumber, status] = args;
159
+
160
+ if (!phaseNumber || !status) {
161
+ console.error('Usage: gsdd phase-status <phase-number> <not_started|todo|in_progress|done>');
162
+ process.exitCode = 1;
163
+ return;
164
+ }
165
+
166
+ if (!existsSync(roadmapPath)) {
167
+ console.error('No ROADMAP.md found. Run the new-project workflow first.');
168
+ process.exitCode = 1;
169
+ return;
170
+ }
117
171
 
118
172
  try {
119
173
  const roadmap = readFileSync(roadmapPath, 'utf-8');
@@ -121,13 +175,14 @@ export function cmdPhaseStatus(...args) {
121
175
  const changed = updated !== roadmap;
122
176
  if (changed) {
123
177
  writeFileSync(roadmapPath, updated);
178
+ try { writeFingerprint(planningDir); } catch { /* best-effort */ }
124
179
  }
125
180
  output({ phase: phaseNumber, status, roadmap: '.planning/ROADMAP.md', changed });
126
- } catch (error) {
127
- console.error(error.message);
128
- process.exitCode = 1;
129
- }
130
- }
181
+ } catch (error) {
182
+ console.error(error.message);
183
+ process.exitCode = 1;
184
+ }
185
+ }
131
186
 
132
187
  export function cmdFindPhase(...args) {
133
188
  const cwd = process.cwd();
@@ -163,9 +218,9 @@ export function cmdFindPhase(...args) {
163
218
  return;
164
219
  }
165
220
 
166
- const allFiles = existsSync(phasesDir) ? readdirSync(phasesDir) : [];
167
- const plans = allFiles.filter((f) => f.includes('PLAN'));
168
- const summaries = allFiles.filter((f) => f.includes('SUMMARY'));
221
+ const allArtifacts = listPhaseArtifacts(phasesDir);
222
+ const plans = allArtifacts.filter((artifact) => artifact.kind === 'PLAN');
223
+ const summaries = allArtifacts.filter((artifact) => artifact.kind === 'SUMMARY');
169
224
 
170
225
  const roadmap = readFileSync(roadmapPath, 'utf-8');
171
226
  const phases = parsePhaseStatuses(roadmap);