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