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
package/bin/lib/phase.mjs CHANGED
@@ -4,16 +4,9 @@
4
4
  // evaluate once, so CWD must be computed inside function bodies.
5
5
 
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
7
- import { dirname, join, relative } from 'path';
7
+ import { basename, dirname, join, relative } from 'path';
8
8
  import { output } from './cli-utils.mjs';
9
- import { writeFingerprint } from './session-fingerprint.mjs';
10
9
  import { resolveWorkspaceContext } from './workspace-root.mjs';
11
- import {
12
- compareUiProofSlots,
13
- findUiProofBundleFiles,
14
- parseUiProofSlotsContent,
15
- readUiProofBundleFile,
16
- } from './ui-proof.mjs';
17
10
 
18
11
  const PHASE_STATUS_MARKERS = {
19
12
  not_started: '[ ]',
@@ -202,290 +195,6 @@ function evaluatePlanArtifacts(artifacts) {
202
195
  };
203
196
  }
204
197
 
205
- function normalizeUiProofIssue(issue) {
206
- return {
207
- ...issue,
208
- severity: issue.severity || 'blocker',
209
- fix_hint: issue.fix_hint || issue.fix || 'Fix the UI proof issue before claiming verification is complete.',
210
- };
211
- }
212
-
213
- function stripInlineComment(value) {
214
- return String(value || '').replace(/\s+#.*$/, '').trim();
215
- }
216
-
217
- function stripOuterScalarQuotes(value) {
218
- return String(value)
219
- .trim()
220
- .replace(/^(['"])([\s\S]*)\1$/g, '$2')
221
- .trim();
222
- }
223
-
224
- function normalizeNullableFrontmatterValue(value) {
225
- const stripped = stripOuterScalarQuotes(value);
226
- if (!stripped) return '';
227
- if (stripped === '~') return '';
228
- if (/^null$/i.test(stripped)) return '';
229
- return stripped;
230
- }
231
-
232
- function extractUiProofSlotIds(value) {
233
- const ids = [];
234
- const slotPattern = /['"]?slot_id['"]?\s*:\s*['"]?([^,'"\]\s}]+)['"]?/g;
235
- for (const match of String(value || '').matchAll(slotPattern)) {
236
- ids.push(match[1].replace(/^['"]|['"]$/g, ''));
237
- }
238
- return ids;
239
- }
240
-
241
- function readPlanFrontmatter(planContent) {
242
- const content = String(planContent || '');
243
- if (!content.startsWith('---')) return '';
244
- const lines = content.split(/\r?\n/);
245
- const frontmatter = [];
246
- for (let index = 1; index < lines.length; index += 1) {
247
- if (lines[index].trim() === '---') return frontmatter.join('\n');
248
- frontmatter.push(lines[index]);
249
- }
250
- return '';
251
- }
252
-
253
- function frontmatterKeyBlock(frontmatter, key) {
254
- const lines = String(frontmatter || '').split(/\r?\n/);
255
- const keyPattern = new RegExp(`^${key}:[ \\t]*(.*)$`);
256
- for (let index = 0; index < lines.length; index += 1) {
257
- const match = lines[index].match(keyPattern);
258
- if (!match) continue;
259
- const block = [];
260
- for (let next = index + 1; next < lines.length; next += 1) {
261
- const line = lines[next];
262
- if (/^\S[^:\n]*:\s*/.test(line)) break;
263
- block.push(line);
264
- }
265
- return { inline: stripInlineComment(match[1]), block };
266
- }
267
- return null;
268
- }
269
-
270
- function frontmatterScalar(frontmatter, key) {
271
- const entry = frontmatterKeyBlock(frontmatter, key);
272
- if (!entry) return null;
273
- if (entry.inline && !['|', '>'].includes(entry.inline)) return entry.inline;
274
- return entry.block
275
- .map((line) => line.trim())
276
- .filter(Boolean)
277
- .join(' ')
278
- .trim();
279
- }
280
-
281
- function readPlanUiProofContract(planContent) {
282
- const frontmatter = readPlanFrontmatter(planContent);
283
- const slotsEntry = frontmatterKeyBlock(frontmatter, 'ui_proof_slots');
284
- const rationale = normalizeNullableFrontmatterValue(frontmatterScalar(frontmatter, 'no_ui_proof_rationale') || '');
285
- const result = {
286
- hasUiProofKey: Boolean(slotsEntry),
287
- declaresSlots: false,
288
- explicitEmptySlots: false,
289
- noUiProofRationale: rationale,
290
- hasNoUiProofRationale: Boolean(rationale.trim()),
291
- slotIds: [],
292
- };
293
-
294
- if (!slotsEntry) return result;
295
-
296
- if (slotsEntry.inline) {
297
- if (['[]', 'null', '~'].includes(slotsEntry.inline)) {
298
- result.explicitEmptySlots = true;
299
- return result;
300
- }
301
- result.declaresSlots = true;
302
- result.slotIds.push(...extractUiProofSlotIds(slotsEntry.inline));
303
- return result;
304
- }
305
-
306
- let sawMeaningfulLine = false;
307
- for (const line of slotsEntry.block) {
308
- const trimmed = line.trim();
309
- if (!trimmed || trimmed.startsWith('#')) continue;
310
- sawMeaningfulLine = true;
311
- if (/^\s+-\s+/.test(line)) result.declaresSlots = true;
312
- result.slotIds.push(...extractUiProofSlotIds(trimmed));
313
- }
314
-
315
- result.explicitEmptySlots = !result.declaresSlots && !sawMeaningfulLine;
316
- return result;
317
- }
318
-
319
- function findUiProofSlotPlansAndFiles(planningDir, planDisplayPaths) {
320
- const candidates = new Set();
321
- const declaredPlans = [];
322
- const declaredSlotIds = [];
323
- const noUiPlans = [];
324
- const contractErrors = [];
325
- const names = new Set([
326
- 'ui-proof-slots.json',
327
- 'ui-proof-slots.md',
328
- 'UI-PROOF-SLOTS.json',
329
- 'UI-PROOF-SLOTS.md',
330
- 'planned-ui-proof.json',
331
- 'planned-ui-proof.md',
332
- ]);
333
-
334
- for (const planDisplayPath of planDisplayPaths) {
335
- const fullPlanPath = join(planningDir, 'phases', planDisplayPath);
336
- if (!existsSync(fullPlanPath)) continue;
337
- const planContent = readFileSync(fullPlanPath, 'utf-8');
338
- const relPlanPath = relative(planningDir, fullPlanPath).replace(/\\/g, '/');
339
- const contract = readPlanUiProofContract(planContent);
340
- const planDir = dirname(fullPlanPath);
341
- const sidecars = [];
342
- if (existsSync(planDir)) {
343
- for (const entry of readdirSync(planDir, { withFileTypes: true })) {
344
- if (entry.isFile() && names.has(entry.name)) {
345
- sidecars.push(join(planDir, entry.name));
346
- }
347
- }
348
- }
349
-
350
- if (contract.hasUiProofKey && !contract.declaresSlots && !contract.hasNoUiProofRationale) {
351
- contractErrors.push(normalizeUiProofIssue({
352
- code: 'missing_no_ui_proof_rationale',
353
- path: `${relPlanPath}.no_ui_proof_rationale`,
354
- message: 'Plan declares empty ui_proof_slots but does not provide a no_ui_proof_rationale.',
355
- fix: 'Add a nonblank no_ui_proof_rationale for non-UI work, or declare required UI proof slots and add a planned slots artifact.',
356
- }));
357
- }
358
-
359
- if (contract.hasNoUiProofRationale && !contract.declaresSlots) {
360
- noUiPlans.push({ plan: relPlanPath, sidecars });
361
- continue;
362
- }
363
-
364
- if (!contract.declaresSlots) continue;
365
-
366
- declaredPlans.push(relPlanPath);
367
- for (const slotId of contract.slotIds) {
368
- declaredSlotIds.push({ plan: relPlanPath, slot_id: slotId });
369
- }
370
- for (const entry of readdirSync(planDir, { withFileTypes: true })) {
371
- if (entry.isFile() && names.has(entry.name)) {
372
- candidates.add(join(planDir, entry.name));
373
- }
374
- }
375
- }
376
- return { declaredPlans, declaredSlotIds, noUiPlans, contractErrors, files: [...candidates].sort() };
377
- }
378
-
379
- function comparePhaseUiProof({ planningDir, workspaceRoot, planDisplayPaths }) {
380
- const plannedDiscovery = findUiProofSlotPlansAndFiles(planningDir, planDisplayPaths);
381
- const plannedFiles = plannedDiscovery.files;
382
- const phaseDirs = new Set(planDisplayPaths.map((planDisplayPath) => dirname(join(planningDir, 'phases', planDisplayPath))));
383
- const observedFiles = findUiProofBundleFiles(planningDir)
384
- .filter((filePath) => phaseDirs.has(dirname(filePath)));
385
-
386
- const plannedSlots = [];
387
- const errors = [];
388
- const warnings = [];
389
- const planned = [];
390
- const observed = [];
391
-
392
- errors.push(...plannedDiscovery.contractErrors);
393
- for (const noUiPlan of plannedDiscovery.noUiPlans) {
394
- for (const filePath of noUiPlan.sidecars) {
395
- warnings.push({
396
- code: 'stale_ui_proof_sidecar_ignored',
397
- severity: 'warn',
398
- path: relative(workspaceRoot, filePath).replace(/\\/g, '/'),
399
- message: `Plan ${noUiPlan.plan} records no_ui_proof_rationale, so the UI proof sidecar is ignored for closure.`,
400
- fix_hint: 'Remove or classify the stale sidecar if it no longer belongs to this non-UI phase.',
401
- });
402
- }
403
- }
404
-
405
- for (const filePath of plannedFiles) {
406
- const rel = relative(workspaceRoot, filePath).replace(/\\/g, '/');
407
- const parsed = parseUiProofSlotsContent(readFileSync(filePath, 'utf-8'), rel);
408
- planned.push(rel);
409
- plannedSlots.push(...parsed.slots);
410
- errors.push(...parsed.errors.map(normalizeUiProofIssue));
411
- }
412
-
413
- if (plannedSlots.length > 0 && plannedDiscovery.declaredSlotIds.length > 0) {
414
- const plannedSlotIds = new Set(plannedSlots.map((slot) => String(slot?.slot_id || '')));
415
- for (const declaredSlot of plannedDiscovery.declaredSlotIds) {
416
- if (plannedSlotIds.has(String(declaredSlot.slot_id))) continue;
417
- errors.push(normalizeUiProofIssue({
418
- code: 'planned_ui_proof_slots_drift',
419
- path: `${declaredSlot.plan}.ui_proof_slots`,
420
- message: `Plan declares UI proof slot ${declaredSlot.slot_id}, but no matching slot exists in the planned UI proof artifact.`,
421
- fix: 'Update ui-proof-slots.json or ui-proof-slots.md beside the plan so it matches the plan-declared slot IDs, or update the plan declaration.',
422
- }));
423
- }
424
- }
425
-
426
- const observedBundles = [];
427
- for (const filePath of observedFiles) {
428
- const rel = relative(workspaceRoot, filePath).replace(/\\/g, '/');
429
- const parsed = readUiProofBundleFile(filePath);
430
- observed.push(rel);
431
- if (parsed.errors.length > 0) {
432
- errors.push(...parsed.errors.map((error) => normalizeUiProofIssue({ ...error, path: error.path || rel })));
433
- continue;
434
- }
435
- observedBundles.push({
436
- source: rel,
437
- bundle: parsed.bundle,
438
- options: {
439
- requireLocalArtifactExists: true,
440
- workspaceRoot,
441
- bundleDir: dirname(filePath),
442
- },
443
- });
444
- }
445
-
446
- if (plannedFiles.length === 0 && plannedDiscovery.declaredPlans.length > 0) {
447
- const missingError = {
448
- code: 'missing_planned_ui_proof_slots_file',
449
- severity: 'blocker',
450
- path: plannedDiscovery.declaredPlans[0],
451
- message: 'Plan declares ui_proof_slots but no ui-proof-slots artifact was found beside the plan.',
452
- fix_hint: 'Create ui-proof-slots.json or ui-proof-slots.md beside the plan, or set ui_proof_slots: [] with a no_ui_proof_rationale if the phase is not UI-sensitive.',
453
- };
454
- return {
455
- planned,
456
- observed,
457
- status: 'missing',
458
- comparison: { status: 'missing', slots: [], errors: [missingError] },
459
- errors: [missingError],
460
- warnings,
461
- };
462
- }
463
-
464
- if (plannedFiles.length === 0) {
465
- return {
466
- planned,
467
- observed,
468
- status: errors.length > 0 ? 'partial' : 'not_applicable',
469
- comparison: null,
470
- errors,
471
- warnings,
472
- };
473
- }
474
-
475
- const comparison = errors.length > 0
476
- ? { status: 'partial', slots: [], errors: errors.map(normalizeUiProofIssue) }
477
- : compareUiProofSlots(plannedSlots, observedBundles);
478
-
479
- return {
480
- planned,
481
- observed,
482
- status: comparison.status,
483
- comparison,
484
- errors: comparison.errors || errors,
485
- warnings,
486
- };
487
- }
488
-
489
198
  export function updateRoadmapPhaseStatus(roadmap, phaseNumber, status) {
490
199
  const marker = PHASE_STATUS_MARKERS[status];
491
200
  if (!marker) {
@@ -568,6 +277,7 @@ export function cmdPhaseStatus(...args) {
568
277
  return;
569
278
  }
570
279
  const roadmapPath = join(planningDir, 'ROADMAP.md');
280
+ const stateName = basename(planningDir);
571
281
  const [phaseNumber, status] = normalizedArgs;
572
282
 
573
283
  if (!phaseNumber || !status) {
@@ -588,9 +298,8 @@ export function cmdPhaseStatus(...args) {
588
298
  const changed = updated !== roadmap;
589
299
  if (changed) {
590
300
  writeFileSync(roadmapPath, updated);
591
- try { writeFingerprint(planningDir); } catch { /* best-effort */ }
592
301
  }
593
- output({ phase: phaseNumber, status, roadmap: '.planning/ROADMAP.md', changed });
302
+ output({ phase: phaseNumber, status, roadmap: `${stateName}/ROADMAP.md`, changed });
594
303
  } catch (error) {
595
304
  console.error(error.message);
596
305
  process.exitCode = 1;
@@ -605,9 +314,10 @@ export function cmdFindPhase(...args) {
605
314
  return;
606
315
  }
607
316
  const phaseNum = normalizedArgs[0];
317
+ const stateName = basename(planningDir);
608
318
 
609
319
  if (!existsSync(planningDir)) {
610
- output({ error: 'No .planning/ directory found. Run `npx -y gsdd-cli init` then the new-project workflow first.' });
320
+ output({ error: `No ${stateName}/ directory found. Run \`npx -y gsdd-cli init\` then the new-project workflow first.` });
611
321
  return;
612
322
  }
613
323
 
@@ -660,9 +370,10 @@ export function buildPhaseVerificationReport(...args) {
660
370
  if (!phaseNum) {
661
371
  return { ok: false, error: 'Usage: gsdd verify <phase-number>', exitCode: 1 };
662
372
  }
373
+ const stateName = basename(planningDir);
663
374
 
664
375
  if (!existsSync(planningDir)) {
665
- return { ok: false, error: 'No .planning/ directory found.', exitCode: 1 };
376
+ return { ok: false, error: `No ${stateName}/ directory found.`, exitCode: 1 };
666
377
  }
667
378
  const phasesDir = join(planningDir, 'phases');
668
379
  const matchingPlans = findFiles(phasesDir, `${padPhase(phaseNum)}-PLAN`);
@@ -672,7 +383,7 @@ export function buildPhaseVerificationReport(...args) {
672
383
  prerequisiteBlockers.push({
673
384
  code: 'missing_phase_plan',
674
385
  severity: 'blocker',
675
- path: `.planning/phases/${padPhase(phaseNum)}-*/${padPhase(phaseNum)}-PLAN.md`,
386
+ path: `${stateName}/phases/${padPhase(phaseNum)}-*/${padPhase(phaseNum)}-PLAN.md`,
676
387
  message: `No PLAN.md artifact was found for phase ${normalizePhaseToken(phaseNum)}.`,
677
388
  fix_hint: `Run /gsdd-plan ${normalizePhaseToken(phaseNum)} before verifying this phase.`,
678
389
  });
@@ -681,7 +392,7 @@ export function buildPhaseVerificationReport(...args) {
681
392
  prerequisiteBlockers.push({
682
393
  code: 'missing_phase_summary',
683
394
  severity: 'blocker',
684
- path: `.planning/phases/${padPhase(phaseNum)}-*/${padPhase(phaseNum)}-SUMMARY.md`,
395
+ path: `${stateName}/phases/${padPhase(phaseNum)}-*/${padPhase(phaseNum)}-SUMMARY.md`,
685
396
  message: `No SUMMARY.md artifact was found for phase ${normalizePhaseToken(phaseNum)}.`,
686
397
  fix_hint: `Run /gsdd-execute ${normalizePhaseToken(phaseNum)} before verifying this phase.`,
687
398
  });
@@ -693,26 +404,12 @@ export function buildPhaseVerificationReport(...args) {
693
404
  : [];
694
405
  });
695
406
  const artifactStatus = evaluatePlanArtifacts(artifacts);
696
- const uiProof = comparePhaseUiProof({
697
- planningDir,
698
- workspaceRoot,
699
- planDisplayPaths: matchingPlans,
700
- });
701
- const uiProofSatisfied = ['satisfied', 'not_applicable'].includes(uiProof.status);
702
407
  const legacyVerified = matchingPlans.length > 0 && matchingSummaries.length > 0;
703
- const uiProofGate = {
704
- status: uiProof.status,
705
- required: uiProof.status !== 'not_applicable',
706
- satisfied: uiProofSatisfied,
707
- blocks_verification: uiProof.status !== 'not_applicable' && !uiProofSatisfied,
708
- required_block: uiProof.status !== 'not_applicable' && !uiProofSatisfied ? 'ui-proof-failed' : null,
709
- };
710
408
  const blockedOn = [
711
409
  ...(prerequisiteBlockers.length > 0 ? ['prerequisites'] : []),
712
410
  ...(artifactStatus.satisfied ? [] : ['artifacts']),
713
- ...(uiProofGate.blocks_verification ? ['ui_proof'] : []),
714
411
  ];
715
- const closureVerified = legacyVerified && prerequisiteBlockers.length === 0 && artifactStatus.satisfied && uiProofSatisfied;
412
+ const closureVerified = legacyVerified && prerequisiteBlockers.length === 0 && artifactStatus.satisfied;
716
413
 
717
414
  const result = {
718
415
  phase: normalizePhaseToken(phaseNum),
@@ -722,7 +419,6 @@ export function buildPhaseVerificationReport(...args) {
722
419
  artifacts,
723
420
  allExist: artifacts.every((artifact) => artifact.exists),
724
421
  artifact_status: artifactStatus,
725
- uiProof,
726
422
  verified: closureVerified,
727
423
  legacy_verified: legacyVerified,
728
424
  phase_artifacts_present: legacyVerified,
@@ -730,7 +426,6 @@ export function buildPhaseVerificationReport(...args) {
730
426
  satisfied: prerequisiteBlockers.length === 0,
731
427
  blockers: prerequisiteBlockers,
732
428
  },
733
- ui_proof: uiProofGate,
734
429
  blocked_on: blockedOn,
735
430
  blocks_verification: blockedOn.length > 0,
736
431
  };
@@ -7,17 +7,27 @@ const __dirname = dirname(__filename);
7
7
  const DISTILLED_DIR = join(__dirname, '..', '..', 'distilled');
8
8
  const HELPER_LIB_FILES = Object.freeze([
9
9
  'cli-utils.mjs',
10
- 'closeout-report.mjs',
11
10
  'control-map.mjs',
12
- 'evidence-contract.mjs',
13
11
  'file-ops.mjs',
14
12
  'lifecycle-preflight.mjs',
15
13
  'lifecycle-state.mjs',
14
+ 'next.mjs',
16
15
  'phase.mjs',
17
- 'session-fingerprint.mjs',
18
- 'ui-proof.mjs',
16
+ 'state-dir.mjs',
17
+ 'work-context.mjs',
19
18
  'workspace-root.mjs',
20
19
  ]);
20
+ const DEFAULT_STATE_DIR_NAME = '.work';
21
+
22
+ function normalizeStateDirName(stateDirName = DEFAULT_STATE_DIR_NAME) {
23
+ return stateDirName || DEFAULT_STATE_DIR_NAME;
24
+ }
25
+
26
+ function localizeStateDirReferences(content, { stateDirName = DEFAULT_STATE_DIR_NAME } = {}) {
27
+ const normalized = normalizeStateDirName(stateDirName);
28
+ if (normalized === DEFAULT_STATE_DIR_NAME) return content;
29
+ return String(content).replace(/\.work(?=\/|\\|`|'|"|\)|\]|\}|,|\.|:|;|\s|$)/g, normalized);
30
+ }
21
31
 
22
32
  function getWorkflowContent(workflowFile) {
23
33
  const filePath = join(DISTILLED_DIR, 'workflows', workflowFile);
@@ -31,8 +41,8 @@ function getDelegateContent(delegateFile) {
31
41
  return `<!-- Delegate file not found: ${delegateFile} -->\n`;
32
42
  }
33
43
 
34
- function renderSkillContent(workflow) {
35
- const workflowContent = getWorkflowContent(workflow.workflow);
44
+ function renderSkillContent(workflow, options = {}) {
45
+ const workflowContent = localizeStateDirReferences(getWorkflowContent(workflow.workflow), options);
36
46
  return `---
37
47
  name: ${workflow.name}
38
48
  description: ${workflow.description}
@@ -43,60 +53,63 @@ agent: ${workflow.agent}
43
53
  ${workflowContent}`;
44
54
  }
45
55
 
46
- function renderPlanningCliLauncher() {
56
+ function renderPlanningCliLauncher({ stateDirName = DEFAULT_STATE_DIR_NAME } = {}) {
57
+ const helperPath = `${normalizeStateDirName(stateDirName)}/bin/gsdd.mjs`;
58
+ const checkpointBackupPath = `${normalizeStateDirName(stateDirName)}/.continue-here.bak`;
47
59
  return `#!/usr/bin/env node
48
60
 
49
61
  import { cmdFileOp } from './lib/file-ops.mjs';
50
62
  import { cmdLifecyclePreflight } from './lib/lifecycle-preflight.mjs';
51
63
  import { cmdPhaseStatus, cmdVerify } from './lib/phase.mjs';
52
- import { cmdSessionFingerprint } from './lib/session-fingerprint.mjs';
53
- import { cmdUiProof } from './lib/ui-proof.mjs';
54
- import { cmdControlMap } from './lib/control-map.mjs';
55
- import { createCmdCloseoutReport } from './lib/closeout-report.mjs';
64
+ import { buildControlMap } from './lib/control-map.mjs';
65
+ import { createCmdNext } from './lib/next.mjs';
56
66
  import { bootstrapHelperWorkspace, consumeWorkspaceRootArg, resolveWorkspaceContext } from './lib/workspace-root.mjs';
57
67
 
58
68
  const HELPER_CONTEXT = {
69
+ cwd: process.cwd(),
59
70
  workflows: [],
60
71
  frameworkVersion: 'generated-helper',
61
72
  };
62
- const cmdCloseoutReport = createCmdCloseoutReport(HELPER_CONTEXT);
73
+ const cmdNext = createCmdNext(HELPER_CONTEXT);
74
+
75
+ function cmdControlMap(...controlArgs) {
76
+ const context = resolveWorkspaceContext([], { cwd: HELPER_CONTEXT.cwd });
77
+ const report = buildControlMap({
78
+ workspaceRoot: context.workspaceRoot,
79
+ planningDir: context.planningDir,
80
+ includeIgnoredPaths: controlArgs.includes('--with-ignored'),
81
+ });
82
+ console.log(JSON.stringify(report, null, 2));
83
+ }
63
84
 
64
85
  const COMMANDS = {
86
+ 'control-map': cmdControlMap,
65
87
  'file-op': cmdFileOp,
66
88
  'lifecycle-preflight': cmdLifecyclePreflight,
67
89
  'phase-status': cmdPhaseStatus,
68
90
  verify: cmdVerify,
69
- 'session-fingerprint': cmdSessionFingerprint,
70
- 'ui-proof': cmdUiProof,
71
- 'control-map': cmdControlMap,
72
- 'closeout-report': cmdCloseoutReport,
91
+ next: cmdNext,
73
92
  };
74
93
 
75
94
  function printHelp() {
76
95
  console.log([
77
- 'Usage: node .planning/bin/gsdd.mjs [--workspace-root <path>] <command> [args]',
96
+ 'Usage: node ${helperPath} [--workspace-root <path>] <command> [args]',
78
97
  '',
79
98
  'Local workflow helper commands:',
99
+ ' control-map [--json] [--with-ignored]',
100
+ ' Print computed repo/worktree/workflow state for workflow-internal checks',
80
101
  ' file-op <copy|delete|regex-sub>',
81
102
  ' Run deterministic workspace-confined file operations',
82
- ' Example: node .planning/bin/gsdd.mjs file-op delete .planning/.continue-here.bak --missing ok',
103
+ ' Example: node ${helperPath} file-op delete ${checkpointBackupPath} --missing ok',
83
104
  ' phase-status <N> <status> Update ROADMAP.md phase status ([ ] / [-] / [x])',
84
- ' Example: node .planning/bin/gsdd.mjs phase-status 1 done',
85
- ' verify <N> Run direct phase artifact and UI-proof gate checks',
86
- ' Example: node .planning/bin/gsdd.mjs verify 1',
105
+ ' Example: node ${helperPath} phase-status 1 done',
106
+ ' verify <N> Run direct phase artifact checks',
107
+ ' Example: node ${helperPath} verify 1',
87
108
  ' lifecycle-preflight <surface> [phase]',
88
109
  ' Inspect lifecycle gate results for a workflow surface',
89
- ' Example: node .planning/bin/gsdd.mjs lifecycle-preflight verify 1 --expects-mutation phase-status',
90
- ' session-fingerprint write [--allow-changed <ROADMAP.md,SPEC.md,config.json>]',
91
- ' Rebaseline planning-state drift after reviewing changed planning files',
92
- ' ui-proof validate <path> [--claim <public|publication|tracked|delivery|release>]',
93
- ' Validate UI proof metadata; use --claim for stronger proof uses',
94
- ' ui-proof compare <planned-slots-json> [observed-bundle-json ...]',
95
- ' Compare planned UI proof slots against observed bundles',
96
- ' control-map [--json] [--with-ignored] [--annotations <path>]',
97
- ' Report computed repo/worktree/planning state and local annotations',
98
- ' closeout-report [--json] [--phase <N>]',
99
- ' Replay read-only closeout status from control-map, health, preflight, verify, and UI-proof signals',
110
+ ' Example: node ${helperPath} lifecycle-preflight verify 1 --expects-mutation phase-status',
111
+ ' next [--json] [--init]',
112
+ ' Route to the next safe Workspine action from ${normalizeStateDirName(stateDirName)}, brownfield, planning, and repo truth',
100
113
  '',
101
114
  'Advanced option:',
102
115
  ' --workspace-root <path> Override workspace root discovery before or after the subcommand',
@@ -106,6 +119,7 @@ function printHelp() {
106
119
  function applyWorkspaceRootOverride(workspaceRootArg) {
107
120
  if (!workspaceRootArg) {
108
121
  bootstrapHelperWorkspace(import.meta.url);
122
+ HELPER_CONTEXT.cwd = process.cwd();
109
123
  return true;
110
124
  }
111
125
 
@@ -117,6 +131,7 @@ function applyWorkspaceRootOverride(workspaceRootArg) {
117
131
  }
118
132
 
119
133
  process.env.GSDD_WORKSPACE_ROOT = context.workspaceRoot;
134
+ HELPER_CONTEXT.cwd = context.workspaceRoot;
120
135
  try {
121
136
  process.chdir(context.workspaceRoot);
122
137
  } catch {
@@ -183,11 +198,11 @@ function readHelperLibContent(fileName) {
183
198
  return readFileSync(join(__dirname, fileName), 'utf-8');
184
199
  }
185
200
 
186
- function buildPlanningCliHelperEntries() {
201
+ function buildPlanningCliHelperEntries(options = {}) {
187
202
  return [
188
203
  {
189
204
  relativePath: 'bin/gsdd.mjs',
190
- content: renderPlanningCliLauncher(),
205
+ content: renderPlanningCliLauncher(options),
191
206
  },
192
207
  {
193
208
  relativePath: 'bin/gsdd',
@@ -208,15 +223,15 @@ function buildPlanningCliHelperEntries() {
208
223
  ];
209
224
  }
210
225
 
211
- function buildPortableSkillEntries(workflows) {
226
+ function buildPortableSkillEntries(workflows, options = {}) {
212
227
  return workflows.map((workflow) => ({
213
228
  relativePath: `.agents/skills/${workflow.name}/SKILL.md`,
214
- content: renderSkillContent(workflow),
229
+ content: renderSkillContent(workflow, options),
215
230
  }));
216
231
  }
217
232
 
218
- function renderOpenCodeCommandContent(workflow) {
219
- const workflowContent = getWorkflowContent(workflow.workflow);
233
+ function renderOpenCodeCommandContent(workflow, options = {}) {
234
+ const workflowContent = localizeStateDirReferences(getWorkflowContent(workflow.workflow), options);
220
235
  return `---
221
236
  description: ${workflow.description}
222
237
  ---
@@ -224,19 +239,23 @@ description: ${workflow.description}
224
239
  ${workflowContent}`;
225
240
  }
226
241
 
227
- function renderAgentsBoundedBlock() {
242
+ function renderAgentsBoundedBlock(options = {}) {
228
243
  const blockPath = join(DISTILLED_DIR, 'templates', 'agents.block.md');
229
- if (existsSync(blockPath)) return readFileSync(blockPath, 'utf-8').trim();
230
- return '## GSDD Governance (Generated)\n\n- Framework: GSDD\n- Planning: .planning/\n- Workflows: .agents/skills/gsdd-*/SKILL.md';
244
+ if (existsSync(blockPath)) return localizeStateDirReferences(readFileSync(blockPath, 'utf-8'), options).trim();
245
+ const stateDirName = normalizeStateDirName(options.stateDirName);
246
+ const planningLine = stateDirName === DEFAULT_STATE_DIR_NAME
247
+ ? 'Planning state: `.work/` (legacy `.planning/` workspaces are still read).'
248
+ : 'Planning state: `.planning/` (legacy workspace; new Workspine projects use `.work/`).';
249
+ return `## GSDD Governance (Generated)\n\n- Framework: GSDD\n- ${planningLine}\n- Workflows: .agents/skills/gsdd-*/SKILL.md`;
231
250
  }
232
251
 
233
- function renderAgentsFileContent() {
252
+ function renderAgentsFileContent(options = {}) {
234
253
  const templatePath = join(DISTILLED_DIR, 'templates', 'agents.md');
235
254
  if (existsSync(templatePath)) {
236
255
  const template = readFileSync(templatePath, 'utf-8');
237
- return template.replace('{{GSDD_BLOCK}}', renderAgentsBoundedBlock()).trimEnd() + '\n';
256
+ return template.replace('{{GSDD_BLOCK}}', renderAgentsBoundedBlock(options)).trimEnd() + '\n';
238
257
  }
239
- const block = renderAgentsBoundedBlock();
258
+ const block = renderAgentsBoundedBlock(options);
240
259
  return `# AGENTS.md - GSDD Governance\n\n<!-- BEGIN GSDD -->\n${block}\n<!-- END GSDD -->\n`;
241
260
  }
242
261
 
@@ -274,6 +293,7 @@ export {
274
293
  buildPortableSkillEntries,
275
294
  getDelegateContent,
276
295
  getWorkflowContent,
296
+ localizeStateDirReferences,
277
297
  renderAgentsBoundedBlock,
278
298
  renderAgentsFileContent,
279
299
  renderOpenCodeCommandContent,