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