gsdd-cli 0.27.0 → 0.29.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 (84) hide show
  1. package/README.md +53 -22
  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 +20 -20
  6. package/agents/integration-checker.md +2 -2
  7. package/agents/planner.md +10 -26
  8. package/agents/researcher.md +2 -2
  9. package/agents/roadmapper.md +6 -6
  10. package/agents/synthesizer.md +18 -18
  11. package/agents/verifier.md +3 -3
  12. package/bin/adapters/agents.mjs +3 -3
  13. package/bin/adapters/claude.mjs +16 -14
  14. package/bin/adapters/opencode.mjs +15 -12
  15. package/bin/gsdd.mjs +16 -13
  16. package/bin/lib/{models.mjs → config.mjs} +23 -16
  17. package/bin/lib/control-map.mjs +17 -488
  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 +11 -30
  23. package/bin/lib/lifecycle-preflight.mjs +97 -410
  24. package/bin/lib/lifecycle-state.mjs +2 -1
  25. package/bin/lib/next.mjs +243 -20
  26. package/bin/lib/phase.mjs +706 -280
  27. package/bin/lib/plan-constants.mjs +0 -5
  28. package/bin/lib/rendering.mjs +64 -44
  29. package/bin/lib/runtime-freshness.mjs +18 -15
  30. package/bin/lib/state-dir.mjs +45 -0
  31. package/bin/lib/templates.mjs +59 -22
  32. package/bin/lib/work-context.mjs +12 -1
  33. package/bin/lib/workflows.mjs +0 -1
  34. package/bin/lib/workspace-root.mjs +11 -6
  35. package/distilled/DESIGN.md +89 -31
  36. package/distilled/EVIDENCE-INDEX.md +18 -5
  37. package/distilled/README.md +23 -33
  38. package/distilled/SKILL.md +9 -10
  39. package/distilled/templates/agents.block.md +5 -5
  40. package/distilled/templates/approach.md +3 -3
  41. package/distilled/templates/auth-matrix.md +2 -2
  42. package/distilled/templates/brownfield-change/CHANGE.md +1 -1
  43. package/distilled/templates/delegates/approach-explorer.md +5 -5
  44. package/distilled/templates/delegates/mapper-arch.md +3 -3
  45. package/distilled/templates/delegates/mapper-concerns.md +4 -4
  46. package/distilled/templates/delegates/mapper-quality.md +3 -3
  47. package/distilled/templates/delegates/mapper-tech.md +3 -3
  48. package/distilled/templates/delegates/plan-checker.md +18 -19
  49. package/distilled/templates/delegates/researcher-architecture.md +3 -3
  50. package/distilled/templates/delegates/researcher-features.md +3 -3
  51. package/distilled/templates/delegates/researcher-pitfalls.md +3 -3
  52. package/distilled/templates/delegates/researcher-stack.md +3 -3
  53. package/distilled/templates/delegates/researcher-synthesizer.md +7 -7
  54. package/distilled/templates/research/architecture.md +1 -1
  55. package/distilled/templates/research/pitfalls.md +1 -1
  56. package/distilled/templates/research/stack.md +1 -1
  57. package/distilled/templates/roadmap.md +4 -4
  58. package/distilled/templates/spec.md +2 -2
  59. package/distilled/templates/ui-proof.md +81 -181
  60. package/distilled/workflows/audit-milestone.md +23 -23
  61. package/distilled/workflows/complete-milestone.md +35 -35
  62. package/distilled/workflows/execute.md +34 -35
  63. package/distilled/workflows/map-codebase.md +30 -30
  64. package/distilled/workflows/new-milestone.md +18 -18
  65. package/distilled/workflows/new-project.md +45 -45
  66. package/distilled/workflows/pause.md +15 -15
  67. package/distilled/workflows/plan.md +106 -114
  68. package/distilled/workflows/progress.md +40 -39
  69. package/distilled/workflows/quick.md +49 -50
  70. package/distilled/workflows/resume.md +23 -22
  71. package/distilled/workflows/verify-work.md +7 -7
  72. package/distilled/workflows/verify.md +26 -26
  73. package/docs/BROWNFIELD-PROOF.md +1 -1
  74. package/docs/RUNTIME-SUPPORT.md +13 -13
  75. package/docs/USER-GUIDE.md +26 -21
  76. package/docs/claude/context-monitor.md +1 -1
  77. package/docs/proof/consumer-node-cli/README.md +1 -1
  78. package/package.json +3 -3
  79. package/bin/lib/closeout-report.mjs +0 -318
  80. package/bin/lib/evidence-contract.mjs +0 -325
  81. package/bin/lib/provenance.mjs +0 -390
  82. package/bin/lib/session-fingerprint.mjs +0 -223
  83. package/bin/lib/ui-proof.mjs +0 -1007
  84. package/distilled/workflows/plan-milestone-gaps.md +0 -204
@@ -1,17 +1,8 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'fs';
2
- import { join, resolve } from 'path';
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,6 @@ 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']);
79
54
  const WORK_PHASE_LINE_RE = /^\s*[-*]\s*\[([ x-])\]\s*\*\*Phase\s+(\d+(?:\.\d+)*[A-Za-z]?):\s*(.+?)\*\*/i;
80
55
 
81
56
  export function evaluateLifecyclePreflight({
@@ -96,18 +71,25 @@ export function evaluateLifecyclePreflight({
96
71
 
97
72
  const lifecycle = evaluateLifecycleState({ planningDir });
98
73
  const normalizedPhase = phaseNumber ? normalizePhaseToken(phaseNumber) : null;
74
+ const usesBrownfieldAuthority = surface === 'plan' && normalizedPhase === 'brownfield-change';
75
+ const usesPlanAmendAuthority = surface === 'plan' && normalizedPhase === 'amend';
99
76
  const workMilestone = normalizedPhase ? evaluateWorkMilestoneState({ planningDir, phaseToken: normalizedPhase }) : null;
100
77
  const checkpointPath = join(planningDir, '.continue-here.md');
78
+ const stateLabel = createStateLabeler(planningDir);
101
79
  const resumeWorkCheckpoint = surface === 'resume'
102
80
  ? evaluateResumeWorkCheckpoint({ planningDir, checkpointPath })
103
81
  : null;
104
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;
105
87
  const specPath = join(planningDir, 'SPEC.md');
106
88
  const milestonesPath = join(planningDir, 'MILESTONES.md');
107
89
  const blockers = [];
108
90
 
109
91
  if (!existsSync(planningDir)) {
110
- 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('.')]));
111
93
  }
112
94
 
113
95
  if (expectsMutation !== 'none' && expectsMutation !== policy.explicitLifecycleMutation) {
@@ -124,83 +106,72 @@ export function evaluateLifecyclePreflight({
124
106
  blockers.push(blocker('missing_phase_argument', `${surface} requires an explicit phase number.`, []));
125
107
  }
126
108
 
127
- if (normalizedPhase) {
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) {
128
130
  blockers.push(
129
131
  ...(usesWorkAuthority
130
132
  ? buildWorkPhaseBlockers({ workMilestone, phaseToken: normalizedPhase, surface })
131
- : buildPhaseBlockers({ lifecycle, phaseToken: normalizedPhase, surface }))
133
+ : buildPhaseBlockers({ lifecycle, phaseToken: normalizedPhase, surface, stateLabel }))
132
134
  );
133
135
  }
134
136
 
135
137
  if (surface === 'audit-milestone') {
136
- blockers.push(...buildRoadmapAlignmentBlockers(lifecycle));
137
- blockers.push(...buildAuditBlockers(lifecycle));
138
+ blockers.push(...buildRoadmapAlignmentBlockers(lifecycle, stateLabel));
139
+ blockers.push(...buildAuditBlockers(lifecycle, { stateLabel }));
138
140
  }
139
141
 
140
142
  if (surface === 'complete-milestone') {
141
- blockers.push(...buildRoadmapAlignmentBlockers(lifecycle));
142
- blockers.push(...buildAuditBlockers(lifecycle, { allowArchivedBlocker: true }));
143
+ blockers.push(...buildRoadmapAlignmentBlockers(lifecycle, stateLabel));
144
+ blockers.push(...buildAuditBlockers(lifecycle, { allowArchivedBlocker: true, stateLabel }));
143
145
  blockers.push(...buildCompletionBlockers(planningDir, lifecycle));
144
146
  }
145
147
 
146
148
  if (surface === 'new-milestone') {
147
- blockers.push(...buildRoadmapAlignmentBlockers(lifecycle));
149
+ blockers.push(...buildRoadmapAlignmentBlockers(lifecycle, stateLabel));
148
150
  if (!existsSync(specPath)) {
149
- 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')]));
150
152
  }
151
153
  if (!existsSync(milestonesPath)) {
152
- 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')]));
153
155
  }
154
156
  if (lifecycle.currentMilestone.version && lifecycle.currentMilestone.archiveState !== 'archived') {
155
157
  blockers.push(
156
158
  blocker(
157
159
  'active_milestone_in_progress',
158
160
  `Milestone ${lifecycle.currentMilestone.version} is still active. Archive or remove the active roadmap before starting the next milestone.`,
159
- ['.planning/ROADMAP.md']
161
+ [stateLabel('ROADMAP.md')]
160
162
  )
161
163
  );
162
164
  }
163
165
  }
164
166
 
165
167
  if (surface === 'resume' && !existsSync(checkpointPath) && lifecycle.nonPhaseState !== 'active_brownfield_change') {
166
- 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]));
167
171
  }
168
172
 
169
173
  const warnings = [];
170
- let planningState = null;
171
-
172
- if (existsSync(planningDir)) {
173
- const drift = checkDrift(planningDir);
174
- planningState = {
175
- classification: drift.classification,
176
- drifted: drift.drifted,
177
- noBaseline: drift.noBaseline,
178
- details: drift.details,
179
- files: drift.files,
180
- };
181
- if (drift.drifted) {
182
- const driftNotice = {
183
- code: 'planning_state_drift',
184
- message: `${surface} cannot proceed because planning state drifted since the last recorded session: ${drift.details.join('; ')}`,
185
- artifacts: ['.planning/ROADMAP.md', '.planning/SPEC.md', '.planning/config.json'],
186
- details: drift.details,
187
- files: drift.files,
188
- };
189
- if (policy.classification === 'owned_write' && !usesWorkAuthority) {
190
- blockers.push(driftNotice);
191
- } else {
192
- const workWarningContext = resumeWorkCheckpoint
193
- ? 'because the checkpoint points at .work/milestone continuity'
194
- : `by .work/milestone for Phase ${normalizedPhase}`;
195
- warnings.push({
196
- ...driftNotice,
197
- message: usesWorkAuthority
198
- ? `Planning state has drifted since the last recorded session, but ${surface} is using work_milestone authority ${workWarningContext}: ${drift.details.join('; ')}`
199
- : `Planning state has drifted since the last recorded session: ${drift.details.join('; ')}`,
200
- });
201
- }
202
- }
203
- }
174
+ const planningState = null;
204
175
 
205
176
  const controlMap = buildPreflightControlMap({
206
177
  planningDir,
@@ -213,11 +184,11 @@ export function evaluateLifecyclePreflight({
213
184
  else warnings.push(notice);
214
185
  }
215
186
 
216
- if (!usesWorkAuthority && lifecycle.phaseStatusAlignment.mismatches.length > 0) {
187
+ if (!usesAlternateAuthority && lifecycle.phaseStatusAlignment.mismatches.length > 0) {
217
188
  warnings.push({
218
189
  code: 'roadmap_phase_status_mismatch',
219
190
  message: `ROADMAP.md overview/detail phase statuses disagree: ${lifecycle.phaseStatusAlignment.mismatches.join('; ')}`,
220
- artifacts: ['.planning/ROADMAP.md'],
191
+ artifacts: [stateLabel('ROADMAP.md')],
221
192
  });
222
193
  }
223
194
 
@@ -225,11 +196,10 @@ export function evaluateLifecyclePreflight({
225
196
  surface,
226
197
  phase: normalizedPhase,
227
198
  classification: policy.classification,
228
- ownedWrites: policy.ownedWrites,
199
+ ownedWrites,
229
200
  explicitLifecycleMutation: policy.explicitLifecycleMutation,
230
- closureEvidence: describeEvidenceSurface(surface),
231
201
  mutationRequest: expectsMutation,
232
- authority: usesWorkAuthority ? 'work_milestone' : 'planning',
202
+ authority: usesPlanAmendAuthority ? 'plan_amend' : usesBrownfieldAuthority ? 'brownfield_change' : usesWorkAuthority ? 'work_milestone' : 'planning',
233
203
  allowed: blockers.length === 0,
234
204
  status: blockers.length === 0 ? 'allowed' : 'blocked',
235
205
  reason: blockers[0]?.code ?? null,
@@ -238,7 +208,7 @@ export function evaluateLifecyclePreflight({
238
208
  planningState,
239
209
  controlMap: controlMap.summary,
240
210
  lifecycle: {
241
- authority: usesWorkAuthority ? 'work_milestone' : 'planning',
211
+ authority: usesPlanAmendAuthority ? 'plan_amend' : usesBrownfieldAuthority ? 'brownfield_change' : usesWorkAuthority ? 'work_milestone' : 'planning',
242
212
  currentMilestone: lifecycle.currentMilestone,
243
213
  currentPhase: lifecycle.currentPhase ? lifecycle.currentPhase.number : null,
244
214
  nextPhase: lifecycle.nextPhase ? lifecycle.nextPhase.number : null,
@@ -252,6 +222,20 @@ export function evaluateLifecyclePreflight({
252
222
  source: resumeWorkCheckpoint ? 'checkpoint' : 'phase',
253
223
  }
254
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,
255
239
  },
256
240
  };
257
241
  }
@@ -268,11 +252,11 @@ function buildPreflightControlMap({ planningDir, policy, existingBlockerCodes, c
268
252
  planningDir,
269
253
  });
270
254
  const risks = (map.risks || []).filter((risk) => (
271
- !PREFLIGHT_CONTROL_MAP_SKIP_CODES.includes(risk.code)
272
- && !(existingBlockerCodes.has(risk.code) && risk.severity !== 'block')
255
+ !(existingBlockerCodes.has(risk.code) && risk.severity !== 'block')
273
256
  ));
257
+ const stateLabel = createStateLabeler(planningDir);
274
258
  const notices = risks.map((risk) => ({
275
- ...controlMapNotice(risk),
259
+ ...controlMapNotice(risk, stateLabel),
276
260
  severity: risk.severity || 'info',
277
261
  }));
278
262
 
@@ -288,17 +272,17 @@ function buildPreflightControlMap({ planningDir, policy, existingBlockerCodes, c
288
272
  };
289
273
  }
290
274
 
291
- function controlMapNotice(risk) {
275
+ function controlMapNotice(risk, stateLabel) {
292
276
  return {
293
277
  code: risk.code,
294
278
  source: 'control-map',
295
279
  message: risk.message,
296
- artifacts: ['gsdd control-map --json'],
280
+ artifacts: [`node ${stateLabel('bin', 'gsdd.mjs')} control-map --json`],
297
281
  risk,
298
282
  };
299
283
  }
300
284
 
301
- function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
285
+ function buildPhaseBlockers({ lifecycle, phaseToken, surface, stateLabel }) {
302
286
  const blockers = [];
303
287
  const phaseEntry = lifecycle.phases.find((phase) => phase.number === phaseToken);
304
288
  if (!phaseEntry) {
@@ -306,7 +290,7 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
306
290
  blocker(
307
291
  'missing_phase',
308
292
  `Phase ${phaseToken} was not found in the active roadmap.`,
309
- ['.planning/ROADMAP.md']
293
+ [stateLabel('ROADMAP.md')]
310
294
  )
311
295
  );
312
296
  return blockers;
@@ -324,7 +308,7 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
324
308
  blocker(
325
309
  'missing_plan',
326
310
  `Phase ${phaseToken} cannot execute because no PLAN artifact exists.`,
327
- ['.planning/phases/']
311
+ [stateLabel('phases')]
328
312
  )
329
313
  );
330
314
  } else if (pendingPlans.length === 0) {
@@ -343,7 +327,7 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
343
327
  blocker(
344
328
  'phase_already_complete',
345
329
  `Phase ${phaseToken} is already complete and should not be planned again.`,
346
- ['.planning/ROADMAP.md']
330
+ [stateLabel('ROADMAP.md')]
347
331
  )
348
332
  );
349
333
  }
@@ -354,7 +338,7 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
354
338
  blocker(
355
339
  'missing_plan',
356
340
  `Phase ${phaseToken} cannot be verified because no PLAN artifact exists.`,
357
- ['.planning/phases/']
341
+ [stateLabel('phases')]
358
342
  )
359
343
  );
360
344
  }
@@ -363,7 +347,7 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
363
347
  blocker(
364
348
  'missing_summary',
365
349
  `Phase ${phaseToken} cannot be verified because no SUMMARY artifact exists yet.`,
366
- ['.planning/phases/']
350
+ [stateLabel('phases')]
367
351
  )
368
352
  );
369
353
  }
@@ -552,21 +536,21 @@ function buildWorkPhaseBlockers({ workMilestone, phaseToken, surface }) {
552
536
  return blockers;
553
537
  }
554
538
 
555
- function buildRoadmapAlignmentBlockers(lifecycle) {
539
+ function buildRoadmapAlignmentBlockers(lifecycle, stateLabel) {
556
540
  if (lifecycle.phaseStatusAlignment.mismatches.length === 0) return [];
557
541
  return [
558
542
  blocker(
559
543
  'roadmap_phase_status_mismatch',
560
544
  `ROADMAP.md overview/detail phase statuses disagree: ${lifecycle.phaseStatusAlignment.mismatches.join('; ')}`,
561
- ['.planning/ROADMAP.md']
545
+ [stateLabel('ROADMAP.md')]
562
546
  ),
563
547
  ];
564
548
  }
565
549
 
566
- function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
550
+ function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false, stateLabel } = {}) {
567
551
  const blockers = [];
568
552
  if (!lifecycle.currentMilestone.version) {
569
- 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')]));
570
554
  return blockers;
571
555
  }
572
556
 
@@ -575,19 +559,19 @@ function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
575
559
  blocker(
576
560
  allowArchivedBlocker ? 'milestone_already_archived' : 'milestone_already_archived',
577
561
  `Milestone ${lifecycle.currentMilestone.version} is already archived-with-ROADMAP.md evidence.`,
578
- ['.planning/ROADMAP.md', '.planning/MILESTONES.md']
562
+ [stateLabel('ROADMAP.md'), stateLabel('MILESTONES.md')]
579
563
  )
580
564
  );
581
565
  }
582
566
 
583
567
  if (lifecycle.counts.total === 0) {
584
- 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')]));
585
569
  } else if (lifecycle.counts.completed !== lifecycle.counts.total) {
586
570
  blockers.push(
587
571
  blocker(
588
572
  'incomplete_phases',
589
573
  `Milestone ${lifecycle.currentMilestone.version} still has incomplete phases (${lifecycle.counts.completed}/${lifecycle.counts.total} complete).`,
590
- ['.planning/ROADMAP.md']
574
+ [stateLabel('ROADMAP.md')]
591
575
  )
592
576
  );
593
577
  }
@@ -602,7 +586,7 @@ function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
602
586
  blocker(
603
587
  'missing_verification',
604
588
  `Completed phases are missing VERIFICATION artifacts (${phasesMissingVerification.join(', ')}).`,
605
- ['.planning/phases/']
589
+ [stateLabel('phases')]
606
590
  )
607
591
  );
608
592
  }
@@ -611,13 +595,14 @@ function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
611
595
  }
612
596
 
613
597
  function buildCompletionBlockers(planningDir, lifecycle) {
598
+ const stateLabel = createStateLabeler(planningDir);
614
599
  const auditPath = join(planningDir, `${lifecycle.currentMilestone.version}-MILESTONE-AUDIT.md`);
615
600
  if (!existsSync(auditPath)) {
616
601
  return [
617
602
  blocker(
618
603
  'missing_milestone_audit',
619
604
  `Milestone ${lifecycle.currentMilestone.version} cannot be completed without a milestone audit artifact.`,
620
- [auditPath]
605
+ [stateLabel(`${lifecycle.currentMilestone.version}-MILESTONE-AUDIT.md`)]
621
606
  ),
622
607
  ];
623
608
  }
@@ -630,194 +615,14 @@ function buildCompletionBlockers(planningDir, lifecycle) {
630
615
  blocker(
631
616
  'audit_not_passed',
632
617
  `Milestone ${lifecycle.currentMilestone.version} requires a passed audit before completion.`,
633
- [auditPath]
618
+ [stateLabel(`${lifecycle.currentMilestone.version}-MILESTONE-AUDIT.md`)]
634
619
  ),
635
620
  ];
636
621
  }
637
622
 
638
- const releaseContractBlockers = buildReleaseClaimCompletionBlockers(auditContent, auditPath);
639
- if (releaseContractBlockers.length > 0) return releaseContractBlockers;
640
-
641
623
  return [];
642
624
  }
643
625
 
644
- function buildReleaseClaimCompletionBlockers(auditContent, auditPath) {
645
- const frontmatter = extractFrontmatter(auditContent);
646
- const deliveryPosture = readTopLevelScalar(frontmatter, 'delivery_posture');
647
- const releaseClaimPosture = readTopLevelScalar(frontmatter, 'release_claim_posture');
648
- const evidenceBlock = extractYamlBlock(frontmatter, 'evidence_contract');
649
- const releaseBlock = extractYamlBlock(frontmatter, 'release_claim_contract');
650
- const missing = [];
651
-
652
- if (!deliveryPosture) missing.push('delivery_posture');
653
- if (!releaseClaimPosture) missing.push('release_claim_posture');
654
- if (!evidenceBlock) missing.push('evidence_contract');
655
- if (!releaseBlock) missing.push('release_claim_contract');
656
-
657
- if (missing.length > 0) {
658
- return [blocker(
659
- 'missing_release_claim_contract',
660
- `Milestone audit is missing release closeout metadata (${missing.join(', ')}). Re-run audit before completion.`,
661
- [auditPath]
662
- )];
663
- }
664
-
665
- const requiredKinds = readBlockList(evidenceBlock, 'required_kinds');
666
- const observedKinds = readBlockList(evidenceBlock, 'observed_kinds');
667
- const missingKinds = readBlockList(evidenceBlock, 'missing_kinds');
668
- const unsupportedClaims = readBlockList(releaseBlock, 'unsupported_claims');
669
- const waivedKinds = readBlockList(releaseBlock, 'waivers');
670
- const deferrals = readBlockList(releaseBlock, 'deferrals');
671
- const contradictionChecks = readNestedStatusBlock(releaseBlock, 'contradiction_checks');
672
- const blockers = [];
673
- const invalidEvidenceKinds = [
674
- ...findInvalidEvidenceKinds('required_kinds', requiredKinds),
675
- ...findInvalidEvidenceKinds('observed_kinds', observedKinds),
676
- ...findInvalidEvidenceKinds('missing_kinds', missingKinds),
677
- ...findInvalidEvidenceKinds('waivers', waivedKinds),
678
- ];
679
-
680
- if (requiredKinds.length === 0 && observedKinds.length === 0) {
681
- blockers.push(blocker(
682
- 'missing_release_evidence_contract',
683
- 'Milestone audit evidence_contract must include required_kinds and observed_kinds before completion.',
684
- [auditPath]
685
- ));
686
- }
687
-
688
- if (!DELIVERY_POSTURES.includes(deliveryPosture)) {
689
- blockers.push(blocker(
690
- 'invalid_delivery_posture',
691
- `Milestone audit has invalid delivery_posture (${deliveryPosture}). Re-run audit before completion.`,
692
- [auditPath]
693
- ));
694
- }
695
-
696
- if (!RELEASE_CLAIM_POSTURES.includes(releaseClaimPosture)) {
697
- blockers.push(blocker(
698
- 'invalid_release_claim_posture',
699
- `Milestone audit has invalid release_claim_posture (${releaseClaimPosture}). Re-run audit before completion.`,
700
- [auditPath]
701
- ));
702
- }
703
-
704
- if (invalidEvidenceKinds.length > 0) {
705
- blockers.push(blocker(
706
- 'invalid_release_evidence_kinds',
707
- `Milestone audit has invalid release evidence kind values (${invalidEvidenceKinds.join(', ')}). Supported values are ${EVIDENCE_KINDS.join(', ')}.`,
708
- [auditPath]
709
- ));
710
- }
711
-
712
- const missingContradictionChecks = RELEASE_CONTRADICTION_CHECKS.filter((name) => !(name in contradictionChecks));
713
- const unknownContradictionChecks = Object.keys(contradictionChecks)
714
- .filter((name) => !RELEASE_CONTRADICTION_CHECKS.includes(name));
715
- const invalidContradictionChecks = Object.entries(contradictionChecks)
716
- .filter(([, status]) => !RELEASE_CONTRADICTION_STATUSES.includes(status))
717
- .map(([name]) => name);
718
-
719
- if (missingContradictionChecks.length > 0) {
720
- blockers.push(blocker(
721
- 'missing_release_contradiction_checks',
722
- `Milestone audit release_claim_contract.contradiction_checks is missing required checks (${missingContradictionChecks.join(', ')}).`,
723
- [auditPath]
724
- ));
725
- }
726
-
727
- if (invalidContradictionChecks.length > 0) {
728
- blockers.push(blocker(
729
- 'invalid_release_contradiction_checks',
730
- `Milestone audit release_claim_contract.contradiction_checks has invalid statuses (${invalidContradictionChecks.join(', ')}).`,
731
- [auditPath]
732
- ));
733
- }
734
-
735
- if (unknownContradictionChecks.length > 0) {
736
- blockers.push(blocker(
737
- 'unknown_release_contradiction_checks',
738
- `Milestone audit release_claim_contract.contradiction_checks has unknown checks (${unknownContradictionChecks.join(', ')}). Supported checks are ${RELEASE_CONTRADICTION_CHECKS.join(', ')}.`,
739
- [auditPath]
740
- ));
741
- }
742
-
743
- if (!DELIVERY_POSTURES.includes(deliveryPosture) || !RELEASE_CLAIM_POSTURES.includes(releaseClaimPosture)) {
744
- return blockers;
745
- }
746
-
747
- const releaseEvaluation = evaluateReleaseClaimCloseoutContract({
748
- surface: 'complete-milestone',
749
- deliveryPosture,
750
- releaseClaimPosture,
751
- observedKinds,
752
- waivedKinds,
753
- unsupportedClaims,
754
- deferrals,
755
- contradictionChecks,
756
- });
757
-
758
- if (DELIVERY_POSTURES.includes(deliveryPosture)) {
759
- const evidenceContract = getEvidenceContract('complete-milestone', deliveryPosture);
760
- const enforcedRequiredKinds = [...new Set([...evidenceContract.requiredKinds, ...releaseEvaluation.requiredKinds])];
761
- const undeclaredRequiredKinds = enforcedRequiredKinds.filter((kind) => !requiredKinds.includes(kind));
762
- const recomputedMissingKinds = enforcedRequiredKinds.filter((kind) => !observedKinds.includes(kind));
763
-
764
- if (undeclaredRequiredKinds.length > 0) {
765
- blockers.push(blocker(
766
- 'invalid_release_evidence_contract',
767
- `Milestone audit evidence_contract.required_kinds omits required closeout evidence (${undeclaredRequiredKinds.join(', ')}).`,
768
- [auditPath]
769
- ));
770
- }
771
-
772
- if (recomputedMissingKinds.length > 0) {
773
- blockers.push(blocker(
774
- 'missing_required_release_evidence',
775
- `Milestone audit observed evidence is missing required closeout kinds (${recomputedMissingKinds.join(', ')}).`,
776
- [auditPath]
777
- ));
778
- }
779
- }
780
-
781
- if (missingKinds.length > 0) {
782
- blockers.push(blocker(
783
- 'missing_required_release_evidence',
784
- `Milestone audit is missing required evidence kinds for closeout (${missingKinds.join(', ')}).`,
785
- [auditPath]
786
- ));
787
- }
788
-
789
- if (releaseEvaluation.invalidWaivers.length > 0) {
790
- blockers.push(blocker(
791
- 'invalid_release_waivers',
792
- `Milestone audit has invalid waivers for missing required evidence (${releaseEvaluation.invalidWaivers.join(', ')}).`,
793
- [auditPath]
794
- ));
795
- }
796
- if (releaseEvaluation.blockers.some((releaseBlocker) => releaseBlocker.code === 'incompatible_release_claim_posture')) {
797
- blockers.push(blocker(
798
- 'incompatible_release_claim_posture',
799
- `Milestone audit release_claim_posture (${releaseClaimPosture}) is incompatible with delivery_posture (${deliveryPosture}).`,
800
- [auditPath]
801
- ));
802
- }
803
- if (releaseEvaluation.unresolvedUnsupportedClaims.length > 0) {
804
- blockers.push(blocker(
805
- 'unsupported_release_claims',
806
- `Milestone audit has unsupported release claims without downgrade or deferral (${releaseEvaluation.unresolvedUnsupportedClaims.join(', ')}).`,
807
- [auditPath]
808
- ));
809
- }
810
- if (releaseEvaluation.failedContradictionChecks.length > 0) {
811
- blockers.push(blocker(
812
- 'failed_release_contradiction_checks',
813
- `Milestone audit has failed release contradiction checks (${releaseEvaluation.failedContradictionChecks.join(', ')}).`,
814
- [auditPath]
815
- ));
816
- }
817
-
818
- return blockers;
819
- }
820
-
821
626
  function extractFrontmatter(content) {
822
627
  const match = String(content || '').replace(/\r\n/g, '\n').match(/^---\n([\s\S]*?)\n---/);
823
628
  return match ? match[1] : '';
@@ -828,136 +633,6 @@ function readTopLevelScalar(frontmatter, key) {
828
633
  return match ? cleanYamlValue(match[1]) : null;
829
634
  }
830
635
 
831
- function extractYamlBlock(frontmatter, key) {
832
- const lines = String(frontmatter || '').replace(/\r\n/g, '\n').split('\n');
833
- const startIndex = lines.findIndex((line) => new RegExp(`^${key}:\\s*(?:#.*)?$`).test(line.trim()));
834
- if (startIndex === -1) return '';
835
-
836
- const collected = [];
837
- for (const line of lines.slice(startIndex + 1)) {
838
- if (/^[A-Za-z0-9_-]+:\s*/.test(line)) break;
839
- collected.push(line);
840
- }
841
- return collected.join('\n');
842
- }
843
-
844
- function readBlockList(block, key) {
845
- const lines = String(block || '').replace(/\r\n/g, '\n').split('\n');
846
- const startIndex = lines.findIndex((line) => new RegExp(`^\\s+${key}:`).test(line));
847
- if (startIndex === -1) return [];
848
- const baseIndent = lines[startIndex].match(/^\s*/)[0].length;
849
-
850
- const inline = lines[startIndex].match(/^\s+[^:]+:\s*\[([^\]]*)\]/);
851
- if (inline) return splitInlineList(inline[1]);
852
-
853
- const collected = [];
854
- for (const line of lines.slice(startIndex + 1)) {
855
- const indent = line.match(/^\s*/)[0].length;
856
- if (line.trim() && indent <= baseIndent && /^\s*[A-Za-z0-9_-]+:\s*/.test(line)) break;
857
- collected.push(line);
858
- }
859
-
860
- return parseYamlListItems(collected, baseIndent)
861
- .map((item) => item.join(' '))
862
- .map(cleanYamlValue);
863
- }
864
-
865
- function parseYamlListItems(lines, baseIndent) {
866
- const items = [];
867
- let current = null;
868
- let itemIndent = null;
869
-
870
- for (const line of lines) {
871
- const match = line.match(/^(\s*)-\s*(.+?)\s*$/);
872
- const indent = line.match(/^\s*/)[0].length;
873
-
874
- if (match && indent > baseIndent && (itemIndent === null || indent === itemIndent)) {
875
- if (current) items.push(current);
876
- current = [match[2]];
877
- itemIndent = indent;
878
- continue;
879
- }
880
-
881
- if (current && line.trim()) {
882
- current.push(line.trim());
883
- }
884
- }
885
-
886
- if (current) items.push(current);
887
- return items;
888
- }
889
-
890
- function findInvalidEvidenceKinds(field, kinds) {
891
- return kinds
892
- .filter((kind) => !EVIDENCE_KINDS.includes(kind))
893
- .map((kind) => `${field}: ${kind}`);
894
- }
895
-
896
- function readNestedStatusBlock(block, key) {
897
- const nested = extractIndentedBlock(block, key);
898
- const statuses = {};
899
- for (const line of nested.split('\n')) {
900
- const match = line.match(/^\s+([A-Za-z0-9_-]+):\s*(.+)$/);
901
- if (match) statuses[match[1]] = cleanYamlValue(match[2]);
902
- }
903
- return statuses;
904
- }
905
-
906
- function extractIndentedBlock(block, key) {
907
- const lines = String(block || '').replace(/\r\n/g, '\n').split('\n');
908
- const startIndex = lines.findIndex((line) => new RegExp(`^\\s+${key}:`).test(line));
909
- if (startIndex === -1) return '';
910
- const baseIndent = lines[startIndex].match(/^\s*/)[0].length;
911
-
912
- const collected = [];
913
- for (const line of lines.slice(startIndex + 1)) {
914
- const indent = line.match(/^\s*/)[0].length;
915
- if (line.trim() && indent <= baseIndent && /^\s*[A-Za-z0-9_-]+:\s*/.test(line)) break;
916
- collected.push(line);
917
- }
918
- return collected.join('\n');
919
- }
920
-
921
- function splitInlineList(value) {
922
- return splitCommaAware(value)
923
- .map(cleanYamlValue)
924
- .filter(Boolean);
925
- }
926
-
927
- function splitCommaAware(value) {
928
- const items = [];
929
- let current = '';
930
- let quote = null;
931
- let escaped = false;
932
-
933
- for (const char of String(value || '')) {
934
- if (escaped) {
935
- current += char;
936
- escaped = false;
937
- continue;
938
- }
939
- if (char === '\\' && quote) {
940
- current += char;
941
- escaped = true;
942
- continue;
943
- }
944
- if ((char === '"' || char === "'") && (!quote || quote === char)) {
945
- quote = quote ? null : char;
946
- current += char;
947
- continue;
948
- }
949
- if (char === ',' && !quote) {
950
- items.push(current);
951
- current = '';
952
- continue;
953
- }
954
- current += char;
955
- }
956
-
957
- items.push(current);
958
- return items;
959
- }
960
-
961
636
  function cleanYamlValue(value) {
962
637
  return stripInlineYamlComment(String(value || ''))
963
638
  .trim()
@@ -1000,6 +675,17 @@ function blocker(code, message, artifacts) {
1000
675
  return { code, message, artifacts };
1001
676
  }
1002
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
+
1003
689
  export function cmdLifecyclePreflight(...args) {
1004
690
  const { args: normalizedArgs, planningDir, invalid, error } = resolveWorkspaceContext(args);
1005
691
  if (invalid) {
@@ -1008,9 +694,10 @@ export function cmdLifecyclePreflight(...args) {
1008
694
  return;
1009
695
  }
1010
696
  const [surface, maybePhase, ...rest] = normalizedArgs;
697
+ const stateLabel = createStateLabeler(planningDir);
1011
698
 
1012
699
  if (!surface) {
1013
- 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>]`);
1014
701
  process.exitCode = 1;
1015
702
  return;
1016
703
  }