gsdd-cli 0.18.5 → 0.19.0

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 (100) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +609 -608
  3. package/agents/DISTILLATION.md +421 -421
  4. package/agents/README.md +62 -62
  5. package/agents/approach-explorer.md +361 -361
  6. package/agents/debugger.md +82 -82
  7. package/agents/executor.md +394 -394
  8. package/agents/integration-checker.md +318 -318
  9. package/agents/mapper.md +103 -103
  10. package/agents/planner.md +313 -313
  11. package/agents/researcher.md +84 -84
  12. package/agents/roadmapper.md +296 -296
  13. package/agents/synthesizer.md +236 -236
  14. package/agents/verifier.md +337 -337
  15. package/bin/adapters/agents.mjs +34 -34
  16. package/bin/adapters/claude.mjs +191 -191
  17. package/bin/adapters/codex.mjs +85 -85
  18. package/bin/adapters/index.mjs +20 -20
  19. package/bin/adapters/opencode.mjs +278 -278
  20. package/bin/gsdd.mjs +123 -116
  21. package/bin/lib/cli-utils.mjs +28 -28
  22. package/bin/lib/evidence-contract.mjs +112 -112
  23. package/bin/lib/file-ops.mjs +186 -144
  24. package/bin/lib/health-truth.mjs +181 -178
  25. package/bin/lib/health.mjs +246 -226
  26. package/bin/lib/init-flow.mjs +247 -231
  27. package/bin/lib/init-prompts.mjs +248 -247
  28. package/bin/lib/init-runtime.mjs +191 -190
  29. package/bin/lib/init.mjs +17 -17
  30. package/bin/lib/lifecycle-preflight.mjs +347 -325
  31. package/bin/lib/lifecycle-state.mjs +351 -267
  32. package/bin/lib/manifest.mjs +116 -114
  33. package/bin/lib/models.mjs +411 -411
  34. package/bin/lib/phase.mjs +360 -358
  35. package/bin/lib/plan-constants.mjs +30 -30
  36. package/bin/lib/provenance.mjs +109 -106
  37. package/bin/lib/rendering.mjs +115 -83
  38. package/bin/lib/runtime-freshness.mjs +214 -214
  39. package/bin/lib/templates.mjs +225 -224
  40. package/bin/lib/workspace-root.mjs +2 -1
  41. package/distilled/DESIGN.md +2333 -2323
  42. package/distilled/EVIDENCE-INDEX.md +394 -392
  43. package/distilled/README.md +196 -193
  44. package/distilled/SKILL.md +86 -85
  45. package/distilled/templates/agents.block.md +21 -21
  46. package/distilled/templates/agents.md +6 -6
  47. package/distilled/templates/approach.md +232 -232
  48. package/distilled/templates/auth-matrix.md +78 -78
  49. package/distilled/templates/brownfield-change/CHANGE.md +99 -0
  50. package/distilled/templates/brownfield-change/HANDOFF.md +38 -0
  51. package/distilled/templates/brownfield-change/VERIFICATION.md +56 -0
  52. package/distilled/templates/codebase/architecture.md +110 -110
  53. package/distilled/templates/codebase/concerns.md +95 -95
  54. package/distilled/templates/codebase/conventions.md +193 -193
  55. package/distilled/templates/codebase/stack.md +96 -96
  56. package/distilled/templates/delegates/approach-explorer.md +25 -25
  57. package/distilled/templates/delegates/mapper-arch.md +26 -26
  58. package/distilled/templates/delegates/mapper-concerns.md +27 -27
  59. package/distilled/templates/delegates/mapper-quality.md +28 -28
  60. package/distilled/templates/delegates/mapper-tech.md +25 -25
  61. package/distilled/templates/delegates/plan-checker.md +68 -68
  62. package/distilled/templates/delegates/researcher-architecture.md +30 -30
  63. package/distilled/templates/delegates/researcher-features.md +30 -30
  64. package/distilled/templates/delegates/researcher-pitfalls.md +30 -30
  65. package/distilled/templates/delegates/researcher-stack.md +30 -30
  66. package/distilled/templates/delegates/researcher-synthesizer.md +31 -31
  67. package/distilled/templates/research/architecture.md +57 -57
  68. package/distilled/templates/research/features.md +23 -23
  69. package/distilled/templates/research/pitfalls.md +46 -46
  70. package/distilled/templates/research/stack.md +45 -45
  71. package/distilled/templates/research/summary.md +67 -67
  72. package/distilled/templates/roadmap.md +74 -62
  73. package/distilled/templates/spec.md +110 -110
  74. package/distilled/workflows/audit-milestone.md +275 -271
  75. package/distilled/workflows/complete-milestone.md +336 -332
  76. package/distilled/workflows/execute.md +454 -449
  77. package/distilled/workflows/map-codebase.md +253 -253
  78. package/distilled/workflows/new-milestone.md +242 -238
  79. package/distilled/workflows/new-project.md +398 -398
  80. package/distilled/workflows/pause.md +160 -156
  81. package/distilled/workflows/plan-milestone-gaps.md +183 -183
  82. package/distilled/workflows/plan.md +451 -447
  83. package/distilled/workflows/progress.md +227 -223
  84. package/distilled/workflows/quick.md +351 -347
  85. package/distilled/workflows/resume.md +220 -212
  86. package/distilled/workflows/verify-work.md +260 -260
  87. package/distilled/workflows/verify.md +431 -429
  88. package/docs/BROWNFIELD-PROOF.md +95 -95
  89. package/docs/RUNTIME-SUPPORT.md +80 -69
  90. package/docs/USER-GUIDE.md +394 -386
  91. package/docs/VERIFICATION-DISCIPLINE.md +59 -59
  92. package/docs/claude/context-monitor.md +98 -98
  93. package/docs/proof/consumer-node-cli/README.md +37 -37
  94. package/docs/proof/consumer-node-cli/ROADMAP.md +14 -14
  95. package/docs/proof/consumer-node-cli/SPEC.md +17 -17
  96. package/docs/proof/consumer-node-cli/brief.md +9 -9
  97. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -34
  98. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -10
  99. package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -30
  100. package/package.json +62 -61
package/bin/lib/phase.mjs CHANGED
@@ -1,157 +1,243 @@
1
- // phase.mjs — Phase discovery, verification, and scaffolding
2
- //
3
- // IMPORTANT: No module-scope process.cwd() — ESM caching means sub-modules
4
- // evaluate once, so CWD must be computed inside function bodies.
5
-
1
+ // phase.mjs — Phase discovery, verification, and scaffolding
2
+ //
3
+ // IMPORTANT: No module-scope process.cwd() — ESM caching means sub-modules
4
+ // evaluate once, so CWD must be computed inside function bodies.
5
+
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
9
  import { writeFingerprint } from './session-fingerprint.mjs';
10
10
  import { resolveWorkspaceContext } from './workspace-root.mjs';
11
-
12
- const PHASE_STATUS_MARKERS = {
13
- not_started: '[ ]',
14
- todo: '[ ]',
15
- in_progress: '[-]',
16
- done: '[x]',
17
- };
18
-
19
- const PHASE_MARKER_RE = '(\\[[ x]\\]|\\[-\\]|⬜|ðŸ"„|✅|⬜|🔄|✅)';
20
- const PHASE_TOKEN_RE = '(\\d+(?:\\.\\d+)*[a-z]?)';
21
- const PHASE_LINE_RE = new RegExp(
22
- `^[-*]\\s*${PHASE_MARKER_RE}\\s*\\*\\*Phase\\s+${PHASE_TOKEN_RE}:\\s*(.+?)\\*\\*`,
23
- 'i'
24
- );
25
- const ROADMAP_PHASE_STATUS_RE = new RegExp(
26
- `^(\\s*[-*]\\s*)${PHASE_MARKER_RE}(\\s*\\*\\*Phase\\s+${PHASE_TOKEN_RE}:.*)$`,
27
- 'i'
28
- );
29
-
30
- function findFiles(dir, prefix) {
31
- if (!existsSync(dir)) return [];
32
-
33
- const phaseArtifactPrefix = String(prefix).match(/^(\d+(?:\.\d+)*[a-z]?)-(PLAN|SUMMARY)$/i);
34
- if (!phaseArtifactPrefix) {
35
- return readdirSync(dir).filter((f) => f.startsWith(prefix) || f.startsWith(prefix.replace(/^0+/, '')));
36
- }
37
-
38
- const targetPhase = normalizePhaseToken(phaseArtifactPrefix[1]);
39
- const targetKind = phaseArtifactPrefix[2].toUpperCase();
40
-
41
- return listPhaseArtifacts(dir)
42
- .filter((artifact) => artifact.phaseToken === targetPhase && artifact.kind === targetKind)
43
- .map((artifact) => artifact.displayPath);
44
- }
45
-
46
- function listPhaseArtifacts(dir) {
47
- if (!existsSync(dir)) return [];
48
-
49
- const artifacts = [];
50
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
51
- if (entry.isFile()) {
52
- const artifact = classifyPhaseArtifact('', entry.name);
53
- if (artifact) artifacts.push(artifact);
54
- continue;
55
- }
56
-
57
- if (!entry.isDirectory()) continue;
58
-
59
- const entryPath = join(dir, entry.name);
60
- for (const child of readdirSync(entryPath, { withFileTypes: true })) {
61
- if (!child.isFile()) continue;
62
- const artifact = classifyPhaseArtifact(entry.name, child.name);
63
- if (artifact) artifacts.push(artifact);
64
- }
65
- }
66
-
67
- return artifacts;
68
- }
69
-
70
- function classifyPhaseArtifact(dir, name) {
71
- const dirMatch = dir ? dir.match(/^(\d+(?:\.\d+)*[a-z]?)-/i) : null;
72
- const nameMatch = name.match(/^(\d+(?:\.\d+)*[a-z]?)/i);
73
- const phaseToken = normalizePhaseToken((dirMatch || nameMatch)?.[1] || '');
74
-
75
- let kind = 'OTHER';
76
- if (name.includes('PLAN')) kind = 'PLAN';
77
- else if (name.includes('SUMMARY')) kind = 'SUMMARY';
78
-
79
- return {
80
- dir,
81
- name,
82
- displayPath: dir ? `${dir}/${name}` : name,
83
- phaseToken,
84
- kind,
85
- };
86
- }
87
-
88
- function padPhase(n) {
89
- return String(n).padStart(2, '0');
90
- }
91
-
92
- function parsePhaseStatuses(roadmap) {
93
- const phases = [];
94
- const lines = roadmap.split('\n');
95
- for (const line of lines) {
96
- const match = line.match(PHASE_LINE_RE);
97
- if (match) {
98
- const rawStatus = match[1].toLowerCase();
99
- let status = 'not_started';
100
- if (rawStatus === '[x]' || rawStatus === '✅' || rawStatus === '✅') status = 'done';
101
- else if (rawStatus === '[-]') status = 'in_progress';
102
- else if (rawStatus === 'ðÿ"„' || rawStatus === '🔄') status = 'in_progress';
103
- phases.push({
104
- number: match[2],
105
- name: match[3].replace(/\*\*/g, '').split('-')[0].trim(),
106
- status,
107
- });
108
- }
109
- }
110
- return phases;
111
- }
112
-
113
- function normalizePhaseToken(value) {
114
- const raw = String(value).trim().toLowerCase();
115
- const match = raw.match(/^(\d+(?:\.\d+)*)([a-z]?)$/i);
116
- if (!match) return raw;
117
-
118
- const numericSegments = match[1]
119
- .split('.')
120
- .map((segment) => String(parseInt(segment, 10)));
121
- return `${numericSegments.join('.')}${match[2] || ''}`;
122
- }
123
-
124
- export function updateRoadmapPhaseStatus(roadmap, phaseNumber, status) {
125
- const marker = PHASE_STATUS_MARKERS[status];
126
- if (!marker) {
127
- throw new Error(`Unsupported phase status: ${status}`);
128
- }
129
-
130
- const normalizedTarget = normalizePhaseToken(phaseNumber);
131
- let matchCount = 0;
132
-
133
- const updated = roadmap
134
- .split('\n')
135
- .map((line) => {
136
- const match = line.match(ROADMAP_PHASE_STATUS_RE);
137
- if (!match) return line;
138
- if (normalizePhaseToken(match[4]) !== normalizedTarget) return line;
139
- matchCount += 1;
140
- return `${match[1]}${marker}${match[3]}`;
141
- })
142
- .join('\n');
143
-
144
- if (matchCount === 0) {
145
- throw new Error(`Phase ${phaseNumber} was not found in ROADMAP.md`);
146
- }
147
-
148
- if (matchCount > 1) {
149
- throw new Error(`Phase ${phaseNumber} matched multiple ROADMAP.md entries`);
150
- }
151
-
152
- return updated;
153
- }
154
-
11
+
12
+ const PHASE_STATUS_MARKERS = {
13
+ not_started: '[ ]',
14
+ todo: '[ ]',
15
+ in_progress: '[-]',
16
+ done: '[x]',
17
+ };
18
+
19
+ const PHASE_MARKER_RE = '(\\[[ x]\\]|\\[-\\]|⬜|ðŸ"„|✅|⬜|🔄|✅)';
20
+ const PHASE_TOKEN_RE = '(\\d+(?:\\.\\d+)*[a-z]?)';
21
+ const PHASE_LINE_RE = new RegExp(
22
+ `^[-*]\\s*${PHASE_MARKER_RE}\\s*\\*\\*Phase\\s+${PHASE_TOKEN_RE}:\\s*(.+?)\\*\\*`,
23
+ 'i'
24
+ );
25
+ const ROADMAP_PHASE_STATUS_RE = new RegExp(
26
+ `^(\\s*[-*]\\s*)${PHASE_MARKER_RE}(\\s*\\*\\*Phase\\s+${PHASE_TOKEN_RE}:.*)$`,
27
+ 'i'
28
+ );
29
+ const PHASE_DETAIL_HEADING_RE = new RegExp(`^#{3,}\\s+Phase\\s+${PHASE_TOKEN_RE}(?::|\\b)`, 'i');
30
+ const PHASE_DETAIL_STATUS_RE = new RegExp(`^(\\s*\\*\\*Status\\*\\*:\\s*)${PHASE_MARKER_RE}(.*)$`, 'i');
31
+ const DETAILS_OPEN_RE = /<details\b/i;
32
+ const DETAILS_CLOSE_RE = /<\/details>/i;
33
+
34
+ function findFiles(dir, prefix) {
35
+ if (!existsSync(dir)) return [];
36
+
37
+ const phaseArtifactPrefix = String(prefix).match(/^(\d+(?:\.\d+)*[a-z]?)-(PLAN|SUMMARY)$/i);
38
+ if (!phaseArtifactPrefix) {
39
+ return readdirSync(dir).filter((f) => f.startsWith(prefix) || f.startsWith(prefix.replace(/^0+/, '')));
40
+ }
41
+
42
+ const targetPhase = normalizePhaseToken(phaseArtifactPrefix[1]);
43
+ const targetKind = phaseArtifactPrefix[2].toUpperCase();
44
+
45
+ return listPhaseArtifacts(dir)
46
+ .filter((artifact) => artifact.phaseToken === targetPhase && artifact.kind === targetKind)
47
+ .map((artifact) => artifact.displayPath);
48
+ }
49
+
50
+ function listPhaseArtifacts(dir) {
51
+ if (!existsSync(dir)) return [];
52
+
53
+ const artifacts = [];
54
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
55
+ if (entry.isFile()) {
56
+ const artifact = classifyPhaseArtifact('', entry.name);
57
+ if (artifact) artifacts.push(artifact);
58
+ continue;
59
+ }
60
+
61
+ if (!entry.isDirectory()) continue;
62
+
63
+ const entryPath = join(dir, entry.name);
64
+ for (const child of readdirSync(entryPath, { withFileTypes: true })) {
65
+ if (!child.isFile()) continue;
66
+ const artifact = classifyPhaseArtifact(entry.name, child.name);
67
+ if (artifact) artifacts.push(artifact);
68
+ }
69
+ }
70
+
71
+ return artifacts;
72
+ }
73
+
74
+ function classifyPhaseArtifact(dir, name) {
75
+ const dirMatch = dir ? dir.match(/^(\d+(?:\.\d+)*[a-z]?)-/i) : null;
76
+ const nameMatch = name.match(/^(\d+(?:\.\d+)*[a-z]?)/i);
77
+ const phaseToken = normalizePhaseToken((dirMatch || nameMatch)?.[1] || '');
78
+
79
+ let kind = 'OTHER';
80
+ if (name.includes('PLAN')) kind = 'PLAN';
81
+ else if (name.includes('SUMMARY')) kind = 'SUMMARY';
82
+
83
+ return {
84
+ dir,
85
+ name,
86
+ displayPath: dir ? `${dir}/${name}` : name,
87
+ phaseToken,
88
+ kind,
89
+ };
90
+ }
91
+
92
+ function padPhase(n) {
93
+ return String(n).padStart(2, '0');
94
+ }
95
+
96
+ function parsePhaseStatuses(roadmap) {
97
+ const phases = [];
98
+ const lines = roadmap.split('\n');
99
+ for (const line of lines) {
100
+ const match = line.match(PHASE_LINE_RE);
101
+ if (match) {
102
+ const rawStatus = match[1].toLowerCase();
103
+ let status = 'not_started';
104
+ if (rawStatus === '[x]' || rawStatus === '✅' || rawStatus === '✅') status = 'done';
105
+ else if (rawStatus === '[-]') status = 'in_progress';
106
+ else if (rawStatus === 'ðÿ"„' || rawStatus === '🔄') status = 'in_progress';
107
+ phases.push({
108
+ number: match[2],
109
+ name: match[3].replace(/\*\*/g, '').split('-')[0].trim(),
110
+ status,
111
+ });
112
+ }
113
+ }
114
+ return phases;
115
+ }
116
+
117
+ function normalizePhaseToken(value) {
118
+ const raw = String(value).trim().toLowerCase();
119
+ const match = raw.match(/^(\d+(?:\.\d+)*)([a-z]?)$/i);
120
+ if (!match) return raw;
121
+
122
+ const numericSegments = match[1]
123
+ .split('.')
124
+ .map((segment) => String(parseInt(segment, 10)));
125
+ return `${numericSegments.join('.')}${match[2] || ''}`;
126
+ }
127
+
128
+ function extractPlanFileArtifacts(planContent, workspaceRoot) {
129
+ const artifacts = [];
130
+ const seen = new Set();
131
+
132
+ for (const line of planContent.split('\n')) {
133
+ const moveMatch = line.match(/^\s*-\s*(RENAME|MOVE):\s*(.+?)\s*->\s*(.+?)\s*$/i);
134
+ if (moveMatch) {
135
+ const operation = moveMatch[1].toLowerCase();
136
+ const from = moveMatch[2].replace(/^`|`$/g, '').trim();
137
+ const to = moveMatch[3].replace(/^`|`$/g, '').trim();
138
+ if (!from || !to || seen.has(`${operation}:${from}->${to}`)) continue;
139
+ seen.add(`${operation}:${from}->${to}`);
140
+ artifacts.push({
141
+ operation,
142
+ from,
143
+ to,
144
+ file: to,
145
+ exists: existsSync(join(workspaceRoot, to)),
146
+ });
147
+ continue;
148
+ }
149
+
150
+ const match = line.match(/^\s*-\s*(CREATE|MODIFY|DELETE|READ|TOUCH):\s*(.+?)\s*$/i);
151
+ if (!match) continue;
152
+
153
+ const operation = match[1].toLowerCase();
154
+ const file = match[2].replace(/^`|`$/g, '').trim();
155
+ if (!file || seen.has(`${operation}:${file}`)) continue;
156
+ seen.add(`${operation}:${file}`);
157
+ artifacts.push({
158
+ operation,
159
+ file,
160
+ exists: existsSync(join(workspaceRoot, file)),
161
+ });
162
+ }
163
+
164
+ return artifacts;
165
+ }
166
+
167
+ export function updateRoadmapPhaseStatus(roadmap, phaseNumber, status) {
168
+ const marker = PHASE_STATUS_MARKERS[status];
169
+ if (!marker) {
170
+ throw new Error(`Unsupported phase status: ${status}`);
171
+ }
172
+
173
+ const normalizedTarget = normalizePhaseToken(phaseNumber);
174
+ const lines = roadmap.split('\n');
175
+ const overviewIndexes = [];
176
+ const detailSections = [];
177
+ let inArchivedDetails = false;
178
+
179
+ for (let index = 0; index < lines.length; index += 1) {
180
+ if (DETAILS_OPEN_RE.test(lines[index]) && !DETAILS_CLOSE_RE.test(lines[index])) {
181
+ inArchivedDetails = true;
182
+ continue;
183
+ }
184
+ if (DETAILS_CLOSE_RE.test(lines[index])) {
185
+ inArchivedDetails = false;
186
+ continue;
187
+ }
188
+ if (inArchivedDetails) continue;
189
+
190
+ const overviewMatch = lines[index].match(ROADMAP_PHASE_STATUS_RE);
191
+ if (overviewMatch && normalizePhaseToken(overviewMatch[4]) === normalizedTarget) {
192
+ overviewIndexes.push({ index, match: overviewMatch });
193
+ continue;
194
+ }
195
+
196
+ const headingMatch = lines[index].match(PHASE_DETAIL_HEADING_RE);
197
+ if (headingMatch && normalizePhaseToken(headingMatch[1]) === normalizedTarget) {
198
+ let statusIndex = -1;
199
+ let statusMatch = null;
200
+ for (let detailIndex = index + 1; detailIndex < lines.length; detailIndex += 1) {
201
+ if (/^#+\s+/.test(lines[detailIndex])) break;
202
+ const candidate = lines[detailIndex].match(PHASE_DETAIL_STATUS_RE);
203
+ if (candidate) {
204
+ statusIndex = detailIndex;
205
+ statusMatch = candidate;
206
+ break;
207
+ }
208
+ }
209
+ detailSections.push({ headingIndex: index, statusIndex, statusMatch });
210
+ }
211
+ }
212
+
213
+ if (overviewIndexes.length === 0) {
214
+ throw new Error(`Phase ${phaseNumber} was not found in ROADMAP.md`);
215
+ }
216
+
217
+ if (overviewIndexes.length > 1) {
218
+ throw new Error(`Phase ${phaseNumber} matched multiple ROADMAP.md entries`);
219
+ }
220
+
221
+ if (detailSections.length > 1) {
222
+ throw new Error(`Phase ${phaseNumber} matched multiple Phase Details sections in ROADMAP.md`);
223
+ }
224
+
225
+ if (detailSections.length === 1 && detailSections[0].statusIndex === -1) {
226
+ throw new Error(`Phase ${phaseNumber} has a Phase Details section but no **Status** line in ROADMAP.md`);
227
+ }
228
+
229
+ const updatedLines = [...lines];
230
+ const overview = overviewIndexes[0];
231
+ updatedLines[overview.index] = `${overview.match[1]}${marker}${overview.match[3]}`;
232
+
233
+ if (detailSections.length === 1) {
234
+ const detail = detailSections[0];
235
+ updatedLines[detail.statusIndex] = `${detail.statusMatch[1]}${marker}${detail.statusMatch[3]}`;
236
+ }
237
+
238
+ return updatedLines.join('\n');
239
+ }
240
+
155
241
  export function cmdPhaseStatus(...args) {
156
242
  const { args: normalizedArgs, planningDir, invalid, error } = resolveWorkspaceContext(args);
157
243
  if (invalid) {
@@ -161,34 +247,34 @@ export function cmdPhaseStatus(...args) {
161
247
  }
162
248
  const roadmapPath = join(planningDir, 'ROADMAP.md');
163
249
  const [phaseNumber, status] = normalizedArgs;
164
-
165
- if (!phaseNumber || !status) {
166
- console.error('Usage: gsdd phase-status <phase-number> <not_started|todo|in_progress|done>');
167
- process.exitCode = 1;
168
- return;
169
- }
170
-
171
- if (!existsSync(roadmapPath)) {
172
- console.error('No ROADMAP.md found. Run the new-project workflow first.');
173
- process.exitCode = 1;
174
- return;
175
- }
176
-
177
- try {
178
- const roadmap = readFileSync(roadmapPath, 'utf-8');
179
- const updated = updateRoadmapPhaseStatus(roadmap, phaseNumber, status);
180
- const changed = updated !== roadmap;
181
- if (changed) {
182
- writeFileSync(roadmapPath, updated);
183
- try { writeFingerprint(planningDir); } catch { /* best-effort */ }
184
- }
185
- output({ phase: phaseNumber, status, roadmap: '.planning/ROADMAP.md', changed });
186
- } catch (error) {
187
- console.error(error.message);
188
- process.exitCode = 1;
189
- }
190
- }
191
-
250
+
251
+ if (!phaseNumber || !status) {
252
+ console.error('Usage: gsdd phase-status <phase-number> <not_started|todo|in_progress|done>');
253
+ process.exitCode = 1;
254
+ return;
255
+ }
256
+
257
+ if (!existsSync(roadmapPath)) {
258
+ console.error('No ROADMAP.md found. Run the new-project workflow first.');
259
+ process.exitCode = 1;
260
+ return;
261
+ }
262
+
263
+ try {
264
+ const roadmap = readFileSync(roadmapPath, 'utf-8');
265
+ const updated = updateRoadmapPhaseStatus(roadmap, phaseNumber, status);
266
+ const changed = updated !== roadmap;
267
+ if (changed) {
268
+ writeFileSync(roadmapPath, updated);
269
+ try { writeFingerprint(planningDir); } catch { /* best-effort */ }
270
+ }
271
+ output({ phase: phaseNumber, status, roadmap: '.planning/ROADMAP.md', changed });
272
+ } catch (error) {
273
+ console.error(error.message);
274
+ process.exitCode = 1;
275
+ }
276
+ }
277
+
192
278
  export function cmdFindPhase(...args) {
193
279
  const { args: normalizedArgs, planningDir, invalid, error } = resolveWorkspaceContext(args);
194
280
  if (invalid) {
@@ -197,52 +283,52 @@ export function cmdFindPhase(...args) {
197
283
  return;
198
284
  }
199
285
  const phaseNum = normalizedArgs[0];
200
-
201
- if (!existsSync(planningDir)) {
202
- output({ error: 'No .planning/ directory found. Run `gsdd init` then the new-project workflow first.' });
203
- return;
204
- }
205
-
206
- const roadmapPath = join(planningDir, 'ROADMAP.md');
207
- if (!existsSync(roadmapPath)) {
208
- output({ error: 'No ROADMAP.md found. Run the new-project workflow first.' });
209
- return;
210
- }
211
-
212
- const phasesDir = join(planningDir, 'phases');
213
- const researchDir = join(planningDir, 'research');
214
-
215
- if (phaseNum) {
216
- const plans = findFiles(phasesDir, `${padPhase(phaseNum)}-PLAN`);
217
- const summaries = findFiles(phasesDir, `${padPhase(phaseNum)}-SUMMARY`);
218
-
219
- output({
220
- phase: normalizePhaseToken(phaseNum),
221
- directory: phasesDir,
222
- plans,
223
- summaries,
224
- hasResearch: existsSync(researchDir) && readdirSync(researchDir).length > 0,
225
- incomplete: plans.filter((p) => !summaries.some((s) => s.replace('SUMMARY', '') === p.replace('PLAN', ''))),
226
- });
227
- return;
228
- }
229
-
230
- const allArtifacts = listPhaseArtifacts(phasesDir);
231
- const plans = allArtifacts.filter((artifact) => artifact.kind === 'PLAN');
232
- const summaries = allArtifacts.filter((artifact) => artifact.kind === 'SUMMARY');
233
-
234
- const roadmap = readFileSync(roadmapPath, 'utf-8');
235
- const phases = parsePhaseStatuses(roadmap);
236
-
237
- output({
238
- phases,
239
- planCount: plans.length,
240
- summaryCount: summaries.length,
241
- currentPhase: phases.find((p) => p.status === 'in_progress') || phases.find((p) => p.status === 'not_started') || null,
242
- hasResearch: existsSync(researchDir) && readdirSync(researchDir).length > 0,
243
- });
244
- }
245
-
286
+
287
+ if (!existsSync(planningDir)) {
288
+ output({ error: 'No .planning/ directory found. Run `npx -y gsdd-cli init` then the new-project workflow first.' });
289
+ return;
290
+ }
291
+
292
+ const roadmapPath = join(planningDir, 'ROADMAP.md');
293
+ if (!existsSync(roadmapPath)) {
294
+ output({ error: 'No ROADMAP.md found. Run the new-project workflow first.' });
295
+ return;
296
+ }
297
+
298
+ const phasesDir = join(planningDir, 'phases');
299
+ const researchDir = join(planningDir, 'research');
300
+
301
+ if (phaseNum) {
302
+ const plans = findFiles(phasesDir, `${padPhase(phaseNum)}-PLAN`);
303
+ const summaries = findFiles(phasesDir, `${padPhase(phaseNum)}-SUMMARY`);
304
+
305
+ output({
306
+ phase: normalizePhaseToken(phaseNum),
307
+ directory: phasesDir,
308
+ plans,
309
+ summaries,
310
+ hasResearch: existsSync(researchDir) && readdirSync(researchDir).length > 0,
311
+ incomplete: plans.filter((p) => !summaries.some((s) => s.replace('SUMMARY', '') === p.replace('PLAN', ''))),
312
+ });
313
+ return;
314
+ }
315
+
316
+ const allArtifacts = listPhaseArtifacts(phasesDir);
317
+ const plans = allArtifacts.filter((artifact) => artifact.kind === 'PLAN');
318
+ const summaries = allArtifacts.filter((artifact) => artifact.kind === 'SUMMARY');
319
+
320
+ const roadmap = readFileSync(roadmapPath, 'utf-8');
321
+ const phases = parsePhaseStatuses(roadmap);
322
+
323
+ output({
324
+ phases,
325
+ planCount: plans.length,
326
+ summaryCount: summaries.length,
327
+ currentPhase: phases.find((p) => p.status === 'in_progress') || phases.find((p) => p.status === 'not_started') || null,
328
+ hasResearch: existsSync(researchDir) && readdirSync(researchDir).length > 0,
329
+ });
330
+ }
331
+
246
332
  export function cmdVerify(...args) {
247
333
  const { args: normalizedArgs, workspaceRoot, planningDir, invalid, error } = resolveWorkspaceContext(args);
248
334
  if (invalid) {
@@ -251,85 +337,37 @@ export function cmdVerify(...args) {
251
337
  return;
252
338
  }
253
339
  const phaseNum = normalizedArgs[0];
254
- if (!phaseNum) {
255
- console.error('Usage: gsdd verify <phase-number>');
256
- process.exitCode = 1; return;
257
- }
258
-
259
- if (!existsSync(planningDir)) {
260
- console.error('No .planning/ directory found.');
261
- process.exitCode = 1; return;
262
- }
263
-
264
- const planFile = findFiles(join(planningDir, 'phases'), `${padPhase(phaseNum)}-PLAN`)[0];
265
- if (!planFile) {
266
- console.error(`No plan found for phase ${phaseNum}`);
267
- process.exitCode = 1; return;
268
- }
269
-
270
- const planPath = join(planningDir, 'phases', planFile);
271
- const plan = readFileSync(planPath, 'utf-8');
272
-
273
- const fileMatches = plan.matchAll(/<files>([\s\S]*?)<\/files>/g);
274
- const expectedFiles = [];
275
- for (const match of fileMatches) {
276
- const lines = match[1]
277
- .split('\n')
278
- .map((l) => l.trim())
279
- .filter((l) => l.startsWith('-'));
280
- for (const line of lines) {
281
- const fileMatch = line.match(/(?:CREATE|MODIFY):\s*(.+)/);
282
- if (fileMatch) expectedFiles.push(fileMatch[1].trim());
283
- }
284
- }
285
-
286
- const results = expectedFiles.map((f) => {
287
- const fullPath = join(workspaceRoot, f);
288
- const exists = existsSync(fullPath);
289
- let substantive = false;
290
- if (exists) {
291
- try {
292
- const content = readFileSync(fullPath, 'utf-8');
293
- const meaningfulLines = content.split('\n').filter(
294
- (l) => l.trim() && !/^\s*(\/\/|\/\*|\*|#)/.test(l)
295
- );
296
- substantive = meaningfulLines.length >= 3 && !content.includes('// TODO: implement');
297
- } catch {
298
- substantive = false;
299
- }
300
- }
301
- return { file: f, exists, substantive };
302
- });
303
-
304
- const antiPatterns = [];
305
- for (const r of results) {
306
- if (!r.exists) continue;
307
- try {
308
- const content = readFileSync(join(workspaceRoot, r.file), 'utf-8');
309
- const lines = content.split('\n');
310
- lines.forEach((line, i) => {
311
- if (/TODO|FIXME|HACK|XXX/.test(line)) {
312
- antiPatterns.push({ file: r.file, line: i + 1, pattern: 'TODO/FIXME', content: line.trim() });
313
- }
314
- if (/catch\s*\([^)]*\)\s*\{[\s]*\}/.test(line) || /catch\s*\([^)]*\)\s*\{\s*$/.test(line)) {
315
- antiPatterns.push({ file: r.file, line: i + 1, pattern: 'Empty catch', content: line.trim() });
316
- }
317
- });
318
- } catch {
319
- // skip unreadable files
320
- }
321
- }
322
-
323
- output({
324
- phase: parseInt(phaseNum, 10),
325
- artifacts: results,
326
- allExist: results.every((r) => r.exists),
327
- allSubstantive: results.filter((r) => r.exists).every((r) => r.substantive),
328
- antiPatterns,
329
- antiPatternCount: antiPatterns.length,
330
- });
331
- }
332
-
340
+ if (!phaseNum) {
341
+ console.error('Usage: gsdd verify <phase-number>');
342
+ process.exitCode = 1; return;
343
+ }
344
+
345
+ if (!existsSync(planningDir)) {
346
+ console.error('No .planning/ directory found.');
347
+ process.exitCode = 1; return;
348
+ }
349
+ const phasesDir = join(planningDir, 'phases');
350
+ const matchingPlans = findFiles(phasesDir, `${padPhase(phaseNum)}-PLAN`);
351
+ const matchingSummaries = findFiles(phasesDir, `${padPhase(phaseNum)}-SUMMARY`);
352
+ const artifacts = matchingPlans.flatMap((planPath) => {
353
+ const fullPath = join(phasesDir, planPath);
354
+ return existsSync(fullPath)
355
+ ? extractPlanFileArtifacts(readFileSync(fullPath, 'utf-8'), workspaceRoot)
356
+ : [];
357
+ });
358
+
359
+ const result = {
360
+ phase: normalizePhaseToken(phaseNum),
361
+ exists: matchingPlans.length > 0,
362
+ plans: matchingPlans,
363
+ summaries: matchingSummaries,
364
+ artifacts,
365
+ allExist: artifacts.every((artifact) => artifact.exists),
366
+ verified: matchingPlans.length > 0 && matchingSummaries.length > 0,
367
+ };
368
+ output(result);
369
+ }
370
+
333
371
  export function cmdScaffold(...args) {
334
372
  const { args: normalizedArgs, planningDir, invalid, error } = resolveWorkspaceContext(args);
335
373
  if (invalid) {
@@ -337,59 +375,23 @@ export function cmdScaffold(...args) {
337
375
  process.exitCode = 1;
338
376
  return;
339
377
  }
340
- const [type, ...rest] = normalizedArgs;
341
-
342
- if (type !== 'phase') {
343
- console.error('Usage: gsdd scaffold phase <number> [name]');
344
- process.exitCode = 1; return;
345
- }
346
-
347
- const phaseNum = rest[0];
348
- const phaseName = rest.slice(1).join(' ');
349
- if (!phaseNum) {
350
- console.error('Usage: gsdd scaffold phase <number> [name]');
351
- process.exitCode = 1; return;
352
- }
353
-
354
- const phasesDir = join(planningDir, 'phases');
355
- mkdirSync(phasesDir, { recursive: true });
356
-
357
- const planFile = join(phasesDir, `${padPhase(phaseNum)}-PLAN.md`);
358
- if (existsSync(planFile)) {
359
- console.log(` - ${basename(planFile)} already exists`);
360
- return;
361
- }
362
-
363
- const content = `# Phase ${phaseNum}: ${phaseName || '[Name]'} - Plan
364
-
365
- ## Phase Goal
366
- [From ROADMAP.md]
367
-
368
- ## Requirements Covered
369
- [REQ-IDs from SPEC.md]
370
-
371
- ## Approach
372
- [2-3 sentences]
373
-
374
- ## Must-Haves (from success criteria)
375
- 1. [Success criterion]
376
-
377
- ## Tasks
378
-
379
- <!-- Add tasks using XML format:
380
- <task id="${phaseNum}-01">
381
- <files>
382
- - CREATE: path/to/file
383
- </files>
384
- <action>Description of what to implement</action>
385
- <verify>How to verify it works</verify>
386
- <done>When is this task done</done>
387
- </task>
388
- -->
389
-
390
- ## Notes
391
- `;
392
-
393
- writeFileSync(planFile, content);
394
- console.log(` - created ${basename(planFile)}`);
395
- }
378
+ const kind = normalizedArgs[0];
379
+ const phaseNum = normalizedArgs[1];
380
+ const phaseName = normalizedArgs[2] || 'phase';
381
+ if (kind !== 'phase' || !phaseNum) {
382
+ console.error('Usage: gsdd scaffold phase <phase-number> [phase-name]');
383
+ process.exitCode = 1; return;
384
+ }
385
+ mkdirSync(planningDir, { recursive: true });
386
+ const phasesDir = join(planningDir, 'phases');
387
+ mkdirSync(phasesDir, { recursive: true });
388
+ const dirName = `${padPhase(phaseNum)}-${phaseName.replace(/\s+/g, '-').toLowerCase()}`;
389
+ const phaseDir = join(phasesDir, dirName);
390
+ mkdirSync(phaseDir, { recursive: true });
391
+ const planPath = join(phaseDir, `${padPhase(phaseNum)}-PLAN.md`);
392
+ const created = !existsSync(planPath);
393
+ if (created) {
394
+ writeFileSync(planPath, `# Phase ${phaseNum} Plan\n\n## Goal\n- \n\n## Tasks\n- [ ] \n`);
395
+ }
396
+ output({ created, path: planPath.replace(/\\/g, '/'), phase: normalizePhaseToken(phaseNum) });
397
+ }