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