gsdd-cli 0.16.1 → 0.18.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.
- package/README.md +109 -60
- 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 +6 -4
- package/bin/lib/evidence-contract.mjs +112 -0
- package/bin/lib/health-truth.mjs +29 -31
- package/bin/lib/health.mjs +32 -80
- package/bin/lib/init-runtime.mjs +44 -24
- package/bin/lib/init.mjs +3 -7
- package/bin/lib/lifecycle-preflight.mjs +333 -0
- package/bin/lib/lifecycle-state.mjs +293 -0
- package/bin/lib/phase.mjs +81 -26
- package/bin/lib/provenance.mjs +20 -1
- package/bin/lib/rendering.mjs +8 -0
- package/bin/lib/runtime-freshness.mjs +239 -0
- package/bin/lib/session-fingerprint.mjs +106 -0
- package/distilled/DESIGN.md +398 -12
- package/distilled/EVIDENCE-INDEX.md +105 -0
- 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 +13 -0
- package/distilled/workflows/map-codebase.md +14 -3
- package/distilled/workflows/new-milestone.md +13 -0
- package/distilled/workflows/new-project.md +3 -1
- package/distilled/workflows/plan.md +3 -1
- package/distilled/workflows/progress.md +68 -13
- package/distilled/workflows/quick.md +15 -8
- package/distilled/workflows/resume.md +14 -1
- package/distilled/workflows/verify.md +52 -17
- package/docs/BROWNFIELD-PROOF.md +95 -0
- package/docs/RUNTIME-SUPPORT.md +77 -0
- package/docs/USER-GUIDE.md +439 -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 +34 -27
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
const PHASE_LINE_RE = /^\s*[-*]\s*\[([ x-])\]\s*\*\*Phase\s+(\d+(?:\.\d+)*[a-z]?):\s*(.+?)\*\*(?:\s+—\s+\[([^\]]+)])?/i;
|
|
5
|
+
const ACTIVE_MILESTONE_HEADING_RE = /^###\s+(v[^\s]+)\s+(.+)$/im;
|
|
6
|
+
const MILESTONE_LEDGER_HEADING_RE = /^##\s+(?:✅\s+)?(v[^\s]+)\s*(?:—|-)?\s*(.*)$/i;
|
|
7
|
+
|
|
8
|
+
export function evaluateLifecycleState({ planningDir, provenance = null } = {}) {
|
|
9
|
+
if (!planningDir) {
|
|
10
|
+
throw new Error('planningDir is required');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const specPath = join(planningDir, 'SPEC.md');
|
|
14
|
+
const roadmapPath = join(planningDir, 'ROADMAP.md');
|
|
15
|
+
const milestonesPath = join(planningDir, 'MILESTONES.md');
|
|
16
|
+
const phasesDir = join(planningDir, 'phases');
|
|
17
|
+
|
|
18
|
+
const spec = readTextIfExists(specPath);
|
|
19
|
+
const roadmap = readTextIfExists(roadmapPath);
|
|
20
|
+
const milestones = readTextIfExists(milestonesPath);
|
|
21
|
+
|
|
22
|
+
const phases = parseActiveRoadmapPhases(roadmap);
|
|
23
|
+
const phaseArtifacts = collectPhaseArtifacts(phasesDir);
|
|
24
|
+
const enrichedPhases = phases.map((phase) => {
|
|
25
|
+
const matchingArtifacts = phaseArtifacts.filter((artifact) => artifact.phaseToken === phase.number);
|
|
26
|
+
const hasPlan = matchingArtifacts.some((artifact) => artifact.kind === 'plan');
|
|
27
|
+
const hasSummary = matchingArtifacts.some((artifact) => artifact.kind === 'summary');
|
|
28
|
+
return {
|
|
29
|
+
...phase,
|
|
30
|
+
hasArtifacts: matchingArtifacts.length > 0,
|
|
31
|
+
hasPlan,
|
|
32
|
+
hasSummary,
|
|
33
|
+
artifacts: matchingArtifacts,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const incompletePlans = phaseArtifacts.filter((artifact) => {
|
|
38
|
+
if (artifact.kind !== 'plan') return false;
|
|
39
|
+
return !phaseArtifacts.some((candidate) =>
|
|
40
|
+
candidate.dir === artifact.dir &&
|
|
41
|
+
candidate.baseId === artifact.baseId &&
|
|
42
|
+
candidate.kind === 'summary'
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const counts = {
|
|
47
|
+
total: enrichedPhases.length,
|
|
48
|
+
completed: enrichedPhases.filter((phase) => phase.status === 'done').length,
|
|
49
|
+
inProgress: enrichedPhases.filter((phase) => phase.status === 'in_progress').length,
|
|
50
|
+
notStarted: enrichedPhases.filter((phase) => phase.status === 'not_started').length,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const milestoneLedger = parseMilestoneLedger(milestones);
|
|
54
|
+
const currentMilestone = deriveCurrentMilestone({
|
|
55
|
+
roadmap,
|
|
56
|
+
planningDir,
|
|
57
|
+
milestoneLedger,
|
|
58
|
+
counts,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
paths: {
|
|
63
|
+
specPath,
|
|
64
|
+
roadmapPath,
|
|
65
|
+
milestonesPath,
|
|
66
|
+
phasesDir,
|
|
67
|
+
},
|
|
68
|
+
currentMilestone,
|
|
69
|
+
phases: enrichedPhases,
|
|
70
|
+
currentPhase: enrichedPhases.find((phase) => phase.status === 'in_progress') || null,
|
|
71
|
+
nextPhase: enrichedPhases.find((phase) => phase.status === 'not_started') || null,
|
|
72
|
+
counts,
|
|
73
|
+
phaseArtifacts,
|
|
74
|
+
incompletePlans,
|
|
75
|
+
requirementAlignment: evaluateRequirementAlignment(spec, enrichedPhases),
|
|
76
|
+
provenance: provenance
|
|
77
|
+
? {
|
|
78
|
+
provided: true,
|
|
79
|
+
...provenance,
|
|
80
|
+
}
|
|
81
|
+
: { provided: false },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readTextIfExists(filePath) {
|
|
86
|
+
return existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeContent(content) {
|
|
90
|
+
return String(content || '').replace(/\r\n/g, '\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizePhaseToken(value) {
|
|
94
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
95
|
+
const match = raw.match(/^(\d+(?:\.\d+)*)([a-z]?)$/i);
|
|
96
|
+
if (!match) return raw;
|
|
97
|
+
|
|
98
|
+
const numericSegments = match[1]
|
|
99
|
+
.split('.')
|
|
100
|
+
.map((segment) => String(parseInt(segment, 10)));
|
|
101
|
+
return `${numericSegments.join('.')}${match[2] || ''}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizePhaseStatus(marker) {
|
|
105
|
+
const normalized = String(marker || '').trim().toLowerCase();
|
|
106
|
+
if (normalized === 'x') return 'done';
|
|
107
|
+
if (normalized === '-') return 'in_progress';
|
|
108
|
+
return 'not_started';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseActiveRoadmapPhases(roadmap) {
|
|
112
|
+
if (!roadmap) return [];
|
|
113
|
+
|
|
114
|
+
const phases = [];
|
|
115
|
+
let inDetails = false;
|
|
116
|
+
|
|
117
|
+
for (const line of normalizeContent(roadmap).split('\n')) {
|
|
118
|
+
if (line.includes('<details>') && !line.includes('</details>')) {
|
|
119
|
+
inDetails = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (line.includes('</details>')) {
|
|
123
|
+
inDetails = false;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (inDetails) continue;
|
|
127
|
+
|
|
128
|
+
const match = line.match(PHASE_LINE_RE);
|
|
129
|
+
if (!match) continue;
|
|
130
|
+
|
|
131
|
+
phases.push({
|
|
132
|
+
number: normalizePhaseToken(match[2]),
|
|
133
|
+
title: match[3].trim(),
|
|
134
|
+
status: normalizePhaseStatus(match[1]),
|
|
135
|
+
requirements: splitRequirementList(match[4]),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return phases;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function splitRequirementList(rawRequirements = '') {
|
|
143
|
+
return String(rawRequirements)
|
|
144
|
+
.split(',')
|
|
145
|
+
.map((entry) => entry.trim())
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function collectPhaseArtifacts(phasesDir) {
|
|
150
|
+
if (!existsSync(phasesDir)) return [];
|
|
151
|
+
|
|
152
|
+
const artifacts = [];
|
|
153
|
+
for (const entry of readdirSync(phasesDir, { withFileTypes: true })) {
|
|
154
|
+
const entryPath = join(phasesDir, entry.name);
|
|
155
|
+
if (entry.isFile()) {
|
|
156
|
+
const artifact = classifyPhaseArtifact('', entry.name);
|
|
157
|
+
if (artifact) artifacts.push(artifact);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!entry.isDirectory()) continue;
|
|
162
|
+
|
|
163
|
+
for (const child of readdirSync(entryPath, { withFileTypes: true })) {
|
|
164
|
+
if (!child.isFile()) continue;
|
|
165
|
+
const artifact = classifyPhaseArtifact(entry.name, child.name);
|
|
166
|
+
if (artifact) artifacts.push(artifact);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return artifacts;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function classifyPhaseArtifact(dir, name) {
|
|
174
|
+
const baseIdMatch = name.match(/^(\d+(?:\.\d+)*[a-z]?(?:-\d+)?)/i);
|
|
175
|
+
const phaseTokenMatch = (dir ? dir.match(/^(\d+(?:\.\d+)*[a-z]?)-/i) : null)
|
|
176
|
+
|| name.match(/^(\d+(?:\.\d+)*[a-z]?)/i);
|
|
177
|
+
|
|
178
|
+
if (!baseIdMatch || !phaseTokenMatch) return null;
|
|
179
|
+
|
|
180
|
+
let kind = 'other';
|
|
181
|
+
if (name.includes('PLAN')) kind = 'plan';
|
|
182
|
+
else if (name.includes('SUMMARY')) kind = 'summary';
|
|
183
|
+
else if (name.includes('VERIFICATION')) kind = 'verification';
|
|
184
|
+
else if (name.includes('APPROACH')) kind = 'approach';
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
dir,
|
|
188
|
+
name,
|
|
189
|
+
displayPath: dir ? `${dir}/${name}` : name,
|
|
190
|
+
baseId: baseIdMatch[1],
|
|
191
|
+
phaseToken: normalizePhaseToken(phaseTokenMatch[1]),
|
|
192
|
+
kind,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function parseMilestoneLedger(content) {
|
|
197
|
+
if (!content) return [];
|
|
198
|
+
|
|
199
|
+
const entries = [];
|
|
200
|
+
let current = null;
|
|
201
|
+
|
|
202
|
+
for (const line of normalizeContent(content).split('\n')) {
|
|
203
|
+
const headingMatch = line.match(MILESTONE_LEDGER_HEADING_RE);
|
|
204
|
+
if (headingMatch) {
|
|
205
|
+
if (current) entries.push(current);
|
|
206
|
+
current = {
|
|
207
|
+
version: headingMatch[1],
|
|
208
|
+
title: headingMatch[2].trim(),
|
|
209
|
+
lines: [],
|
|
210
|
+
};
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (current) {
|
|
215
|
+
current.lines.push(line);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (current) entries.push(current);
|
|
220
|
+
|
|
221
|
+
return entries.map((entry) => {
|
|
222
|
+
const section = entry.lines.join('\n');
|
|
223
|
+
const statusMatch = section.match(/^- Status:\s*(.+)$/im);
|
|
224
|
+
const shipped = /(^- Status:\s*shipped$)|(^- Shipped:\s+)/im.test(section) || /^✅/.test(entry.title);
|
|
225
|
+
return {
|
|
226
|
+
version: entry.version,
|
|
227
|
+
title: entry.title.replace(/^✅\s*/, ''),
|
|
228
|
+
status: statusMatch ? statusMatch[1].trim().toLowerCase() : shipped ? 'shipped' : 'unknown',
|
|
229
|
+
shipped,
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function deriveCurrentMilestone({ roadmap, planningDir, milestoneLedger, counts }) {
|
|
235
|
+
const normalizedRoadmap = normalizeContent(roadmap);
|
|
236
|
+
const headingMatch = normalizedRoadmap.match(ACTIVE_MILESTONE_HEADING_RE);
|
|
237
|
+
if (!headingMatch) {
|
|
238
|
+
return {
|
|
239
|
+
version: null,
|
|
240
|
+
title: null,
|
|
241
|
+
shippedInLedger: false,
|
|
242
|
+
hasMatchingAudit: false,
|
|
243
|
+
archiveState: 'unknown',
|
|
244
|
+
counts,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const version = headingMatch[1];
|
|
249
|
+
const title = headingMatch[2].trim();
|
|
250
|
+
const ledgerEntry = milestoneLedger.find((entry) => entry.version === version) || null;
|
|
251
|
+
const auditPath = join(planningDir, `${version}-MILESTONE-AUDIT.md`);
|
|
252
|
+
const hasMatchingAudit = existsSync(auditPath);
|
|
253
|
+
const shippedInLedger = Boolean(ledgerEntry?.shipped);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
version,
|
|
257
|
+
title,
|
|
258
|
+
shippedInLedger,
|
|
259
|
+
hasMatchingAudit,
|
|
260
|
+
archiveState: shippedInLedger && hasMatchingAudit ? 'archived' : 'active',
|
|
261
|
+
counts,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function evaluateRequirementAlignment(spec, phases) {
|
|
266
|
+
const checkedRequirements = new Set(
|
|
267
|
+
[...normalizeContent(spec).matchAll(/- \[x\] \*\*\[([A-Z0-9-]+)]\*\*/g)].map((result) => result[1])
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const roadmapRequirements = new Map();
|
|
271
|
+
for (const phase of phases) {
|
|
272
|
+
for (const requirementId of phase.requirements) {
|
|
273
|
+
roadmapRequirements.set(requirementId, phase.status === 'done');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const mismatches = [];
|
|
278
|
+
for (const [requirementId, roadmapStatus] of roadmapRequirements.entries()) {
|
|
279
|
+
const specChecked = checkedRequirements.has(requirementId);
|
|
280
|
+
if (roadmapStatus && !specChecked) {
|
|
281
|
+
mismatches.push(`${requirementId} phase complete but SPEC unchecked`);
|
|
282
|
+
}
|
|
283
|
+
if (!roadmapStatus && specChecked) {
|
|
284
|
+
mismatches.push(`${requirementId} SPEC checked but phase incomplete`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
checkedRequirements,
|
|
290
|
+
roadmapRequirements,
|
|
291
|
+
mismatches,
|
|
292
|
+
};
|
|
293
|
+
}
|
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);
|
package/bin/lib/provenance.mjs
CHANGED
|
@@ -9,6 +9,23 @@ function normalizeCount(value) {
|
|
|
9
9
|
return Number.isFinite(numeric) ? numeric : 'unknown';
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
function normalizeCheckpointWorkflow(workflow) {
|
|
13
|
+
const normalized = String(workflow || 'generic').trim().toLowerCase();
|
|
14
|
+
return ['phase', 'quick', 'generic'].includes(normalized) ? normalized : 'generic';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function classifyCheckpointRouting(workflow) {
|
|
18
|
+
const normalizedWorkflow = normalizeCheckpointWorkflow(workflow);
|
|
19
|
+
const progressBlocks = normalizedWorkflow === 'phase' || normalizedWorkflow === 'quick';
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
workflow: normalizedWorkflow,
|
|
23
|
+
routingClass: progressBlocks ? 'blocking' : 'informational',
|
|
24
|
+
progressBlocks,
|
|
25
|
+
resumeOwnsCleanup: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
export function parseGitStatusShort(statusText = '') {
|
|
13
30
|
const lines = statusText
|
|
14
31
|
.replace(/\r\n/g, '\n')
|
|
@@ -48,6 +65,7 @@ export function buildProvenanceSnapshot({
|
|
|
48
65
|
planning = {},
|
|
49
66
|
git = {},
|
|
50
67
|
} = {}) {
|
|
68
|
+
const checkpointRouting = classifyCheckpointRouting(checkpoint.workflow);
|
|
51
69
|
const status = parseGitStatusShort(git.statusShort || '');
|
|
52
70
|
const commitsAheadOfMain = normalizeCount(git.commitsAheadOfMain);
|
|
53
71
|
const commitsAheadOfRemote = normalizeCount(git.commitsAheadOfRemote);
|
|
@@ -113,10 +131,11 @@ export function buildProvenanceSnapshot({
|
|
|
113
131
|
|
|
114
132
|
return {
|
|
115
133
|
checkpoint: {
|
|
116
|
-
workflow:
|
|
134
|
+
workflow: checkpointRouting.workflow,
|
|
117
135
|
phase: checkpoint.phase ?? null,
|
|
118
136
|
runtime: checkpoint.runtime || 'unknown',
|
|
119
137
|
hasNarrative: Boolean(checkpoint.hasNarrative),
|
|
138
|
+
routing: checkpointRouting,
|
|
120
139
|
},
|
|
121
140
|
planning: {
|
|
122
141
|
currentPhase: planning.currentPhase || null,
|
package/bin/lib/rendering.mjs
CHANGED
|
@@ -30,6 +30,13 @@ agent: ${workflow.agent}
|
|
|
30
30
|
${workflowContent}`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function buildPortableSkillEntries(workflows) {
|
|
34
|
+
return workflows.map((workflow) => ({
|
|
35
|
+
relativePath: `.agents/skills/${workflow.name}/SKILL.md`,
|
|
36
|
+
content: renderSkillContent(workflow),
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
function renderOpenCodeCommandContent(workflow) {
|
|
34
41
|
const workflowContent = getWorkflowContent(workflow.workflow);
|
|
35
42
|
return `---
|
|
@@ -85,6 +92,7 @@ function upsertBoundedBlock(existing, blockContent) {
|
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
export {
|
|
95
|
+
buildPortableSkillEntries,
|
|
88
96
|
getDelegateContent,
|
|
89
97
|
getWorkflowContent,
|
|
90
98
|
renderAgentsBoundedBlock,
|