gsdd-cli 0.18.5 → 0.19.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 (101) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +610 -608
  3. package/agents/DISTILLATION.md +421 -421
  4. package/agents/README.md +62 -62
  5. package/agents/approach-explorer.md +370 -361
  6. package/agents/debugger.md +82 -82
  7. package/agents/executor.md +473 -394
  8. package/agents/integration-checker.md +318 -318
  9. package/agents/mapper.md +103 -103
  10. package/agents/planner.md +342 -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 +193 -191
  17. package/bin/adapters/codex.mjs +85 -85
  18. package/bin/adapters/index.mjs +20 -20
  19. package/bin/adapters/opencode.mjs +280 -278
  20. package/bin/gsdd.mjs +123 -116
  21. package/bin/lib/cli-utils.mjs +28 -28
  22. package/bin/lib/evidence-contract.mjs +325 -112
  23. package/bin/lib/file-ops.mjs +186 -144
  24. package/bin/lib/health-truth.mjs +196 -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 +193 -190
  29. package/bin/lib/init.mjs +17 -17
  30. package/bin/lib/lifecycle-preflight.mjs +760 -326
  31. package/bin/lib/lifecycle-state.mjs +356 -267
  32. package/bin/lib/manifest.mjs +116 -114
  33. package/bin/lib/models.mjs +411 -411
  34. package/bin/lib/phase.mjs +365 -358
  35. package/bin/lib/plan-constants.mjs +35 -30
  36. package/bin/lib/provenance.mjs +109 -106
  37. package/bin/lib/rendering.mjs +119 -83
  38. package/bin/lib/runtime-freshness.mjs +214 -214
  39. package/bin/lib/session-fingerprint.mjs +91 -14
  40. package/bin/lib/templates.mjs +225 -224
  41. package/bin/lib/workspace-root.mjs +2 -1
  42. package/distilled/DESIGN.md +2461 -2323
  43. package/distilled/EVIDENCE-INDEX.md +418 -392
  44. package/distilled/README.md +196 -193
  45. package/distilled/SKILL.md +86 -85
  46. package/distilled/templates/agents.block.md +21 -21
  47. package/distilled/templates/agents.md +6 -6
  48. package/distilled/templates/approach.md +272 -232
  49. package/distilled/templates/auth-matrix.md +78 -78
  50. package/distilled/templates/brownfield-change/CHANGE.md +99 -0
  51. package/distilled/templates/brownfield-change/HANDOFF.md +38 -0
  52. package/distilled/templates/brownfield-change/VERIFICATION.md +56 -0
  53. package/distilled/templates/codebase/architecture.md +110 -110
  54. package/distilled/templates/codebase/concerns.md +95 -95
  55. package/distilled/templates/codebase/conventions.md +193 -193
  56. package/distilled/templates/codebase/stack.md +96 -96
  57. package/distilled/templates/delegates/approach-explorer.md +28 -25
  58. package/distilled/templates/delegates/mapper-arch.md +26 -26
  59. package/distilled/templates/delegates/mapper-concerns.md +27 -27
  60. package/distilled/templates/delegates/mapper-quality.md +28 -28
  61. package/distilled/templates/delegates/mapper-tech.md +25 -25
  62. package/distilled/templates/delegates/plan-checker.md +78 -68
  63. package/distilled/templates/delegates/researcher-architecture.md +30 -30
  64. package/distilled/templates/delegates/researcher-features.md +30 -30
  65. package/distilled/templates/delegates/researcher-pitfalls.md +30 -30
  66. package/distilled/templates/delegates/researcher-stack.md +30 -30
  67. package/distilled/templates/delegates/researcher-synthesizer.md +31 -31
  68. package/distilled/templates/research/architecture.md +57 -57
  69. package/distilled/templates/research/features.md +23 -23
  70. package/distilled/templates/research/pitfalls.md +46 -46
  71. package/distilled/templates/research/stack.md +45 -45
  72. package/distilled/templates/research/summary.md +67 -67
  73. package/distilled/templates/roadmap.md +74 -62
  74. package/distilled/templates/spec.md +110 -110
  75. package/distilled/workflows/audit-milestone.md +303 -271
  76. package/distilled/workflows/complete-milestone.md +349 -332
  77. package/distilled/workflows/execute.md +457 -450
  78. package/distilled/workflows/map-codebase.md +253 -253
  79. package/distilled/workflows/new-milestone.md +242 -238
  80. package/distilled/workflows/new-project.md +398 -398
  81. package/distilled/workflows/pause.md +160 -156
  82. package/distilled/workflows/plan-milestone-gaps.md +183 -183
  83. package/distilled/workflows/plan.md +454 -448
  84. package/distilled/workflows/progress.md +227 -223
  85. package/distilled/workflows/quick.md +351 -347
  86. package/distilled/workflows/resume.md +220 -212
  87. package/distilled/workflows/verify-work.md +260 -260
  88. package/distilled/workflows/verify.md +431 -429
  89. package/docs/BROWNFIELD-PROOF.md +95 -95
  90. package/docs/RUNTIME-SUPPORT.md +80 -69
  91. package/docs/USER-GUIDE.md +394 -386
  92. package/docs/VERIFICATION-DISCIPLINE.md +59 -59
  93. package/docs/claude/context-monitor.md +98 -98
  94. package/docs/proof/consumer-node-cli/README.md +37 -37
  95. package/docs/proof/consumer-node-cli/ROADMAP.md +14 -14
  96. package/docs/proof/consumer-node-cli/SPEC.md +17 -17
  97. package/docs/proof/consumer-node-cli/brief.md +9 -9
  98. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -34
  99. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -10
  100. package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -30
  101. package/package.json +62 -61
@@ -1,303 +1,737 @@
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
- import { describeEvidenceSurface } from './evidence-contract.mjs';
4
+ import {
5
+ DELIVERY_POSTURES,
6
+ EVIDENCE_KINDS,
7
+ RELEASE_CLAIM_POSTURES,
8
+ describeEvidenceSurface,
9
+ evaluateReleaseClaimCloseoutContract,
10
+ getEvidenceContract,
11
+ } from './evidence-contract.mjs';
5
12
  import { evaluateLifecycleState, normalizePhaseToken } from './lifecycle-state.mjs';
6
13
  import { checkDrift } from './session-fingerprint.mjs';
7
14
  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
-
15
+
16
+ const SURFACE_POLICIES = {
17
+ progress: {
18
+ classification: 'read_only',
19
+ ownedWrites: [],
20
+ explicitLifecycleMutation: 'none',
21
+ },
22
+ plan: {
23
+ classification: 'owned_write',
24
+ ownedWrites: ['research', 'plan'],
25
+ explicitLifecycleMutation: 'none',
26
+ phaseRequired: true,
27
+ },
28
+ execute: {
29
+ classification: 'owned_write',
30
+ ownedWrites: ['summary'],
31
+ explicitLifecycleMutation: 'phase-status',
32
+ phaseRequired: true,
33
+ },
34
+ verify: {
35
+ classification: 'owned_write',
36
+ ownedWrites: ['verification'],
37
+ explicitLifecycleMutation: 'phase-status',
38
+ phaseRequired: true,
39
+ },
40
+ 'audit-milestone': {
41
+ classification: 'owned_write',
42
+ ownedWrites: ['milestone-audit'],
43
+ explicitLifecycleMutation: 'none',
44
+ },
45
+ 'complete-milestone': {
46
+ classification: 'owned_write',
47
+ ownedWrites: ['milestone-archives', 'milestones-ledger', 'spec', 'roadmap'],
48
+ explicitLifecycleMutation: 'none',
49
+ },
50
+ 'new-milestone': {
51
+ classification: 'owned_write',
52
+ ownedWrites: ['spec', 'roadmap', 'phase-directories'],
53
+ explicitLifecycleMutation: 'none',
54
+ },
55
+ resume: {
56
+ classification: 'owned_write',
57
+ ownedWrites: ['checkpoint-cleanup'],
58
+ explicitLifecycleMutation: 'none',
59
+ },
60
+ };
61
+
62
+ const RELEASE_CONTRADICTION_CHECKS = Object.freeze([
63
+ 'evidence',
64
+ 'public_surface',
65
+ 'runtime',
66
+ 'delivery',
67
+ 'planning_drift',
68
+ 'generated_surface',
69
+ ]);
70
+
71
+ const RELEASE_CONTRADICTION_STATUSES = Object.freeze(['passed', 'failed', 'not_applicable']);
72
+
73
+ export function evaluateLifecyclePreflight({
74
+ planningDir,
75
+ surface,
76
+ phaseNumber = null,
77
+ expectsMutation = 'none',
78
+ } = {}) {
79
+ if (!planningDir) {
80
+ throw new Error('planningDir is required');
81
+ }
82
+
83
+ const policy = SURFACE_POLICIES[surface];
84
+ if (!policy) {
85
+ throw new Error(`Unsupported lifecycle surface: ${surface}`);
86
+ }
87
+
88
+ const lifecycle = evaluateLifecycleState({ planningDir });
89
+ const normalizedPhase = phaseNumber ? normalizePhaseToken(phaseNumber) : null;
90
+ const checkpointPath = join(planningDir, '.continue-here.md');
91
+ const specPath = join(planningDir, 'SPEC.md');
92
+ const milestonesPath = join(planningDir, 'MILESTONES.md');
93
+ const blockers = [];
94
+
95
+ if (!existsSync(planningDir)) {
96
+ blockers.push(blocker('missing_planning_dir', '.planning/ does not exist yet.', ['.planning/']));
97
+ }
98
+
99
+ if (expectsMutation !== 'none' && expectsMutation !== policy.explicitLifecycleMutation) {
100
+ blockers.push(
101
+ blocker(
102
+ 'illegal_lifecycle_mutation',
103
+ `${surface} is classified as ${policy.classification} and cannot mutate lifecycle state via ${expectsMutation}.`,
104
+ []
105
+ )
106
+ );
107
+ }
108
+
109
+ if (policy.phaseRequired && !normalizedPhase) {
110
+ blockers.push(blocker('missing_phase_argument', `${surface} requires an explicit phase number.`, []));
111
+ }
112
+
113
+ if (normalizedPhase) {
114
+ blockers.push(...buildPhaseBlockers({ lifecycle, phaseToken: normalizedPhase, surface }));
115
+ }
116
+
117
+ if (surface === 'audit-milestone') {
118
+ blockers.push(...buildRoadmapAlignmentBlockers(lifecycle));
119
+ blockers.push(...buildAuditBlockers(lifecycle));
120
+ }
121
+
122
+ if (surface === 'complete-milestone') {
123
+ blockers.push(...buildRoadmapAlignmentBlockers(lifecycle));
124
+ blockers.push(...buildAuditBlockers(lifecycle, { allowArchivedBlocker: true }));
125
+ blockers.push(...buildCompletionBlockers(planningDir, lifecycle));
126
+ }
127
+
128
+ if (surface === 'new-milestone') {
129
+ blockers.push(...buildRoadmapAlignmentBlockers(lifecycle));
130
+ if (!existsSync(specPath)) {
131
+ blockers.push(blocker('missing_spec', 'SPEC.md is required before starting a new milestone.', ['.planning/SPEC.md']));
132
+ }
133
+ if (!existsSync(milestonesPath)) {
134
+ blockers.push(blocker('missing_milestones', 'MILESTONES.md is required before starting a new milestone.', ['.planning/MILESTONES.md']));
135
+ }
136
+ if (lifecycle.currentMilestone.version && lifecycle.currentMilestone.archiveState !== 'archived') {
137
+ blockers.push(
138
+ blocker(
139
+ 'active_milestone_in_progress',
140
+ `Milestone ${lifecycle.currentMilestone.version} is still active. Archive or remove the active roadmap before starting the next milestone.`,
141
+ ['.planning/ROADMAP.md']
142
+ )
143
+ );
144
+ }
145
+ }
146
+
147
+ if (surface === 'resume' && !existsSync(checkpointPath) && lifecycle.nonPhaseState !== 'active_brownfield_change') {
148
+ 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']));
149
+ }
150
+
151
+ const warnings = [];
152
+ let planningState = null;
153
+
154
+ if (existsSync(planningDir)) {
155
+ const drift = checkDrift(planningDir);
156
+ planningState = {
157
+ classification: drift.classification,
158
+ drifted: drift.drifted,
159
+ noBaseline: drift.noBaseline,
160
+ details: drift.details,
161
+ files: drift.files,
162
+ };
163
+ if (drift.drifted) {
164
+ const driftNotice = {
165
+ code: 'planning_state_drift',
166
+ message: `${surface} cannot proceed because planning state drifted since the last recorded session: ${drift.details.join('; ')}`,
167
+ artifacts: ['.planning/ROADMAP.md', '.planning/SPEC.md', '.planning/config.json'],
168
+ details: drift.details,
169
+ files: drift.files,
170
+ };
171
+ if (policy.classification === 'owned_write') {
172
+ blockers.push(driftNotice);
173
+ } else {
174
+ warnings.push({
175
+ ...driftNotice,
176
+ message: `Planning state has drifted since the last recorded session: ${drift.details.join('; ')}`,
177
+ });
178
+ }
179
+ }
180
+ }
181
+
182
+ if (lifecycle.phaseStatusAlignment.mismatches.length > 0) {
183
+ warnings.push({
184
+ code: 'roadmap_phase_status_mismatch',
185
+ message: `ROADMAP.md overview/detail phase statuses disagree: ${lifecycle.phaseStatusAlignment.mismatches.join('; ')}`,
186
+ artifacts: ['.planning/ROADMAP.md'],
187
+ });
188
+ }
189
+
190
+ return {
191
+ surface,
192
+ phase: normalizedPhase,
193
+ classification: policy.classification,
194
+ ownedWrites: policy.ownedWrites,
195
+ explicitLifecycleMutation: policy.explicitLifecycleMutation,
196
+ closureEvidence: describeEvidenceSurface(surface),
197
+ mutationRequest: expectsMutation,
198
+ allowed: blockers.length === 0,
199
+ status: blockers.length === 0 ? 'allowed' : 'blocked',
200
+ reason: blockers[0]?.code ?? null,
201
+ blockers,
202
+ warnings,
203
+ planningState,
204
+ lifecycle: {
205
+ currentMilestone: lifecycle.currentMilestone,
206
+ currentPhase: lifecycle.currentPhase ? lifecycle.currentPhase.number : null,
207
+ nextPhase: lifecycle.nextPhase ? lifecycle.nextPhase.number : null,
208
+ counts: lifecycle.counts,
209
+ },
210
+ };
211
+ }
212
+
213
+ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
214
+ const blockers = [];
215
+ const phaseEntry = lifecycle.phases.find((phase) => phase.number === phaseToken);
216
+ if (!phaseEntry) {
217
+ blockers.push(
218
+ blocker(
219
+ 'missing_phase',
220
+ `Phase ${phaseToken} was not found in the active roadmap.`,
221
+ ['.planning/ROADMAP.md']
222
+ )
223
+ );
224
+ return blockers;
225
+ }
226
+
227
+ const planArtifacts = lifecycle.phaseArtifacts.filter((artifact) => artifact.phaseToken === phaseToken && artifact.kind === 'plan');
228
+ const summaryArtifacts = lifecycle.phaseArtifacts.filter((artifact) => artifact.phaseToken === phaseToken && artifact.kind === 'summary');
229
+ const pendingPlans = planArtifacts.filter(
230
+ (artifact) => !summaryArtifacts.some((candidate) => candidate.dir === artifact.dir && candidate.baseId === artifact.baseId)
231
+ );
232
+
233
+ if (surface === 'execute') {
234
+ if (planArtifacts.length === 0) {
235
+ blockers.push(
236
+ blocker(
237
+ 'missing_plan',
238
+ `Phase ${phaseToken} cannot execute because no PLAN artifact exists.`,
239
+ ['.planning/phases/']
240
+ )
241
+ );
242
+ } else if (pendingPlans.length === 0) {
243
+ blockers.push(
244
+ blocker(
245
+ 'no_pending_plan',
246
+ `Phase ${phaseToken} has no pending PLAN artifacts left to execute.`,
247
+ planArtifacts.map((artifact) => artifact.displayPath)
248
+ )
249
+ );
250
+ }
251
+ }
252
+
253
+ if (surface === 'plan' && phaseEntry.status === 'done') {
254
+ blockers.push(
255
+ blocker(
256
+ 'phase_already_complete',
257
+ `Phase ${phaseToken} is already complete and should not be planned again.`,
258
+ ['.planning/ROADMAP.md']
259
+ )
260
+ );
261
+ }
262
+
263
+ if (surface === 'verify') {
264
+ if (planArtifacts.length === 0) {
265
+ blockers.push(
266
+ blocker(
267
+ 'missing_plan',
268
+ `Phase ${phaseToken} cannot be verified because no PLAN artifact exists.`,
269
+ ['.planning/phases/']
270
+ )
271
+ );
272
+ }
273
+ if (summaryArtifacts.length === 0) {
274
+ blockers.push(
275
+ blocker(
276
+ 'missing_summary',
277
+ `Phase ${phaseToken} cannot be verified because no SUMMARY artifact exists yet.`,
278
+ ['.planning/phases/']
279
+ )
280
+ );
281
+ }
282
+ }
283
+
284
+ return blockers;
285
+ }
286
+
287
+ function buildRoadmapAlignmentBlockers(lifecycle) {
288
+ if (lifecycle.phaseStatusAlignment.mismatches.length === 0) return [];
289
+ return [
290
+ blocker(
291
+ 'roadmap_phase_status_mismatch',
292
+ `ROADMAP.md overview/detail phase statuses disagree: ${lifecycle.phaseStatusAlignment.mismatches.join('; ')}`,
293
+ ['.planning/ROADMAP.md']
294
+ ),
295
+ ];
296
+ }
297
+
298
+ function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
299
+ const blockers = [];
300
+ if (!lifecycle.currentMilestone.version) {
301
+ blockers.push(blocker('missing_milestone', 'No active or retained milestone could be derived from ROADMAP.md.', ['.planning/ROADMAP.md']));
302
+ return blockers;
303
+ }
304
+
305
+ if (lifecycle.currentMilestone.archiveState === 'archived') {
306
+ blockers.push(
307
+ blocker(
308
+ allowArchivedBlocker ? 'milestone_already_archived' : 'milestone_already_archived',
309
+ `Milestone ${lifecycle.currentMilestone.version} is already archived-with-ROADMAP.md evidence.`,
310
+ ['.planning/ROADMAP.md', '.planning/MILESTONES.md']
311
+ )
312
+ );
313
+ }
314
+
315
+ if (lifecycle.counts.total === 0) {
316
+ blockers.push(blocker('missing_phases', 'No active milestone phases were found in ROADMAP.md.', ['.planning/ROADMAP.md']));
317
+ } else if (lifecycle.counts.completed !== lifecycle.counts.total) {
318
+ blockers.push(
319
+ blocker(
320
+ 'incomplete_phases',
321
+ `Milestone ${lifecycle.currentMilestone.version} still has incomplete phases (${lifecycle.counts.completed}/${lifecycle.counts.total} complete).`,
322
+ ['.planning/ROADMAP.md']
323
+ )
324
+ );
325
+ }
326
+
327
+ const phasesMissingVerification = lifecycle.phases
328
+ .filter((phase) => phase.status === 'done')
329
+ .filter((phase) => !phase.artifacts.some((artifact) => artifact.kind === 'verification'))
330
+ .map((phase) => phase.number);
331
+
332
+ if (phasesMissingVerification.length > 0) {
333
+ blockers.push(
334
+ blocker(
335
+ 'missing_verification',
336
+ `Completed phases are missing VERIFICATION artifacts (${phasesMissingVerification.join(', ')}).`,
337
+ ['.planning/phases/']
338
+ )
339
+ );
340
+ }
341
+
342
+ return blockers;
343
+ }
344
+
345
+ function buildCompletionBlockers(planningDir, lifecycle) {
346
+ const auditPath = join(planningDir, `${lifecycle.currentMilestone.version}-MILESTONE-AUDIT.md`);
347
+ if (!existsSync(auditPath)) {
348
+ return [
349
+ blocker(
350
+ 'missing_milestone_audit',
351
+ `Milestone ${lifecycle.currentMilestone.version} cannot be completed without a milestone audit artifact.`,
352
+ [auditPath]
353
+ ),
354
+ ];
355
+ }
356
+
357
+ const auditContent = readFileSync(auditPath, 'utf-8');
358
+ const auditFrontmatter = extractFrontmatter(auditContent);
359
+ const auditStatus = readTopLevelScalar(auditFrontmatter || auditContent, 'status');
360
+ if (auditStatus !== 'passed') {
361
+ return [
362
+ blocker(
363
+ 'audit_not_passed',
364
+ `Milestone ${lifecycle.currentMilestone.version} requires a passed audit before completion.`,
365
+ [auditPath]
366
+ ),
367
+ ];
368
+ }
369
+
370
+ const releaseContractBlockers = buildReleaseClaimCompletionBlockers(auditContent, auditPath);
371
+ if (releaseContractBlockers.length > 0) return releaseContractBlockers;
372
+
373
+ return [];
374
+ }
375
+
376
+ function buildReleaseClaimCompletionBlockers(auditContent, auditPath) {
377
+ const frontmatter = extractFrontmatter(auditContent);
378
+ const deliveryPosture = readTopLevelScalar(frontmatter, 'delivery_posture');
379
+ const releaseClaimPosture = readTopLevelScalar(frontmatter, 'release_claim_posture');
380
+ const evidenceBlock = extractYamlBlock(frontmatter, 'evidence_contract');
381
+ const releaseBlock = extractYamlBlock(frontmatter, 'release_claim_contract');
382
+ const missing = [];
383
+
384
+ if (!deliveryPosture) missing.push('delivery_posture');
385
+ if (!releaseClaimPosture) missing.push('release_claim_posture');
386
+ if (!evidenceBlock) missing.push('evidence_contract');
387
+ if (!releaseBlock) missing.push('release_claim_contract');
388
+
389
+ if (missing.length > 0) {
390
+ return [blocker(
391
+ 'missing_release_claim_contract',
392
+ `Milestone audit is missing release closeout metadata (${missing.join(', ')}). Re-run audit before completion.`,
393
+ [auditPath]
394
+ )];
395
+ }
396
+
397
+ const requiredKinds = readBlockList(evidenceBlock, 'required_kinds');
398
+ const observedKinds = readBlockList(evidenceBlock, 'observed_kinds');
399
+ const missingKinds = readBlockList(evidenceBlock, 'missing_kinds');
400
+ const unsupportedClaims = readBlockList(releaseBlock, 'unsupported_claims');
401
+ const waivedKinds = readBlockList(releaseBlock, 'waivers');
402
+ const deferrals = readBlockList(releaseBlock, 'deferrals');
403
+ const contradictionChecks = readNestedStatusBlock(releaseBlock, 'contradiction_checks');
404
+ const blockers = [];
405
+ const invalidEvidenceKinds = [
406
+ ...findInvalidEvidenceKinds('required_kinds', requiredKinds),
407
+ ...findInvalidEvidenceKinds('observed_kinds', observedKinds),
408
+ ...findInvalidEvidenceKinds('missing_kinds', missingKinds),
409
+ ...findInvalidEvidenceKinds('waivers', waivedKinds),
410
+ ];
411
+
412
+ if (requiredKinds.length === 0 && observedKinds.length === 0) {
413
+ blockers.push(blocker(
414
+ 'missing_release_evidence_contract',
415
+ 'Milestone audit evidence_contract must include required_kinds and observed_kinds before completion.',
416
+ [auditPath]
417
+ ));
418
+ }
419
+
420
+ if (!DELIVERY_POSTURES.includes(deliveryPosture)) {
421
+ blockers.push(blocker(
422
+ 'invalid_delivery_posture',
423
+ `Milestone audit has invalid delivery_posture (${deliveryPosture}). Re-run audit before completion.`,
424
+ [auditPath]
425
+ ));
426
+ }
427
+
428
+ if (!RELEASE_CLAIM_POSTURES.includes(releaseClaimPosture)) {
429
+ blockers.push(blocker(
430
+ 'invalid_release_claim_posture',
431
+ `Milestone audit has invalid release_claim_posture (${releaseClaimPosture}). Re-run audit before completion.`,
432
+ [auditPath]
433
+ ));
434
+ }
435
+
436
+ if (invalidEvidenceKinds.length > 0) {
437
+ blockers.push(blocker(
438
+ 'invalid_release_evidence_kinds',
439
+ `Milestone audit has invalid release evidence kind values (${invalidEvidenceKinds.join(', ')}). Supported values are ${EVIDENCE_KINDS.join(', ')}.`,
440
+ [auditPath]
441
+ ));
442
+ }
443
+
444
+ const missingContradictionChecks = RELEASE_CONTRADICTION_CHECKS.filter((name) => !(name in contradictionChecks));
445
+ const unknownContradictionChecks = Object.keys(contradictionChecks)
446
+ .filter((name) => !RELEASE_CONTRADICTION_CHECKS.includes(name));
447
+ const invalidContradictionChecks = Object.entries(contradictionChecks)
448
+ .filter(([, status]) => !RELEASE_CONTRADICTION_STATUSES.includes(status))
449
+ .map(([name]) => name);
450
+
451
+ if (missingContradictionChecks.length > 0) {
452
+ blockers.push(blocker(
453
+ 'missing_release_contradiction_checks',
454
+ `Milestone audit release_claim_contract.contradiction_checks is missing required checks (${missingContradictionChecks.join(', ')}).`,
455
+ [auditPath]
456
+ ));
457
+ }
458
+
459
+ if (invalidContradictionChecks.length > 0) {
460
+ blockers.push(blocker(
461
+ 'invalid_release_contradiction_checks',
462
+ `Milestone audit release_claim_contract.contradiction_checks has invalid statuses (${invalidContradictionChecks.join(', ')}).`,
463
+ [auditPath]
464
+ ));
465
+ }
466
+
467
+ if (unknownContradictionChecks.length > 0) {
468
+ blockers.push(blocker(
469
+ 'unknown_release_contradiction_checks',
470
+ `Milestone audit release_claim_contract.contradiction_checks has unknown checks (${unknownContradictionChecks.join(', ')}). Supported checks are ${RELEASE_CONTRADICTION_CHECKS.join(', ')}.`,
471
+ [auditPath]
472
+ ));
473
+ }
474
+
475
+ if (!DELIVERY_POSTURES.includes(deliveryPosture) || !RELEASE_CLAIM_POSTURES.includes(releaseClaimPosture)) {
476
+ return blockers;
477
+ }
478
+
479
+ const releaseEvaluation = evaluateReleaseClaimCloseoutContract({
480
+ surface: 'complete-milestone',
481
+ deliveryPosture,
482
+ releaseClaimPosture,
483
+ observedKinds,
484
+ waivedKinds,
485
+ unsupportedClaims,
486
+ deferrals,
487
+ contradictionChecks,
488
+ });
489
+
490
+ if (DELIVERY_POSTURES.includes(deliveryPosture)) {
491
+ const evidenceContract = getEvidenceContract('complete-milestone', deliveryPosture);
492
+ const enforcedRequiredKinds = [...new Set([...evidenceContract.requiredKinds, ...releaseEvaluation.requiredKinds])];
493
+ const undeclaredRequiredKinds = enforcedRequiredKinds.filter((kind) => !requiredKinds.includes(kind));
494
+ const recomputedMissingKinds = enforcedRequiredKinds.filter((kind) => !observedKinds.includes(kind));
495
+
496
+ if (undeclaredRequiredKinds.length > 0) {
497
+ blockers.push(blocker(
498
+ 'invalid_release_evidence_contract',
499
+ `Milestone audit evidence_contract.required_kinds omits required closeout evidence (${undeclaredRequiredKinds.join(', ')}).`,
500
+ [auditPath]
501
+ ));
502
+ }
503
+
504
+ if (recomputedMissingKinds.length > 0) {
505
+ blockers.push(blocker(
506
+ 'missing_required_release_evidence',
507
+ `Milestone audit observed evidence is missing required closeout kinds (${recomputedMissingKinds.join(', ')}).`,
508
+ [auditPath]
509
+ ));
510
+ }
511
+ }
512
+
513
+ if (missingKinds.length > 0) {
514
+ blockers.push(blocker(
515
+ 'missing_required_release_evidence',
516
+ `Milestone audit is missing required evidence kinds for closeout (${missingKinds.join(', ')}).`,
517
+ [auditPath]
518
+ ));
519
+ }
520
+
521
+ if (releaseEvaluation.invalidWaivers.length > 0) {
522
+ blockers.push(blocker(
523
+ 'invalid_release_waivers',
524
+ `Milestone audit has invalid waivers for missing required evidence (${releaseEvaluation.invalidWaivers.join(', ')}).`,
525
+ [auditPath]
526
+ ));
527
+ }
528
+ if (releaseEvaluation.blockers.some((releaseBlocker) => releaseBlocker.code === 'incompatible_release_claim_posture')) {
529
+ blockers.push(blocker(
530
+ 'incompatible_release_claim_posture',
531
+ `Milestone audit release_claim_posture (${releaseClaimPosture}) is incompatible with delivery_posture (${deliveryPosture}).`,
532
+ [auditPath]
533
+ ));
534
+ }
535
+ if (releaseEvaluation.unresolvedUnsupportedClaims.length > 0) {
536
+ blockers.push(blocker(
537
+ 'unsupported_release_claims',
538
+ `Milestone audit has unsupported release claims without downgrade or deferral (${releaseEvaluation.unresolvedUnsupportedClaims.join(', ')}).`,
539
+ [auditPath]
540
+ ));
541
+ }
542
+ if (releaseEvaluation.failedContradictionChecks.length > 0) {
543
+ blockers.push(blocker(
544
+ 'failed_release_contradiction_checks',
545
+ `Milestone audit has failed release contradiction checks (${releaseEvaluation.failedContradictionChecks.join(', ')}).`,
546
+ [auditPath]
547
+ ));
548
+ }
549
+
550
+ return blockers;
551
+ }
552
+
553
+ function extractFrontmatter(content) {
554
+ const match = String(content || '').replace(/\r\n/g, '\n').match(/^---\n([\s\S]*?)\n---/);
555
+ return match ? match[1] : '';
556
+ }
557
+
558
+ function readTopLevelScalar(frontmatter, key) {
559
+ const match = String(frontmatter || '').match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
560
+ return match ? cleanYamlValue(match[1]) : null;
561
+ }
562
+
563
+ function extractYamlBlock(frontmatter, key) {
564
+ const lines = String(frontmatter || '').replace(/\r\n/g, '\n').split('\n');
565
+ const startIndex = lines.findIndex((line) => new RegExp(`^${key}:\\s*(?:#.*)?$`).test(line.trim()));
566
+ if (startIndex === -1) return '';
567
+
568
+ const collected = [];
569
+ for (const line of lines.slice(startIndex + 1)) {
570
+ if (/^[A-Za-z0-9_-]+:\s*/.test(line)) break;
571
+ collected.push(line);
572
+ }
573
+ return collected.join('\n');
574
+ }
575
+
576
+ function readBlockList(block, key) {
577
+ const lines = String(block || '').replace(/\r\n/g, '\n').split('\n');
578
+ const startIndex = lines.findIndex((line) => new RegExp(`^\\s+${key}:`).test(line));
579
+ if (startIndex === -1) return [];
580
+ const baseIndent = lines[startIndex].match(/^\s*/)[0].length;
581
+
582
+ const inline = lines[startIndex].match(/^\s+[^:]+:\s*\[([^\]]*)\]/);
583
+ if (inline) return splitInlineList(inline[1]);
584
+
585
+ const collected = [];
586
+ for (const line of lines.slice(startIndex + 1)) {
587
+ const indent = line.match(/^\s*/)[0].length;
588
+ if (line.trim() && indent <= baseIndent && /^\s*[A-Za-z0-9_-]+:\s*/.test(line)) break;
589
+ collected.push(line);
590
+ }
591
+
592
+ return parseYamlListItems(collected, baseIndent)
593
+ .map((item) => item.join(' '))
594
+ .map(cleanYamlValue);
595
+ }
596
+
597
+ function parseYamlListItems(lines, baseIndent) {
598
+ const items = [];
599
+ let current = null;
600
+ let itemIndent = null;
601
+
602
+ for (const line of lines) {
603
+ const match = line.match(/^(\s*)-\s*(.+?)\s*$/);
604
+ const indent = line.match(/^\s*/)[0].length;
605
+
606
+ if (match && indent > baseIndent && (itemIndent === null || indent === itemIndent)) {
607
+ if (current) items.push(current);
608
+ current = [match[2]];
609
+ itemIndent = indent;
610
+ continue;
611
+ }
612
+
613
+ if (current && line.trim()) {
614
+ current.push(line.trim());
615
+ }
616
+ }
617
+
618
+ if (current) items.push(current);
619
+ return items;
620
+ }
621
+
622
+ function findInvalidEvidenceKinds(field, kinds) {
623
+ return kinds
624
+ .filter((kind) => !EVIDENCE_KINDS.includes(kind))
625
+ .map((kind) => `${field}: ${kind}`);
626
+ }
627
+
628
+ function readNestedStatusBlock(block, key) {
629
+ const nested = extractIndentedBlock(block, key);
630
+ const statuses = {};
631
+ for (const line of nested.split('\n')) {
632
+ const match = line.match(/^\s+([A-Za-z0-9_-]+):\s*(.+)$/);
633
+ if (match) statuses[match[1]] = cleanYamlValue(match[2]);
634
+ }
635
+ return statuses;
636
+ }
637
+
638
+ function extractIndentedBlock(block, key) {
639
+ const lines = String(block || '').replace(/\r\n/g, '\n').split('\n');
640
+ const startIndex = lines.findIndex((line) => new RegExp(`^\\s+${key}:`).test(line));
641
+ if (startIndex === -1) return '';
642
+ const baseIndent = lines[startIndex].match(/^\s*/)[0].length;
643
+
644
+ const collected = [];
645
+ for (const line of lines.slice(startIndex + 1)) {
646
+ const indent = line.match(/^\s*/)[0].length;
647
+ if (line.trim() && indent <= baseIndent && /^\s*[A-Za-z0-9_-]+:\s*/.test(line)) break;
648
+ collected.push(line);
649
+ }
650
+ return collected.join('\n');
651
+ }
652
+
653
+ function splitInlineList(value) {
654
+ return splitCommaAware(value)
655
+ .map(cleanYamlValue)
656
+ .filter(Boolean);
657
+ }
658
+
659
+ function splitCommaAware(value) {
660
+ const items = [];
661
+ let current = '';
662
+ let quote = null;
663
+ let escaped = false;
664
+
665
+ for (const char of String(value || '')) {
666
+ if (escaped) {
667
+ current += char;
668
+ escaped = false;
669
+ continue;
670
+ }
671
+ if (char === '\\' && quote) {
672
+ current += char;
673
+ escaped = true;
674
+ continue;
675
+ }
676
+ if ((char === '"' || char === "'") && (!quote || quote === char)) {
677
+ quote = quote ? null : char;
678
+ current += char;
679
+ continue;
680
+ }
681
+ if (char === ',' && !quote) {
682
+ items.push(current);
683
+ current = '';
684
+ continue;
685
+ }
686
+ current += char;
687
+ }
688
+
689
+ items.push(current);
690
+ return items;
691
+ }
692
+
693
+ function cleanYamlValue(value) {
694
+ return stripInlineYamlComment(String(value || ''))
695
+ .trim()
696
+ .replace(/^['"]|['"]$/g, '')
697
+ .trim();
698
+ }
699
+
700
+ function stripInlineYamlComment(value) {
701
+ let current = '';
702
+ let quote = null;
703
+ let escaped = false;
704
+
705
+ for (let index = 0; index < value.length; index += 1) {
706
+ const char = value[index];
707
+ if (escaped) {
708
+ current += char;
709
+ escaped = false;
710
+ continue;
711
+ }
712
+ if (char === '\\' && quote) {
713
+ current += char;
714
+ escaped = true;
715
+ continue;
716
+ }
717
+ if ((char === '"' || char === "'") && (!quote || quote === char)) {
718
+ quote = quote ? null : char;
719
+ current += char;
720
+ continue;
721
+ }
722
+ if (char === '#' && !quote && (index === 0 || /\s/.test(value[index - 1]))) {
723
+ return current.trimEnd();
724
+ }
725
+ current += char;
726
+ }
727
+
728
+ return current;
729
+ }
730
+
731
+ function blocker(code, message, artifacts) {
732
+ return { code, message, artifacts };
733
+ }
734
+
301
735
  export function cmdLifecyclePreflight(...args) {
302
736
  const { args: normalizedArgs, planningDir, invalid, error } = resolveWorkspaceContext(args);
303
737
  if (invalid) {
@@ -306,33 +740,33 @@ export function cmdLifecyclePreflight(...args) {
306
740
  return;
307
741
  }
308
742
  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
- }
743
+
744
+ if (!surface) {
745
+ console.error('Usage: node .planning/bin/gsdd.mjs lifecycle-preflight <surface> [phase] [--expects-mutation <none|phase-status>]');
746
+ process.exitCode = 1;
747
+ return;
748
+ }
749
+
750
+ let phaseNumber = maybePhase && !maybePhase.startsWith('--') ? maybePhase : null;
751
+ let expectsMutation = 'none';
752
+
753
+ const flagArgs = phaseNumber ? rest : [maybePhase, ...rest].filter(Boolean);
754
+ for (let index = 0; index < flagArgs.length; index += 1) {
755
+ const arg = flagArgs[index];
756
+ if (arg === '--expects-mutation') {
757
+ expectsMutation = flagArgs[index + 1] ?? 'none';
758
+ index += 1;
759
+ }
760
+ }
761
+
762
+ try {
763
+ const result = evaluateLifecyclePreflight({ planningDir, surface, phaseNumber, expectsMutation });
764
+ output(result);
765
+ if (!result.allowed) {
766
+ process.exitCode = 1;
767
+ }
768
+ } catch (error) {
769
+ console.error(error.message);
770
+ process.exitCode = 1;
771
+ }
772
+ }