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.
- package/README.md +111 -61
- package/agents/README.md +1 -1
- package/bin/adapters/claude.mjs +8 -1
- package/bin/adapters/codex.mjs +5 -1
- package/bin/adapters/opencode.mjs +7 -1
- package/bin/gsdd.mjs +25 -20
- package/bin/lib/evidence-contract.mjs +112 -0
- package/bin/lib/health-truth.mjs +29 -31
- package/bin/lib/health.mjs +61 -106
- package/bin/lib/init-flow.mjs +91 -34
- package/bin/lib/init-runtime.mjs +46 -25
- package/bin/lib/init.mjs +3 -7
- package/bin/lib/lifecycle-preflight.mjs +333 -0
- package/bin/lib/lifecycle-state.mjs +476 -0
- package/bin/lib/manifest.mjs +24 -20
- package/bin/lib/phase.mjs +81 -26
- package/bin/lib/provenance.mjs +295 -54
- package/bin/lib/rendering.mjs +70 -12
- package/bin/lib/runtime-freshness.mjs +264 -0
- package/bin/lib/session-fingerprint.mjs +106 -0
- package/distilled/DESIGN.md +707 -13
- package/distilled/EVIDENCE-INDEX.md +166 -1
- package/distilled/README.md +44 -28
- package/distilled/SKILL.md +4 -4
- package/distilled/templates/agents.block.md +1 -1
- package/distilled/workflows/audit-milestone.md +50 -0
- package/distilled/workflows/complete-milestone.md +38 -2
- package/distilled/workflows/execute.md +16 -3
- package/distilled/workflows/map-codebase.md +15 -4
- package/distilled/workflows/new-milestone.md +53 -24
- package/distilled/workflows/new-project.md +44 -25
- package/distilled/workflows/pause.md +1 -1
- package/distilled/workflows/plan.md +187 -74
- package/distilled/workflows/progress.md +135 -31
- package/distilled/workflows/quick.md +20 -12
- package/distilled/workflows/resume.md +152 -65
- package/distilled/workflows/verify.md +55 -20
- package/docs/BROWNFIELD-PROOF.md +95 -0
- package/docs/RUNTIME-SUPPORT.md +77 -0
- package/docs/USER-GUIDE.md +443 -0
- package/docs/VERIFICATION-DISCIPLINE.md +59 -0
- package/docs/claude/context-monitor.md +98 -0
- package/docs/proof/consumer-node-cli/README.md +37 -0
- package/docs/proof/consumer-node-cli/ROADMAP.md +14 -0
- package/docs/proof/consumer-node-cli/SPEC.md +17 -0
- package/docs/proof/consumer-node-cli/brief.md +9 -0
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -0
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -0
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -0
- 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
|
+
}
|
package/bin/lib/manifest.mjs
CHANGED
|
@@ -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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
167
|
-
const plans =
|
|
168
|
-
const summaries =
|
|
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);
|