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.
Files changed (47) hide show
  1. package/README.md +109 -60
  2. package/agents/README.md +1 -1
  3. package/bin/adapters/claude.mjs +8 -1
  4. package/bin/adapters/codex.mjs +5 -1
  5. package/bin/adapters/opencode.mjs +7 -1
  6. package/bin/gsdd.mjs +6 -4
  7. package/bin/lib/evidence-contract.mjs +112 -0
  8. package/bin/lib/health-truth.mjs +29 -31
  9. package/bin/lib/health.mjs +32 -80
  10. package/bin/lib/init-runtime.mjs +44 -24
  11. package/bin/lib/init.mjs +3 -7
  12. package/bin/lib/lifecycle-preflight.mjs +333 -0
  13. package/bin/lib/lifecycle-state.mjs +293 -0
  14. package/bin/lib/phase.mjs +81 -26
  15. package/bin/lib/provenance.mjs +20 -1
  16. package/bin/lib/rendering.mjs +8 -0
  17. package/bin/lib/runtime-freshness.mjs +239 -0
  18. package/bin/lib/session-fingerprint.mjs +106 -0
  19. package/distilled/DESIGN.md +398 -12
  20. package/distilled/EVIDENCE-INDEX.md +105 -0
  21. package/distilled/README.md +44 -28
  22. package/distilled/SKILL.md +4 -4
  23. package/distilled/templates/agents.block.md +1 -1
  24. package/distilled/workflows/audit-milestone.md +50 -0
  25. package/distilled/workflows/complete-milestone.md +38 -2
  26. package/distilled/workflows/execute.md +13 -0
  27. package/distilled/workflows/map-codebase.md +14 -3
  28. package/distilled/workflows/new-milestone.md +13 -0
  29. package/distilled/workflows/new-project.md +3 -1
  30. package/distilled/workflows/plan.md +3 -1
  31. package/distilled/workflows/progress.md +68 -13
  32. package/distilled/workflows/quick.md +15 -8
  33. package/distilled/workflows/resume.md +14 -1
  34. package/distilled/workflows/verify.md +52 -17
  35. package/docs/BROWNFIELD-PROOF.md +95 -0
  36. package/docs/RUNTIME-SUPPORT.md +77 -0
  37. package/docs/USER-GUIDE.md +439 -0
  38. package/docs/VERIFICATION-DISCIPLINE.md +59 -0
  39. package/docs/claude/context-monitor.md +98 -0
  40. package/docs/proof/consumer-node-cli/README.md +37 -0
  41. package/docs/proof/consumer-node-cli/ROADMAP.md +14 -0
  42. package/docs/proof/consumer-node-cli/SPEC.md +17 -0
  43. package/docs/proof/consumer-node-cli/brief.md +9 -0
  44. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -0
  45. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -0
  46. package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -0
  47. 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
- return readdirSync(dir).filter((f) => f.startsWith(prefix) || f.startsWith(prefix.replace(/^0+/, '')));
31
+
32
+ const phaseArtifactPrefix = String(prefix).match(/^(\d+(?:\.\d+)*[a-z]?)-(PLAN|SUMMARY)$/i);
33
+ if (!phaseArtifactPrefix) {
34
+ return readdirSync(dir).filter((f) => f.startsWith(prefix) || f.startsWith(prefix.replace(/^0+/, '')));
35
+ }
36
+
37
+ const targetPhase = normalizePhaseToken(phaseArtifactPrefix[1]);
38
+ const targetKind = phaseArtifactPrefix[2].toUpperCase();
39
+
40
+ return listPhaseArtifacts(dir)
41
+ .filter((artifact) => artifact.phaseToken === targetPhase && artifact.kind === targetKind)
42
+ .map((artifact) => artifact.displayPath);
43
+ }
44
+
45
+ function listPhaseArtifacts(dir) {
46
+ if (!existsSync(dir)) return [];
47
+
48
+ const artifacts = [];
49
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
50
+ if (entry.isFile()) {
51
+ const artifact = classifyPhaseArtifact('', entry.name);
52
+ if (artifact) artifacts.push(artifact);
53
+ continue;
54
+ }
55
+
56
+ if (!entry.isDirectory()) continue;
57
+
58
+ const entryPath = join(dir, entry.name);
59
+ for (const child of readdirSync(entryPath, { withFileTypes: true })) {
60
+ if (!child.isFile()) continue;
61
+ const artifact = classifyPhaseArtifact(entry.name, child.name);
62
+ if (artifact) artifacts.push(artifact);
63
+ }
64
+ }
65
+
66
+ return artifacts;
67
+ }
68
+
69
+ function classifyPhaseArtifact(dir, name) {
70
+ const dirMatch = dir ? dir.match(/^(\d+(?:\.\d+)*[a-z]?)-/i) : null;
71
+ const nameMatch = name.match(/^(\d+(?:\.\d+)*[a-z]?)/i);
72
+ const phaseToken = normalizePhaseToken((dirMatch || nameMatch)?.[1] || '');
73
+
74
+ let kind = 'OTHER';
75
+ if (name.includes('PLAN')) kind = 'PLAN';
76
+ else if (name.includes('SUMMARY')) kind = 'SUMMARY';
77
+
78
+ return {
79
+ dir,
80
+ name,
81
+ displayPath: dir ? `${dir}/${name}` : name,
82
+ phaseToken,
83
+ kind,
84
+ };
31
85
  }
32
86
 
33
87
  function padPhase(n) {
@@ -97,23 +151,23 @@ export function updateRoadmapPhaseStatus(roadmap, phaseNumber, status) {
97
151
  return updated;
98
152
  }
99
153
 
100
- export function cmdPhaseStatus(...args) {
101
- const cwd = process.cwd();
102
- const planningDir = join(cwd, '.planning');
103
- const roadmapPath = join(planningDir, 'ROADMAP.md');
104
- const [phaseNumber, status] = args;
105
-
106
- if (!phaseNumber || !status) {
107
- console.error('Usage: gsdd phase-status <phase-number> <not_started|todo|in_progress|done>');
108
- process.exitCode = 1;
109
- return;
110
- }
111
-
112
- if (!existsSync(roadmapPath)) {
113
- console.error('No ROADMAP.md found. Run the new-project workflow first.');
114
- process.exitCode = 1;
115
- return;
116
- }
154
+ export function cmdPhaseStatus(...args) {
155
+ const cwd = process.cwd();
156
+ const planningDir = join(cwd, '.planning');
157
+ const roadmapPath = join(planningDir, 'ROADMAP.md');
158
+ const [phaseNumber, status] = args;
159
+
160
+ if (!phaseNumber || !status) {
161
+ console.error('Usage: gsdd phase-status <phase-number> <not_started|todo|in_progress|done>');
162
+ process.exitCode = 1;
163
+ return;
164
+ }
165
+
166
+ if (!existsSync(roadmapPath)) {
167
+ console.error('No ROADMAP.md found. Run the new-project workflow first.');
168
+ process.exitCode = 1;
169
+ return;
170
+ }
117
171
 
118
172
  try {
119
173
  const roadmap = readFileSync(roadmapPath, 'utf-8');
@@ -121,13 +175,14 @@ export function cmdPhaseStatus(...args) {
121
175
  const changed = updated !== roadmap;
122
176
  if (changed) {
123
177
  writeFileSync(roadmapPath, updated);
178
+ try { writeFingerprint(planningDir); } catch { /* best-effort */ }
124
179
  }
125
180
  output({ phase: phaseNumber, status, roadmap: '.planning/ROADMAP.md', changed });
126
- } catch (error) {
127
- console.error(error.message);
128
- process.exitCode = 1;
129
- }
130
- }
181
+ } catch (error) {
182
+ console.error(error.message);
183
+ process.exitCode = 1;
184
+ }
185
+ }
131
186
 
132
187
  export function cmdFindPhase(...args) {
133
188
  const cwd = process.cwd();
@@ -163,9 +218,9 @@ export function cmdFindPhase(...args) {
163
218
  return;
164
219
  }
165
220
 
166
- const allFiles = existsSync(phasesDir) ? readdirSync(phasesDir) : [];
167
- const plans = allFiles.filter((f) => f.includes('PLAN'));
168
- const summaries = allFiles.filter((f) => f.includes('SUMMARY'));
221
+ const allArtifacts = listPhaseArtifacts(phasesDir);
222
+ const plans = allArtifacts.filter((artifact) => artifact.kind === 'PLAN');
223
+ const summaries = allArtifacts.filter((artifact) => artifact.kind === 'SUMMARY');
169
224
 
170
225
  const roadmap = readFileSync(roadmapPath, 'utf-8');
171
226
  const phases = parsePhaseStatuses(roadmap);
@@ -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: checkpoint.workflow || 'generic',
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,
@@ -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,