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
@@ -293,7 +293,8 @@ export function readBrownfieldChangeState(planningDir) {
293
293
  }
294
294
 
295
295
  export function deriveNonPhaseState({ planningDir, hasSpec, hasRoadmap, brownfieldChange } = {}) {
296
- if (brownfieldChange?.exists) return 'active_brownfield_change';
296
+ const brownfieldStatus = String(brownfieldChange?.currentStatus || '').trim().toLowerCase();
297
+ if (brownfieldChange?.exists && brownfieldStatus !== 'closed') return 'active_brownfield_change';
297
298
  if (hasRoadmap) return null;
298
299
  if (hasSpec) return 'between_milestones';
299
300
  if (hasSubstantiveCodebaseMaps(planningDir)) return 'codebase_only';
package/bin/lib/next.mjs CHANGED
@@ -2,6 +2,7 @@ import { join } from 'path';
2
2
  import { existsSync } from 'fs';
3
3
  import { output, parseFlagValue } from './cli-utils.mjs';
4
4
  import { buildControlMap } from './control-map.mjs';
5
+ import { resolveStateDir } from './state-dir.mjs';
5
6
  import {
6
7
  NEXT_STATES,
7
8
  addOpenQuestion,
@@ -68,6 +69,9 @@ function packet(overrides) {
68
69
  confidence: overrides.confidence || 'medium',
69
70
  next_command: overrides.next_command || null,
70
71
  next_action: overrides.next_action || null,
72
+ authority: overrides.authority || null,
73
+ route_kind: overrides.route_kind || null,
74
+ blocked_by: overrides.blocked_by || [],
71
75
  requires_user: Boolean(overrides.requires_user),
72
76
  questions: overrides.questions || [],
73
77
  constraints: overrides.constraints || [],
@@ -110,6 +114,15 @@ function manualReviewAction(targets, description) {
110
114
  };
111
115
  }
112
116
 
117
+ function stateDirName(context) {
118
+ return context?.planning?.state_dir_name || '.work';
119
+ }
120
+
121
+ function statePath(context, relativePath = '') {
122
+ const dirName = stateDirName(context);
123
+ return relativePath ? `${dirName}/${relativePath}` : dirName;
124
+ }
125
+
113
126
  function userQuestionAction(questionIds, description) {
114
127
  return {
115
128
  type: 'user_question',
@@ -120,9 +133,10 @@ function userQuestionAction(questionIds, description) {
120
133
 
121
134
  function summarizeControlMap(cwd) {
122
135
  try {
136
+ const stateDir = resolveStateDir(cwd).dir;
123
137
  return buildControlMap({
124
138
  workspaceRoot: cwd,
125
- planningDir: join(cwd, '.planning'),
139
+ planningDir: stateDir,
126
140
  });
127
141
  } catch (error) {
128
142
  return {
@@ -145,10 +159,29 @@ function readManifestStatus(context) {
145
159
 
146
160
  function repoWarningsFromControlMap(controlMap) {
147
161
  return (controlMap.risks || [])
148
- .filter((risk) => risk.code === 'canonical_dirty')
162
+ .filter((risk) => risk.code === 'canonical_dirty' || risk.severity === 'block')
149
163
  .map((risk) => risk.message);
150
164
  }
151
165
 
166
+ function controlMapBlockers(controlMap) {
167
+ return (controlMap.risks || [])
168
+ .filter((risk) => risk.severity === 'block')
169
+ .map((risk) => risk.code);
170
+ }
171
+
172
+ function hasActiveBrownfieldChange(context) {
173
+ return brownfieldPosture(context) === 'active';
174
+ }
175
+
176
+ function brownfieldPosture(context) {
177
+ if (!context.planning.has_brownfield_change) return null;
178
+ const status = String(context.planning.brownfield_change?.currentStatus || '').trim().toLowerCase();
179
+ if (status === 'closed') return 'closed';
180
+ if (status === 'ready_for_verification') return 'ready_for_verification';
181
+ if (status === 'blocked') return 'blocked';
182
+ return 'active';
183
+ }
184
+
152
185
  function routeFromStateObject(stateValue) {
153
186
  const state = stateValue || {};
154
187
  const workflow = state.workflow || state.milestone || state;
@@ -277,6 +310,7 @@ function routeNext(ctx) {
277
310
  if (context.milestone?.has_audit) inputsConsidered.push('.work/milestone/AUDIT.md');
278
311
  else inputsSkipped.push('.work/milestone/AUDIT.md: missing');
279
312
  if (context.milestone?.phase_packet_count > 0) inputsConsidered.push('.work/milestone/phases/*');
313
+ if (context.planning.has_brownfield_change) inputsConsidered.push(statePath(context, 'brownfield-change/CHANGE.md'));
280
314
 
281
315
  if (context.graph.invalid.length > 0) {
282
316
  return packet({
@@ -423,8 +457,114 @@ function routeNext(ctx) {
423
457
  }, { context, controlMap, constraints, privacyNotes, inputsConsidered, inputsSkipped, traceRefs });
424
458
  }
425
459
 
460
+ const brownfieldStatus = brownfieldPosture(context);
461
+ if (['active', 'blocked', 'ready_for_verification'].includes(brownfieldStatus) && context.milestone?.exists) {
462
+ return packet({
463
+ state: 'blocked',
464
+ reason: 'Active brownfield-change authority and .work/milestone authority both exist; continuing would silently choose between two continuity roots.',
465
+ confidence: 'high',
466
+ next_command: null,
467
+ next_action: manualReviewAction([statePath(context, 'brownfield-change/CHANGE.md'), '.work/milestone/MILESTONE.md', '.work/milestone/ROADMAP.md'], 'Resolve the authority conflict before routing to plan, execute, or verify.'),
468
+ authority: 'blocked',
469
+ route_kind: 'authority_conflict',
470
+ blocked_by: ['brownfield_change', 'work_milestone'],
471
+ requires_user: false,
472
+ constraints,
473
+ evidence_required: ['One continuity authority must be selected or archived before continuing.'],
474
+ artifacts_to_read: [statePath(context, 'brownfield-change/CHANGE.md'), '.work/milestone/MILESTONE.md', '.work/milestone/ROADMAP.md'],
475
+ repo_warnings: repoWarningsFromControlMap(controlMap),
476
+ privacy_notes: privacyNotes,
477
+ inputs_considered: inputsConsidered,
478
+ inputs_skipped: inputsSkipped,
479
+ trace_refs: traceRefs,
480
+ });
481
+ }
482
+
483
+ if (brownfieldStatus === 'blocked') {
484
+ return packet({
485
+ state: 'blocked',
486
+ reason: 'Active bounded brownfield change is marked blocked; resolve the blocker recorded in CHANGE.md before planning or execution.',
487
+ confidence: 'high',
488
+ next_command: null,
489
+ next_action: manualReviewAction([statePath(context, 'brownfield-change/CHANGE.md'), statePath(context, 'brownfield-change/HANDOFF.md')], 'Resolve the blocker and update the brownfield change status before continuing.'),
490
+ authority: 'brownfield_change',
491
+ route_kind: 'brownfield_change_blocked',
492
+ blocked_by: ['brownfield_change'],
493
+ requires_user: false,
494
+ constraints,
495
+ artifacts_to_read: [statePath(context, 'brownfield-change/CHANGE.md'), statePath(context, 'brownfield-change/HANDOFF.md')],
496
+ artifacts_to_write: [statePath(context, 'brownfield-change/CHANGE.md'), statePath(context, 'brownfield-change/HANDOFF.md')],
497
+ repo_warnings: repoWarningsFromControlMap(controlMap),
498
+ privacy_notes: privacyNotes,
499
+ inputs_considered: inputsConsidered,
500
+ inputs_skipped: inputsSkipped,
501
+ trace_refs: traceRefs,
502
+ });
503
+ }
504
+
505
+ if (brownfieldStatus === 'ready_for_verification') {
506
+ return packet({
507
+ state: 'verify',
508
+ reason: 'Active bounded brownfield change is ready for verification; verify the bounded closeout proof before more planning.',
509
+ confidence: 'high',
510
+ next_command: null,
511
+ next_action: manualReviewAction([statePath(context, 'brownfield-change/CHANGE.md'), statePath(context, 'brownfield-change/VERIFICATION.md')], 'Verify the bounded brownfield change and update VERIFICATION.md.'),
512
+ authority: 'brownfield_change',
513
+ route_kind: 'brownfield_change_verification',
514
+ requires_user: false,
515
+ constraints,
516
+ evidence_required: ['VERIFICATION.md must prove the CHANGE.md done-when and closeout path or record concrete gaps.'],
517
+ artifacts_to_read: [
518
+ statePath(context, 'brownfield-change/CHANGE.md'),
519
+ statePath(context, 'brownfield-change/HANDOFF.md'),
520
+ statePath(context, 'brownfield-change/VERIFICATION.md'),
521
+ ],
522
+ artifacts_to_write: [statePath(context, 'brownfield-change/VERIFICATION.md')],
523
+ repo_warnings: repoWarningsFromControlMap(controlMap),
524
+ privacy_notes: privacyNotes,
525
+ inputs_considered: inputsConsidered,
526
+ inputs_skipped: inputsSkipped,
527
+ trace_refs: traceRefs,
528
+ });
529
+ }
530
+
531
+ if (brownfieldStatus === 'active') {
532
+ return packet({
533
+ state: 'plan',
534
+ reason: 'Active bounded brownfield change exists; route planning through the brownfield change lane before phase roadmap preflight.',
535
+ confidence: 'high',
536
+ next_command: 'gsdd-plan',
537
+ next_action: workflowAction('gsdd-plan', 'Plan the active bounded brownfield change from CHANGE.md and HANDOFF.md without requiring unrelated ROADMAP phase membership.'),
538
+ authority: 'brownfield_change',
539
+ route_kind: 'brownfield_change',
540
+ requires_user: false,
541
+ constraints: [
542
+ ...constraints,
543
+ `Bounded brownfield changes use \`${statePath(context, 'brownfield-change/')}\`, not \`${statePath(context, 'phases/')}\` or ROADMAP checkboxes.`,
544
+ 'Promote to `gsdd-new-project` or `gsdd-new-milestone` only when the change no longer fits one active stream.',
545
+ ],
546
+ evidence_required: ['Plan must preserve the bounded CHANGE.md goal, scope, done-when, next action, and closeout path.'],
547
+ artifacts_to_read: [
548
+ statePath(context, 'brownfield-change/CHANGE.md'),
549
+ statePath(context, 'brownfield-change/HANDOFF.md'),
550
+ statePath(context, 'brownfield-change/VERIFICATION.md'),
551
+ statePath(context, 'SPEC.md'),
552
+ statePath(context, 'ROADMAP.md'),
553
+ ],
554
+ artifacts_to_write: [
555
+ statePath(context, 'brownfield-change/CHANGE.md'),
556
+ statePath(context, 'brownfield-change/HANDOFF.md'),
557
+ ],
558
+ repo_warnings: repoWarningsFromControlMap(controlMap),
559
+ privacy_notes: privacyNotes,
560
+ inputs_considered: inputsConsidered,
561
+ inputs_skipped: inputsSkipped,
562
+ trace_refs: traceRefs,
563
+ });
564
+ }
565
+
426
566
  if (hasUnverifiedSummaries(context.planning.phases)) {
427
- return enrichRoute({ state: 'verify', reason: 'Legacy `.planning` phase summaries exist without matching verification reports.' }, { context, controlMap, constraints, privacyNotes, inputsConsidered, inputsSkipped, traceRefs });
567
+ return enrichRoute({ state: 'verify', reason: `${statePath(context)} phase summaries exist without matching verification reports.` }, { context, controlMap, constraints, privacyNotes, inputsConsidered, inputsSkipped, traceRefs });
428
568
  }
429
569
 
430
570
  const workMilestoneRoute = routeFromWorkMilestone(context, manifest);
@@ -436,14 +576,18 @@ function routeNext(ctx) {
436
576
  if (!legacyComplete) {
437
577
  return packet({
438
578
  state: 'plan',
439
- reason: '`.work/goal.md` exists, but canonical `.planning` lifecycle truth is incomplete; create or refresh the Workspine-native plan from `.work`.',
579
+ reason: `\`.work/goal.md\` exists, but canonical ${statePath(context)} lifecycle truth is incomplete; create or refresh the Workspine-native plan from \`.work\`.`,
440
580
  confidence: context.planning.exists ? 'medium' : 'high',
441
581
  next_command: 'gsdd-plan',
442
582
  next_action: workflowAction('gsdd-plan', 'Plan the Workspine-native milestone from `.work` truth.'),
583
+ authority: 'work',
584
+ route_kind: 'work_native_plan',
585
+ blocked_by: controlMapBlockers(controlMap),
586
+ repo_warnings: repoWarningsFromControlMap(controlMap),
443
587
  requires_user: false,
444
588
  constraints: [
445
589
  ...constraints,
446
- 'Do not infer normal `.planning` milestone progress when SPEC.md, ROADMAP.md, or MILESTONES.md are missing.',
590
+ `Do not infer normal ${statePath(context)} milestone progress when SPEC.md, ROADMAP.md, or MILESTONES.md are missing.`,
447
591
  ],
448
592
  evidence_required: ['Plan must map `.work/goal.md` requirements to implementation and verification artifacts.'],
449
593
  artifacts_to_read: ['.work/goal.md', '.work/research/2026-06-20-long-term-agent-harness-consistency.md'],
@@ -452,9 +596,9 @@ function routeNext(ctx) {
452
596
  inputs_considered: inputsConsidered,
453
597
  inputs_skipped: [
454
598
  ...inputsSkipped,
455
- !context.planning.has_spec ? '.planning/SPEC.md: missing' : null,
456
- !context.planning.has_roadmap ? '.planning/ROADMAP.md: missing' : null,
457
- !context.planning.has_milestones ? '.planning/MILESTONES.md: missing' : null,
599
+ !context.planning.has_spec ? `${statePath(context, 'SPEC.md')}: missing` : null,
600
+ !context.planning.has_roadmap ? `${statePath(context, 'ROADMAP.md')}: missing` : null,
601
+ !context.planning.has_milestones ? `${statePath(context, 'MILESTONES.md')}: missing` : null,
458
602
  ].filter(Boolean),
459
603
  trace_refs: traceRefs,
460
604
  });
@@ -466,9 +610,13 @@ function routeNext(ctx) {
466
610
  confidence: 'medium',
467
611
  next_command: 'gsdd-plan',
468
612
  next_action: workflowAction('gsdd-plan', 'Plan the next approved work slice.'),
613
+ authority: 'planning',
614
+ route_kind: 'phase_plan',
615
+ blocked_by: controlMapBlockers(controlMap),
616
+ repo_warnings: repoWarningsFromControlMap(controlMap),
469
617
  requires_user: false,
470
618
  constraints,
471
- artifacts_to_read: ['.work/goal.md', '.planning/SPEC.md', '.planning/ROADMAP.md', '.planning/MILESTONES.md'],
619
+ artifacts_to_read: ['.work/goal.md', statePath(context, 'SPEC.md'), statePath(context, 'ROADMAP.md'), statePath(context, 'MILESTONES.md')],
472
620
  artifacts_to_write: ['.work/focus/current.md'],
473
621
  privacy_notes: privacyNotes,
474
622
  inputs_considered: inputsConsidered,
@@ -523,7 +671,7 @@ function enrichRoute(route, { context, controlMap, constraints, privacyNotes, in
523
671
  execute: workflowAction('gsdd-execute', 'Execute the approved Workspine plan.'),
524
672
  verify: workflowAction('gsdd-verify', 'Verify executed artifacts against the plan.'),
525
673
  audit: workflowAction('gsdd-audit-milestone', 'Audit milestone-level integration and closure evidence.'),
526
- fix_gaps: workflowAction('gsdd-plan-milestone-gaps', 'Plan gap-fix work from audit or verification findings.'),
674
+ fix_gaps: workflowAction('gsdd-plan', 'Plan amend/extend work from audit or verification findings.'),
527
675
  dogfood: cliAction(['next', 'dogfood', 'capture', '--id', '<id>', '--title', '<text>', '--body', '<text>'], 'Capture one bounded local dogfood finding.'),
528
676
  pause: manualReviewAction(['.work/handoff/current.md'], 'Update handoff before pausing.'),
529
677
  blocked: null,
@@ -537,6 +685,9 @@ function enrichRoute(route, { context, controlMap, constraints, privacyNotes, in
537
685
  confidence: route.state === 'blocked' || route.state === 'ask_user' ? 'high' : 'medium',
538
686
  next_command: route.next_command || actionToLegacyCommand(nextAction),
539
687
  next_action: nextAction,
688
+ authority: route.authority || inferAuthorityForState(route.state, context),
689
+ route_kind: route.route_kind || route.state,
690
+ blocked_by: [...(route.blocked_by || []), ...controlMapBlockers(controlMap)],
540
691
  requires_user: route.state === 'ask_user',
541
692
  questions: route.questions || [],
542
693
  constraints,
@@ -551,6 +702,13 @@ function enrichRoute(route, { context, controlMap, constraints, privacyNotes, in
551
702
  });
552
703
  }
553
704
 
705
+ function inferAuthorityForState(state, context) {
706
+ if (context.milestone?.exists) return 'work_milestone';
707
+ if (hasActiveBrownfieldChange(context)) return 'brownfield_change';
708
+ if (state === 'blocked') return 'blocked';
709
+ return 'planning';
710
+ }
711
+
554
712
  function actionToLegacyCommand(action) {
555
713
  if (!action) return null;
556
714
  if (action.type === 'cli_command') return action.command;
@@ -562,11 +720,11 @@ function defaultReadArtifacts(state, context) {
562
720
  if (state === 'execute') return ['.work/goal.md', '.work/focus/current.md'];
563
721
  if (state === 'verify') return context.milestone?.has_roadmap
564
722
  ? ['.work/goal.md', '.work/milestone/ROADMAP.md', '.work/milestone/phases/*/*-VERIFY.md']
565
- : ['.work/goal.md', '.work/evidence/manifest.json', '.planning/phases/*/*-SUMMARY.md'];
723
+ : ['.work/goal.md', '.work/evidence/manifest.json', statePath(context, 'phases/*/*-SUMMARY.md')];
566
724
  if (state === 'audit') return context.milestone?.has_roadmap
567
725
  ? ['.work/goal.md', '.work/milestone/ROADMAP.md', '.work/milestone/phases/*/*-VERIFY.md']
568
- : ['.work/goal.md', '.work/evidence/manifest.json', '.planning/phases/**/*-VERIFICATION.md'];
569
- if (state === 'fix_gaps') return ['.work/evidence/manifest.json'];
726
+ : ['.work/goal.md', '.work/evidence/manifest.json', statePath(context, 'phases/**/*-VERIFICATION.md')];
727
+ if (state === 'fix_gaps') return ['.work/evidence/manifest.json', '.work/*-MILESTONE-AUDIT.md', '.work/milestone/AUDIT.md'];
570
728
  if (state === 'dogfood') return ['.work/goal.md', '.work/evidence/manifest.json'];
571
729
  if (state === 'pause') return ['.work/handoff/current.md'];
572
730
  if (state === 'complete') return ['.work/goal.md', '.work/evidence/manifest.json', '.work/dogfood/'];
@@ -577,7 +735,7 @@ function defaultReadArtifacts(state, context) {
577
735
  function defaultWriteArtifacts(state) {
578
736
  if (state === 'verify') return ['.work/evidence/manifest.json'];
579
737
  if (state === 'audit') return ['.work/evidence/manifest.json'];
580
- if (state === 'fix_gaps') return ['.work/focus/current.md', '.work/graph/events.jsonl'];
738
+ if (state === 'fix_gaps') return ['.work/ROADMAP.md', '.work/phases/', '.work/focus/current.md', '.work/graph/events.jsonl'];
581
739
  if (state === 'dogfood') return ['.work/dogfood/*.md', '.work/graph/events.jsonl'];
582
740
  if (state === 'pause') return ['.work/handoff/current.md'];
583
741
  return [];
@@ -601,13 +759,78 @@ function findTrustGate(manifest) {
601
759
  };
602
760
  }
603
761
 
762
+ const CARD_WIDTH = 62;
763
+
764
+ const STATE_LABELS = {
765
+ research: 'Look into the problem before planning',
766
+ plan: 'Plan the next piece of work',
767
+ execute: 'Build the planned work',
768
+ verify: 'Prove the last piece of work is done',
769
+ audit: 'Check the whole milestone holds together',
770
+ fix_gaps: 'Plan the gaps that checking found',
771
+ dogfood: 'Use the result and record one honest finding',
772
+ complete: 'Finish and archive the milestone',
773
+ ask_user: 'Answer a question before work can continue',
774
+ pause: 'Work is paused; pick it back up when ready',
775
+ blocked: 'Work is stuck; clear the blocker below',
776
+ };
777
+
778
+ function cardFrame(text) {
779
+ return `│${text.padEnd(CARD_WIDTH)}│`;
780
+ }
781
+
782
+ function pushCardLine(rows, indent, hang, text) {
783
+ const words = String(text).split(/\s+/).filter(Boolean);
784
+ if (words.length === 0) {
785
+ rows.push(cardFrame(' '.repeat(indent)));
786
+ return;
787
+ }
788
+ let prefix = ' '.repeat(indent);
789
+ let line = prefix;
790
+ for (const word of words) {
791
+ const candidate = line === prefix ? line + word : `${line} ${word}`;
792
+ if (candidate.length <= CARD_WIDTH) {
793
+ line = candidate;
794
+ } else {
795
+ rows.push(cardFrame(line));
796
+ prefix = ' '.repeat(hang);
797
+ line = prefix + word;
798
+ }
799
+ }
800
+ rows.push(cardFrame(line));
801
+ }
802
+
803
+ export function renderNextCard(packetValue) {
804
+ const top = `┌${'─'.repeat(CARD_WIDTH)}┐`;
805
+ const rule = `├${'─'.repeat(CARD_WIDTH)}┤`;
806
+ const bottom = `└${'─'.repeat(CARD_WIDTH)}┘`;
807
+ const label = STATE_LABELS[packetValue.state] || packetValue.state;
808
+ const action = (packetValue.next_action ? renderAction(packetValue.next_action) : null)
809
+ || packetValue.next_command
810
+ || '(nothing queued)';
811
+ const waiting = packetValue.requires_user ? 'yes' : 'no';
812
+
813
+ const rows = [top];
814
+ rows.push(cardFrame(' Where things stand'));
815
+ rows.push(rule);
816
+ pushCardLine(rows, 2, 7, `Now: ${label}`);
817
+ pushCardLine(rows, 2, 7, `Why: ${packetValue.reason || ''}`);
818
+ rows.push(cardFrame(''));
819
+ rows.push(cardFrame(' Do this next:'));
820
+ pushCardLine(rows, 4, 4, action);
821
+ rows.push(cardFrame(''));
822
+ rows.push(cardFrame(` Waiting on you: ${waiting}`));
823
+ rows.push(cardFrame(''));
824
+ rows.push(cardFrame(' Stuck? Run: gsdd next --format human'));
825
+ rows.push(cardFrame(''));
826
+ rows.push(cardFrame(' Safety checks — this computer: not set up yet ·'));
827
+ rows.push(cardFrame(' server: not set up yet'));
828
+ rows.push(bottom);
829
+ return rows.join('\n');
830
+ }
831
+
604
832
  function printHuman(packetValue) {
605
- console.log(`gsdd next: ${packetValue.state}`);
606
- console.log(`Why: ${packetValue.reason}`);
607
- if (packetValue.next_action) console.log(`Next: ${renderAction(packetValue.next_action)}`);
608
- else if (packetValue.next_command) console.log(`Next: ${packetValue.next_command}`);
609
- if (packetValue.requires_user) console.log('Approval: required');
610
- else console.log('Approval: not required');
833
+ console.log(renderNextCard(packetValue));
611
834
  if (packetValue.questions.length > 0) {
612
835
  console.log('\nQuestions:');
613
836
  for (const question of packetValue.questions) {