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.
Files changed (50) hide show
  1. package/README.md +111 -61
  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 +25 -20
  7. package/bin/lib/evidence-contract.mjs +112 -0
  8. package/bin/lib/health-truth.mjs +29 -31
  9. package/bin/lib/health.mjs +61 -106
  10. package/bin/lib/init-flow.mjs +91 -34
  11. package/bin/lib/init-runtime.mjs +46 -25
  12. package/bin/lib/init.mjs +3 -7
  13. package/bin/lib/lifecycle-preflight.mjs +333 -0
  14. package/bin/lib/lifecycle-state.mjs +476 -0
  15. package/bin/lib/manifest.mjs +24 -20
  16. package/bin/lib/phase.mjs +81 -26
  17. package/bin/lib/provenance.mjs +295 -54
  18. package/bin/lib/rendering.mjs +70 -12
  19. package/bin/lib/runtime-freshness.mjs +264 -0
  20. package/bin/lib/session-fingerprint.mjs +106 -0
  21. package/distilled/DESIGN.md +707 -13
  22. package/distilled/EVIDENCE-INDEX.md +166 -1
  23. package/distilled/README.md +44 -28
  24. package/distilled/SKILL.md +4 -4
  25. package/distilled/templates/agents.block.md +1 -1
  26. package/distilled/workflows/audit-milestone.md +50 -0
  27. package/distilled/workflows/complete-milestone.md +38 -2
  28. package/distilled/workflows/execute.md +16 -3
  29. package/distilled/workflows/map-codebase.md +15 -4
  30. package/distilled/workflows/new-milestone.md +53 -24
  31. package/distilled/workflows/new-project.md +44 -25
  32. package/distilled/workflows/pause.md +1 -1
  33. package/distilled/workflows/plan.md +187 -74
  34. package/distilled/workflows/progress.md +135 -31
  35. package/distilled/workflows/quick.md +20 -12
  36. package/distilled/workflows/resume.md +152 -65
  37. package/distilled/workflows/verify.md +55 -20
  38. package/docs/BROWNFIELD-PROOF.md +95 -0
  39. package/docs/RUNTIME-SUPPORT.md +77 -0
  40. package/docs/USER-GUIDE.md +443 -0
  41. package/docs/VERIFICATION-DISCIPLINE.md +59 -0
  42. package/docs/claude/context-monitor.md +98 -0
  43. package/docs/proof/consumer-node-cli/README.md +37 -0
  44. package/docs/proof/consumer-node-cli/ROADMAP.md +14 -0
  45. package/docs/proof/consumer-node-cli/SPEC.md +17 -0
  46. package/docs/proof/consumer-node-cli/brief.md +9 -0
  47. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -0
  48. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -0
  49. package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -0
  50. package/package.json +26 -20
@@ -0,0 +1,333 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { output } from './cli-utils.mjs';
4
+ import { describeEvidenceSurface } from './evidence-contract.mjs';
5
+ import { evaluateLifecycleState, normalizePhaseToken } from './lifecycle-state.mjs';
6
+ import { checkDrift } from './session-fingerprint.mjs';
7
+
8
+ const SURFACE_POLICIES = {
9
+ progress: {
10
+ classification: 'read_only',
11
+ ownedWrites: [],
12
+ explicitLifecycleMutation: 'none',
13
+ },
14
+ execute: {
15
+ classification: 'owned_write',
16
+ ownedWrites: ['summary'],
17
+ explicitLifecycleMutation: 'phase-status',
18
+ phaseRequired: true,
19
+ },
20
+ verify: {
21
+ classification: 'owned_write',
22
+ ownedWrites: ['verification'],
23
+ explicitLifecycleMutation: 'phase-status',
24
+ phaseRequired: true,
25
+ },
26
+ 'audit-milestone': {
27
+ classification: 'owned_write',
28
+ ownedWrites: ['milestone-audit'],
29
+ explicitLifecycleMutation: 'none',
30
+ },
31
+ 'complete-milestone': {
32
+ classification: 'owned_write',
33
+ ownedWrites: ['milestone-archives', 'milestones-ledger', 'spec', 'roadmap'],
34
+ explicitLifecycleMutation: 'none',
35
+ },
36
+ 'new-milestone': {
37
+ classification: 'owned_write',
38
+ ownedWrites: ['spec', 'roadmap', 'phase-directories'],
39
+ explicitLifecycleMutation: 'none',
40
+ },
41
+ resume: {
42
+ classification: 'owned_write',
43
+ ownedWrites: ['checkpoint-cleanup'],
44
+ explicitLifecycleMutation: 'none',
45
+ },
46
+ };
47
+
48
+ export function evaluateLifecyclePreflight({
49
+ planningDir,
50
+ surface,
51
+ phaseNumber = null,
52
+ expectsMutation = 'none',
53
+ } = {}) {
54
+ if (!planningDir) {
55
+ throw new Error('planningDir is required');
56
+ }
57
+
58
+ const policy = SURFACE_POLICIES[surface];
59
+ if (!policy) {
60
+ throw new Error(`Unsupported lifecycle surface: ${surface}`);
61
+ }
62
+
63
+ const lifecycle = evaluateLifecycleState({ planningDir });
64
+ const normalizedPhase = phaseNumber ? normalizePhaseToken(phaseNumber) : null;
65
+ const checkpointPath = join(planningDir, '.continue-here.md');
66
+ const specPath = join(planningDir, 'SPEC.md');
67
+ const milestonesPath = join(planningDir, 'MILESTONES.md');
68
+ const blockers = [];
69
+
70
+ if (!existsSync(planningDir)) {
71
+ blockers.push(blocker('missing_planning_dir', '.planning/ does not exist yet.', ['.planning/']));
72
+ }
73
+
74
+ if (expectsMutation !== 'none' && expectsMutation !== policy.explicitLifecycleMutation) {
75
+ blockers.push(
76
+ blocker(
77
+ 'illegal_lifecycle_mutation',
78
+ `${surface} is classified as ${policy.classification} and cannot mutate lifecycle state via ${expectsMutation}.`,
79
+ []
80
+ )
81
+ );
82
+ }
83
+
84
+ if (policy.phaseRequired && !normalizedPhase) {
85
+ blockers.push(blocker('missing_phase_argument', `${surface} requires an explicit phase number.`, []));
86
+ }
87
+
88
+ if (normalizedPhase) {
89
+ blockers.push(...buildPhaseBlockers({ lifecycle, phaseToken: normalizedPhase, surface }));
90
+ }
91
+
92
+ if (surface === 'audit-milestone') {
93
+ blockers.push(...buildAuditBlockers(lifecycle));
94
+ }
95
+
96
+ if (surface === 'complete-milestone') {
97
+ blockers.push(...buildAuditBlockers(lifecycle, { allowArchivedBlocker: true }));
98
+ blockers.push(...buildCompletionBlockers(planningDir, lifecycle));
99
+ }
100
+
101
+ if (surface === 'new-milestone') {
102
+ if (!existsSync(specPath)) {
103
+ blockers.push(blocker('missing_spec', 'SPEC.md is required before starting a new milestone.', ['.planning/SPEC.md']));
104
+ }
105
+ if (!existsSync(milestonesPath)) {
106
+ blockers.push(blocker('missing_milestones', 'MILESTONES.md is required before starting a new milestone.', ['.planning/MILESTONES.md']));
107
+ }
108
+ if (lifecycle.currentMilestone.version && lifecycle.currentMilestone.archiveState !== 'archived') {
109
+ blockers.push(
110
+ blocker(
111
+ 'active_milestone_in_progress',
112
+ `Milestone ${lifecycle.currentMilestone.version} is still active. Archive or remove the active roadmap before starting the next milestone.`,
113
+ ['.planning/ROADMAP.md']
114
+ )
115
+ );
116
+ }
117
+ }
118
+
119
+ if (surface === 'resume' && !existsSync(checkpointPath)) {
120
+ blockers.push(blocker('missing_checkpoint', 'resume requires .planning/.continue-here.md to exist.', ['.planning/.continue-here.md']));
121
+ }
122
+
123
+ const warnings = [];
124
+
125
+ if (existsSync(planningDir)) {
126
+ const drift = checkDrift(planningDir);
127
+ if (drift.drifted) {
128
+ warnings.push({
129
+ code: 'planning_state_drift',
130
+ message: `Planning state has drifted since the last recorded session: ${drift.details.join('; ')}`,
131
+ artifacts: ['.planning/ROADMAP.md', '.planning/SPEC.md', '.planning/config.json'],
132
+ });
133
+ }
134
+ }
135
+
136
+ return {
137
+ surface,
138
+ phase: normalizedPhase,
139
+ classification: policy.classification,
140
+ ownedWrites: policy.ownedWrites,
141
+ explicitLifecycleMutation: policy.explicitLifecycleMutation,
142
+ closureEvidence: describeEvidenceSurface(surface),
143
+ mutationRequest: expectsMutation,
144
+ allowed: blockers.length === 0,
145
+ status: blockers.length === 0 ? 'allowed' : 'blocked',
146
+ reason: blockers[0]?.code ?? null,
147
+ blockers,
148
+ warnings,
149
+ lifecycle: {
150
+ currentMilestone: lifecycle.currentMilestone,
151
+ currentPhase: lifecycle.currentPhase ? lifecycle.currentPhase.number : null,
152
+ nextPhase: lifecycle.nextPhase ? lifecycle.nextPhase.number : null,
153
+ counts: lifecycle.counts,
154
+ },
155
+ };
156
+ }
157
+
158
+ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
159
+ const blockers = [];
160
+ const phaseEntry = lifecycle.phases.find((phase) => phase.number === phaseToken);
161
+ if (!phaseEntry) {
162
+ blockers.push(
163
+ blocker(
164
+ 'missing_phase',
165
+ `Phase ${phaseToken} was not found in the active roadmap.`,
166
+ ['.planning/ROADMAP.md']
167
+ )
168
+ );
169
+ return blockers;
170
+ }
171
+
172
+ const planArtifacts = lifecycle.phaseArtifacts.filter((artifact) => artifact.phaseToken === phaseToken && artifact.kind === 'plan');
173
+ const summaryArtifacts = lifecycle.phaseArtifacts.filter((artifact) => artifact.phaseToken === phaseToken && artifact.kind === 'summary');
174
+ const pendingPlans = planArtifacts.filter(
175
+ (artifact) => !summaryArtifacts.some((candidate) => candidate.dir === artifact.dir && candidate.baseId === artifact.baseId)
176
+ );
177
+
178
+ if (surface === 'execute') {
179
+ if (planArtifacts.length === 0) {
180
+ blockers.push(
181
+ blocker(
182
+ 'missing_plan',
183
+ `Phase ${phaseToken} cannot execute because no PLAN artifact exists.`,
184
+ ['.planning/phases/']
185
+ )
186
+ );
187
+ } else if (pendingPlans.length === 0) {
188
+ blockers.push(
189
+ blocker(
190
+ 'no_pending_plan',
191
+ `Phase ${phaseToken} has no pending PLAN artifacts left to execute.`,
192
+ planArtifacts.map((artifact) => artifact.displayPath)
193
+ )
194
+ );
195
+ }
196
+ }
197
+
198
+ if (surface === 'verify') {
199
+ if (planArtifacts.length === 0) {
200
+ blockers.push(
201
+ blocker(
202
+ 'missing_plan',
203
+ `Phase ${phaseToken} cannot be verified because no PLAN artifact exists.`,
204
+ ['.planning/phases/']
205
+ )
206
+ );
207
+ }
208
+ if (summaryArtifacts.length === 0) {
209
+ blockers.push(
210
+ blocker(
211
+ 'missing_summary',
212
+ `Phase ${phaseToken} cannot be verified because no SUMMARY artifact exists yet.`,
213
+ ['.planning/phases/']
214
+ )
215
+ );
216
+ }
217
+ }
218
+
219
+ return blockers;
220
+ }
221
+
222
+ function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
223
+ const blockers = [];
224
+ if (!lifecycle.currentMilestone.version) {
225
+ blockers.push(blocker('missing_milestone', 'No active or retained milestone could be derived from ROADMAP.md.', ['.planning/ROADMAP.md']));
226
+ return blockers;
227
+ }
228
+
229
+ if (lifecycle.currentMilestone.archiveState === 'archived') {
230
+ blockers.push(
231
+ blocker(
232
+ allowArchivedBlocker ? 'milestone_already_archived' : 'milestone_already_archived',
233
+ `Milestone ${lifecycle.currentMilestone.version} is already archived-with-ROADMAP.md evidence.`,
234
+ ['.planning/ROADMAP.md', '.planning/MILESTONES.md']
235
+ )
236
+ );
237
+ }
238
+
239
+ if (lifecycle.counts.total === 0) {
240
+ blockers.push(blocker('missing_phases', 'No active milestone phases were found in ROADMAP.md.', ['.planning/ROADMAP.md']));
241
+ } else if (lifecycle.counts.completed !== lifecycle.counts.total) {
242
+ blockers.push(
243
+ blocker(
244
+ 'incomplete_phases',
245
+ `Milestone ${lifecycle.currentMilestone.version} still has incomplete phases (${lifecycle.counts.completed}/${lifecycle.counts.total} complete).`,
246
+ ['.planning/ROADMAP.md']
247
+ )
248
+ );
249
+ }
250
+
251
+ const phasesMissingVerification = lifecycle.phases
252
+ .filter((phase) => phase.status === 'done')
253
+ .filter((phase) => !phase.artifacts.some((artifact) => artifact.kind === 'verification'))
254
+ .map((phase) => phase.number);
255
+
256
+ if (phasesMissingVerification.length > 0) {
257
+ blockers.push(
258
+ blocker(
259
+ 'missing_verification',
260
+ `Completed phases are missing VERIFICATION artifacts (${phasesMissingVerification.join(', ')}).`,
261
+ ['.planning/phases/']
262
+ )
263
+ );
264
+ }
265
+
266
+ return blockers;
267
+ }
268
+
269
+ function buildCompletionBlockers(planningDir, lifecycle) {
270
+ const auditPath = join(planningDir, `${lifecycle.currentMilestone.version}-MILESTONE-AUDIT.md`);
271
+ if (!existsSync(auditPath)) {
272
+ return [
273
+ blocker(
274
+ 'missing_milestone_audit',
275
+ `Milestone ${lifecycle.currentMilestone.version} cannot be completed without a milestone audit artifact.`,
276
+ [auditPath]
277
+ ),
278
+ ];
279
+ }
280
+
281
+ const auditContent = readFileSync(auditPath, 'utf-8');
282
+ const statusMatch = auditContent.match(/^status:\s*(.+)$/m);
283
+ if (!statusMatch || statusMatch[1].trim() !== 'passed') {
284
+ return [
285
+ blocker(
286
+ 'audit_not_passed',
287
+ `Milestone ${lifecycle.currentMilestone.version} requires a passed audit before completion.`,
288
+ [auditPath]
289
+ ),
290
+ ];
291
+ }
292
+
293
+ return [];
294
+ }
295
+
296
+ function blocker(code, message, artifacts) {
297
+ return { code, message, artifacts };
298
+ }
299
+
300
+ export function cmdLifecyclePreflight(...args) {
301
+ const cwd = process.cwd();
302
+ const planningDir = join(cwd, '.planning');
303
+ const [surface, maybePhase, ...rest] = args;
304
+
305
+ if (!surface) {
306
+ console.error('Usage: gsdd lifecycle-preflight <surface> [phase] [--expects-mutation <none|phase-status>]');
307
+ process.exitCode = 1;
308
+ return;
309
+ }
310
+
311
+ let phaseNumber = maybePhase && !maybePhase.startsWith('--') ? maybePhase : null;
312
+ let expectsMutation = 'none';
313
+
314
+ const flagArgs = phaseNumber ? rest : [maybePhase, ...rest].filter(Boolean);
315
+ for (let index = 0; index < flagArgs.length; index += 1) {
316
+ const arg = flagArgs[index];
317
+ if (arg === '--expects-mutation') {
318
+ expectsMutation = flagArgs[index + 1] ?? 'none';
319
+ index += 1;
320
+ }
321
+ }
322
+
323
+ try {
324
+ const result = evaluateLifecyclePreflight({ planningDir, surface, phaseNumber, expectsMutation });
325
+ output(result);
326
+ if (!result.allowed) {
327
+ process.exitCode = 1;
328
+ }
329
+ } catch (error) {
330
+ console.error(error.message);
331
+ process.exitCode = 1;
332
+ }
333
+ }