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
@@ -5,16 +5,12 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
5
5
  import { spawnSync } from 'child_process';
6
6
  import { output, parseFlagValue } from './cli-utils.mjs';
7
7
  import { evaluateLifecycleState } from './lifecycle-state.mjs';
8
- import { checkDrift } from './session-fingerprint.mjs';
8
+ import { resolveStateDir } from './state-dir.mjs';
9
9
  import { resolveWorkspaceContext } from './workspace-root.mjs';
10
10
 
11
- const DEFAULT_ANNOTATIONS_RELATIVE_PATH = '.planning/.local/control-map.annotations.json';
12
11
  const MAX_DIRTY_BUCKET_ENTRIES = 200;
13
12
  const CLEANUP_STATES = Object.freeze(['active', 'paused', 'merged', 'abandoned', 'superseded', 'cleanup_deferred']);
14
13
  const ACTIVE_ANNOTATION_STATES = Object.freeze(['active', 'paused', 'cleanup_deferred']);
15
- const CONTROL_MAP_USAGE = 'Usage: gsdd control-map [--json] [--with-ignored] [--annotations <path>]';
16
- const CONTROL_MAP_ANNOTATE_SET_USAGE = 'Usage: gsdd control-map annotate set [--id <id>] [--path <worktree>] --write-set <paths> [--owner <runtime>] [--scope <text>] [--cleanup-state <state>] [--next-step <text>] [--refresh] [--annotations <path>]';
17
- const CONTROL_MAP_ANNOTATE_CLEAR_USAGE = 'Usage: gsdd control-map annotate clear (--id <id> | --path <worktree>) [--annotations <path>]';
18
14
  const AUTHORITY_ORDER = Object.freeze([
19
15
  'repo_truth',
20
16
  'planning_artifacts',
@@ -241,7 +237,7 @@ function findDirtyWriteSetOverlaps(writeEntries, dirtyEntries) {
241
237
  if (!pathsIntersect(writeEntry.repo_path, dirtyEntry.repo_path)) continue;
242
238
  overlaps.push({
243
239
  annotation_id: writeEntry.annotation_id,
244
- annotated_worktree_id: writeEntry.worktree_id,
240
+ classified_worktree_id: writeEntry.worktree_id,
245
241
  write_path: writeEntry.repo_path,
246
242
  dirty_worktree_id: dirtyEntry.worktree_id,
247
243
  dirty_path: dirtyEntry.repo_path,
@@ -497,383 +493,6 @@ function loadAnnotations(workspaceRoot, planningDir, annotationPathArg = null) {
497
493
  };
498
494
  }
499
495
 
500
- function parseAnnotationMutationFlags(args, usage, { valueFlags, booleanFlags = [] }) {
501
- const allowedValueFlags = new Set(valueFlags);
502
- const allowedBooleanFlags = new Set(booleanFlags);
503
- const values = new Map();
504
- const listValues = new Map([['--write-set', []]]);
505
- const booleans = new Set();
506
- const unknown = [];
507
-
508
- for (let index = 0; index < args.length; index += 1) {
509
- const arg = args[index];
510
- if (allowedBooleanFlags.has(arg)) {
511
- booleans.add(arg);
512
- continue;
513
- }
514
- if (!allowedValueFlags.has(arg)) {
515
- unknown.push(arg);
516
- continue;
517
- }
518
-
519
- const value = args[index + 1];
520
- if (!value || value.startsWith('--')) {
521
- return { valid: false, usage, error: `Missing value for ${arg}.` };
522
- }
523
- if (arg === '--write-set') listValues.get(arg).push(value);
524
- else values.set(arg, value);
525
- index += 1;
526
- }
527
-
528
- if (unknown.length > 0) {
529
- return { valid: false, usage, error: `Unsupported argument(s): ${unknown.join(', ')}` };
530
- }
531
-
532
- return {
533
- valid: true,
534
- values,
535
- listValues,
536
- booleans,
537
- };
538
- }
539
-
540
- function parseWriteSet(values, worktreePath) {
541
- const entries = [];
542
- for (const rawValue of values || []) {
543
- for (const part of String(rawValue).split(',')) {
544
- const normalized = normalizeRepoPath(part, worktreePath);
545
- if (normalized && !entries.includes(normalized)) entries.push(normalized);
546
- }
547
- }
548
- return entries;
549
- }
550
-
551
- function annotationPathLabelForWrite(workspaceRoot, worktreePath) {
552
- return workspacePathLabel(workspaceRoot, worktreePath);
553
- }
554
-
555
- function annotationIdForWorktree(worktree) {
556
- if (worktree.id === '.') return 'canonical';
557
- return worktree.id;
558
- }
559
-
560
- function serializeAnnotationEntry(workspaceRoot, entry) {
561
- const serialized = {
562
- id: entry.id,
563
- path: entry.path || annotationPathLabelForWrite(workspaceRoot, entry.normalized_path),
564
- cleanup_state: entry.cleanup_state || 'active',
565
- };
566
- if (entry.runtime_owner) serialized.runtime_owner = entry.runtime_owner;
567
- if (entry.branch) serialized.branch = entry.branch;
568
- if (entry.last_known_head) serialized.last_known_head = entry.last_known_head;
569
- if (entry.intended_scope) serialized.intended_scope = entry.intended_scope;
570
- if (Array.isArray(entry.write_set) && entry.write_set.length > 0) serialized.write_set = entry.write_set;
571
- if (entry.next_step) serialized.next_step = entry.next_step;
572
- if (entry.updated_at) serialized.updated_at = entry.updated_at;
573
- return serialized;
574
- }
575
-
576
- function writeAnnotationsDocument(workspaceRoot, annotationFile, worktrees) {
577
- mkdirSync(dirname(annotationFile.path), { recursive: true });
578
- const document = {
579
- schema_version: 1,
580
- worktrees: worktrees.map((entry) => serializeAnnotationEntry(workspaceRoot, entry)),
581
- };
582
- writeFileSync(annotationFile.path, `${JSON.stringify(document, null, 2)}\n`);
583
- }
584
-
585
- function buildMutationContext(context, annotationPathArg) {
586
- const annotationFile = resolveAnnotationFilePath(context.workspaceRoot, context.planningDir, annotationPathArg);
587
- if (!annotationFile.inside_workspace) {
588
- return {
589
- ok: false,
590
- result: {
591
- status: 'blocked',
592
- reason: 'annotations_path_outside_workspace',
593
- annotations_path: annotationFile.label,
594
- message: 'Control-map annotations path must stay inside the workspace.',
595
- },
596
- };
597
- }
598
-
599
- const annotations = loadAnnotations(context.workspaceRoot, context.planningDir, annotationPathArg);
600
- if (!annotations.valid) {
601
- return {
602
- ok: false,
603
- result: {
604
- status: 'blocked',
605
- reason: 'invalid_annotations',
606
- annotations_path: annotations.path,
607
- errors: annotations.errors,
608
- },
609
- };
610
- }
611
-
612
- const discovered = discoverGitWorktrees(context.workspaceRoot);
613
- return {
614
- ok: true,
615
- annotationFile,
616
- annotations,
617
- worktrees: discovered.worktrees,
618
- };
619
- }
620
-
621
- function findWorktreeByPath(worktrees, pathValue) {
622
- const normalized = normalizeSlashes(resolve(pathValue));
623
- return worktrees.find((worktree) => normalizeSlashes(resolve(worktree.path)) === normalized) || null;
624
- }
625
-
626
- function findAnnotationIndex(annotations, { id = null, normalizedPath = null } = {}) {
627
- if (id) {
628
- const byId = annotations.worktrees.findIndex((entry) => entry.id === id);
629
- if (byId !== -1) return byId;
630
- }
631
- if (normalizedPath) {
632
- return annotations.worktrees.findIndex((entry) => (
633
- entry.normalized_path && normalizeSlashes(resolve(entry.normalized_path)) === normalizeSlashes(resolve(normalizedPath))
634
- ));
635
- }
636
- return -1;
637
- }
638
-
639
- function staleAnnotationIssues(annotation, worktree) {
640
- const issues = [];
641
- if (annotation.branch && worktree.branch && annotation.branch !== worktree.branch) {
642
- issues.push({
643
- code: 'branch_mismatch',
644
- saved: annotation.branch,
645
- live: worktree.branch,
646
- });
647
- }
648
- if (annotation.last_known_head && worktree.head && annotation.last_known_head !== worktree.head) {
649
- issues.push({
650
- code: 'head_mismatch',
651
- saved: annotation.last_known_head,
652
- live: worktree.head,
653
- });
654
- }
655
- return issues;
656
- }
657
-
658
- function annotationMutationSummary(annotation) {
659
- return {
660
- id: annotation.id,
661
- path: annotation.path,
662
- runtime_owner: annotation.runtime_owner || null,
663
- branch: annotation.branch || null,
664
- last_known_head: annotation.last_known_head || null,
665
- intended_scope: annotation.intended_scope || null,
666
- write_set: annotation.write_set || [],
667
- cleanup_state: annotation.cleanup_state || 'active',
668
- next_step: annotation.next_step || null,
669
- updated_at: annotation.updated_at || null,
670
- };
671
- }
672
-
673
- function setAnnotation(context, args) {
674
- const parsed = parseAnnotationMutationFlags(args, CONTROL_MAP_ANNOTATE_SET_USAGE, {
675
- valueFlags: [
676
- '--annotations',
677
- '--cleanup-state',
678
- '--id',
679
- '--next-step',
680
- '--owner',
681
- '--path',
682
- '--runtime-owner',
683
- '--scope',
684
- '--write-set',
685
- ],
686
- booleanFlags: ['--refresh'],
687
- });
688
- if (!parsed.valid) return { ok: false, result: { status: 'blocked', reason: 'invalid_arguments', message: parsed.error, usage: parsed.usage } };
689
-
690
- const annotationPathArg = parsed.values.get('--annotations') || null;
691
- const mutation = buildMutationContext(context, annotationPathArg);
692
- if (!mutation.ok) return mutation;
693
-
694
- const explicitId = parsed.values.get('--id') || null;
695
- const pathValue = parsed.values.get('--path') || '.';
696
- const targetPath = resolve(context.workspaceRoot, pathValue);
697
- const worktree = findWorktreeByPath(mutation.worktrees, targetPath);
698
- if (!worktree || !worktree.git_valid) {
699
- return {
700
- ok: false,
701
- result: {
702
- status: 'blocked',
703
- reason: 'worktree_not_found',
704
- message: `No live git worktree found for ${pathValue}.`,
705
- path: workspacePathLabel(context.workspaceRoot, targetPath),
706
- },
707
- };
708
- }
709
-
710
- const writeSet = parseWriteSet(parsed.listValues.get('--write-set'), worktree.path);
711
- if (writeSet.length === 0) {
712
- return {
713
- ok: false,
714
- result: {
715
- status: 'blocked',
716
- reason: 'missing_write_set',
717
- message: 'Annotation set requires at least one --write-set value.',
718
- usage: CONTROL_MAP_ANNOTATE_SET_USAGE,
719
- },
720
- };
721
- }
722
-
723
- const normalizedPath = normalizeSlashes(resolve(worktree.path));
724
- const existingIndex = findAnnotationIndex(mutation.annotations, { id: explicitId, normalizedPath });
725
- const existing = existingIndex === -1 ? null : mutation.annotations.worktrees[existingIndex];
726
-
727
- const cleanupState = parsed.values.get('--cleanup-state') || existing?.cleanup_state || 'active';
728
- if (!CLEANUP_STATES.includes(cleanupState)) {
729
- return {
730
- ok: false,
731
- result: {
732
- status: 'blocked',
733
- reason: 'invalid_cleanup_state',
734
- message: `Unsupported cleanup_state: ${cleanupState}`,
735
- supported_cleanup_states: CLEANUP_STATES,
736
- },
737
- };
738
- }
739
-
740
- const staleIssues = existing ? staleAnnotationIssues(existing, worktree) : [];
741
- const refresh = parsed.booleans.has('--refresh');
742
- if (staleIssues.length > 0 && !refresh) {
743
- return {
744
- ok: false,
745
- result: {
746
- operation: 'control-map annotate set',
747
- status: 'blocked',
748
- reason: 'stale_annotation',
749
- annotations_path: mutation.annotationFile.label,
750
- annotation_id: existing.id,
751
- stale_issues: staleIssues,
752
- message: 'Existing annotation is stale against live worktree truth; rerun with --refresh to update it or use annotate clear to remove it.',
753
- },
754
- };
755
- }
756
-
757
- const now = new Date().toISOString();
758
- const annotation = {
759
- id: explicitId || existing?.id || annotationIdForWorktree(worktree),
760
- path: annotationPathLabelForWrite(context.workspaceRoot, worktree.path),
761
- runtime_owner: parsed.values.get('--runtime-owner') || parsed.values.get('--owner') || existing?.runtime_owner || null,
762
- branch: worktree.branch,
763
- last_known_head: worktree.head,
764
- intended_scope: parsed.values.get('--scope') || existing?.intended_scope || null,
765
- write_set: writeSet,
766
- cleanup_state: cleanupState,
767
- next_step: parsed.values.get('--next-step') || existing?.next_step || null,
768
- updated_at: now,
769
- };
770
- const nextWorktrees = mutation.annotations.worktrees.slice();
771
- if (existingIndex === -1) nextWorktrees.push(annotation);
772
- else nextWorktrees[existingIndex] = annotation;
773
- writeAnnotationsDocument(context.workspaceRoot, mutation.annotationFile, nextWorktrees);
774
-
775
- return {
776
- ok: true,
777
- result: {
778
- operation: 'control-map annotate set',
779
- changed: true,
780
- status: existing ? (refresh && staleIssues.length > 0 ? 'refreshed' : 'updated') : 'created',
781
- annotations_path: mutation.annotationFile.label,
782
- annotation: annotationMutationSummary(annotation),
783
- stale_check: {
784
- status: staleIssues.length > 0 ? 'refreshed' : 'passed',
785
- issues: staleIssues,
786
- },
787
- },
788
- };
789
- }
790
-
791
- function clearAnnotation(context, args) {
792
- const parsed = parseAnnotationMutationFlags(args, CONTROL_MAP_ANNOTATE_CLEAR_USAGE, {
793
- valueFlags: ['--annotations', '--id', '--path'],
794
- });
795
- if (!parsed.valid) return { ok: false, result: { status: 'blocked', reason: 'invalid_arguments', message: parsed.error, usage: parsed.usage } };
796
-
797
- const id = parsed.values.get('--id') || null;
798
- const pathValue = parsed.values.get('--path') || null;
799
- if (!id && !pathValue) {
800
- return {
801
- ok: false,
802
- result: {
803
- status: 'blocked',
804
- reason: 'missing_selector',
805
- message: 'Annotation clear requires --id or --path.',
806
- usage: CONTROL_MAP_ANNOTATE_CLEAR_USAGE,
807
- },
808
- };
809
- }
810
-
811
- const annotationPathArg = parsed.values.get('--annotations') || null;
812
- const mutation = buildMutationContext(context, annotationPathArg);
813
- if (!mutation.ok) return mutation;
814
- if (!mutation.annotations.exists) {
815
- return {
816
- ok: true,
817
- result: {
818
- operation: 'control-map annotate clear',
819
- changed: false,
820
- status: 'missing',
821
- annotations_path: mutation.annotationFile.label,
822
- },
823
- };
824
- }
825
-
826
- const normalizedPath = pathValue ? normalizeSlashes(resolve(context.workspaceRoot, pathValue)) : null;
827
- const existingIndex = findAnnotationIndex(mutation.annotations, { id, normalizedPath });
828
- if (existingIndex === -1) {
829
- return {
830
- ok: true,
831
- result: {
832
- operation: 'control-map annotate clear',
833
- changed: false,
834
- status: 'not_found',
835
- annotations_path: mutation.annotationFile.label,
836
- selector: id ? { id } : { path: workspacePathLabel(context.workspaceRoot, normalizedPath) },
837
- },
838
- };
839
- }
840
-
841
- const removed = mutation.annotations.worktrees[existingIndex];
842
- const nextWorktrees = mutation.annotations.worktrees.filter((_, index) => index !== existingIndex);
843
- writeAnnotationsDocument(context.workspaceRoot, mutation.annotationFile, nextWorktrees);
844
-
845
- return {
846
- ok: true,
847
- result: {
848
- operation: 'control-map annotate clear',
849
- changed: true,
850
- status: 'cleared',
851
- annotations_path: mutation.annotationFile.label,
852
- annotation: annotationMutationSummary(removed),
853
- },
854
- };
855
- }
856
-
857
- function cmdControlMapAnnotate(context, args) {
858
- const [operation, ...operationArgs] = args;
859
- let result;
860
- if (operation === 'set') result = setAnnotation(context, operationArgs);
861
- else if (operation === 'clear') result = clearAnnotation(context, operationArgs);
862
- else {
863
- result = {
864
- ok: false,
865
- result: {
866
- status: 'blocked',
867
- reason: 'invalid_operation',
868
- message: 'Usage: gsdd control-map annotate <set|clear> ...',
869
- },
870
- };
871
- }
872
-
873
- output(result.result);
874
- if (!result.ok) process.exitCode = 1;
875
- }
876
-
877
496
  function reconcileAnnotations(worktrees, annotations) {
878
497
  const warnings = [];
879
498
  const byPath = new Map(worktrees.map((entry) => [normalizeSlashes(resolve(entry.path)), entry]));
@@ -923,7 +542,7 @@ function reconcileAnnotations(worktrees, annotations) {
923
542
  function classifyWorkflowState(planningDir) {
924
543
  const lifecycle = evaluateLifecycleState({ planningDir });
925
544
  const checkpointPath = join(planningDir, '.continue-here.md');
926
- const drift = existsSync(planningDir) ? checkDrift(planningDir) : { drifted: false, details: [] };
545
+ const workspaceRoot = resolve(planningDir, '..');
927
546
  const milestoneVersion = lifecycle.currentMilestone?.version || null;
928
547
  const milestoneTitle = lifecycle.currentMilestone?.title || null;
929
548
  const milestone = milestoneVersion || milestoneTitle
@@ -936,11 +555,7 @@ function classifyWorkflowState(planningDir) {
936
555
  non_phase_state: lifecycle.nonPhaseState || null,
937
556
  checkpoint: {
938
557
  exists: existsSync(checkpointPath),
939
- path: '.planning/.continue-here.md',
940
- },
941
- planning_drift: {
942
- drifted: drift.drifted,
943
- details: drift.details || [],
558
+ path: workspacePathLabel(workspaceRoot, checkpointPath),
944
559
  },
945
560
  };
946
561
  }
@@ -975,7 +590,7 @@ function addBranchStateRisks(risks, worktree, { canonical = false } = {}) {
975
590
  }
976
591
  }
977
592
 
978
- function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtimeDirs, workflowState, gitErrors }) {
593
+ function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtimeDirs, workflowState, gitErrors, helperCommand }) {
979
594
  const risks = [];
980
595
  const writeEntries = annotationWriteEntries(worktrees, rawAnnotations || { worktrees: [] });
981
596
  const dirtyEntries = worktrees.flatMap((worktree) => dirtyRepoPathEntries(worktree));
@@ -988,7 +603,7 @@ function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtime
988
603
  case 'canonical_git_invalid':
989
604
  case 'worktree_git_invalid': {
990
605
  const targetPath = risk.worktree_id || canonical.path;
991
- return `Run \`git config --global --add safe.directory ${targetPath}\`, then re-run \`gsdd control-map --json\`.`;
606
+ return `Run \`git config --global --add safe.directory ${targetPath}\`, then re-run \`${helperCommand}\`.`;
992
607
  }
993
608
  case 'canonical_dirty':
994
609
  return 'Commit, stash, or checkpoint the canonical changes before planning, cleanup, merge, or broad execution.';
@@ -1002,14 +617,12 @@ function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtime
1002
617
  case 'detached_candidate_worktree':
1003
618
  return 'Classify the detached worktree intent (active vs abandoned) before using it for execution or cleanup decisions.';
1004
619
  case 'sibling_worktree_dirty':
1005
- case 'unannotated_candidate_worktree':
620
+ case 'unclassified_candidate_worktree':
1006
621
  return 'Review sibling worktree ownership and write set before starting overlapping implementation.';
1007
622
  case 'write_set_overlap':
1008
623
  return 'Resolve overlapping local annotation write sets before starting another owned-write workflow.';
1009
624
  case 'dirty_path_write_set_overlap':
1010
- return 'Checkpoint or classify dirty paths that overlap annotated write sets before owned-write transitions.';
1011
- case 'planning_state_drift':
1012
- return 'Review drift and rebaseline with session-fingerprint only after confirming the planning changes are intentional.';
625
+ return 'Checkpoint or classify dirty paths that overlap classified write sets before owned-write transitions.';
1013
626
  default:
1014
627
  return null;
1015
628
  }
@@ -1091,7 +704,7 @@ function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtime
1091
704
  }
1092
705
  if (!worktree.annotation && (worktree.dirty.counts.tracked > 0 || worktree.dirty.counts.untracked > 0 || worktree.detached)) {
1093
706
  const risk = {
1094
- code: 'unannotated_candidate_worktree',
707
+ code: 'unclassified_candidate_worktree',
1095
708
  severity: 'info',
1096
709
  worktree_id: worktree.id,
1097
710
  message: `Worktree ${worktree.id} has candidate-work signals but no local control-map annotation.`,
@@ -1115,7 +728,7 @@ function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtime
1115
728
  const risk = {
1116
729
  code: 'dirty_path_write_set_overlap',
1117
730
  severity: 'block',
1118
- message: `Live dirty paths overlap annotated write sets (${dirtyWriteSetOverlaps.length} overlap(s)).`,
731
+ message: `Live dirty paths overlap classified write sets (${dirtyWriteSetOverlaps.length} overlap(s)).`,
1119
732
  overlaps: dirtyWriteSetOverlaps.slice(0, MAX_DIRTY_BUCKET_ENTRIES),
1120
733
  omitted_count: Math.max(0, dirtyWriteSetOverlaps.length - MAX_DIRTY_BUCKET_ENTRIES),
1121
734
  };
@@ -1134,16 +747,6 @@ function buildRisks({ canonical, worktrees, annotations, rawAnnotations, runtime
1134
747
  path: runtimeDir.path_relative,
1135
748
  });
1136
749
  }
1137
- if (workflowState.planning_drift.drifted) {
1138
- const risk = {
1139
- code: 'planning_state_drift',
1140
- severity: 'warn',
1141
- message: `Planning state drifted since the last fingerprint: ${workflowState.planning_drift.details.join('; ')}`,
1142
- };
1143
- risk.fix_hint = fixHintForRisk(risk);
1144
- risks.push(risk);
1145
- }
1146
-
1147
750
  // Ensure the common closure risks expose actionable fix guidance even when
1148
751
  // the originating helper (for example branch-state risks) didn't attach it.
1149
752
  for (const risk of risks) {
@@ -1160,14 +763,13 @@ function buildInterventions(risks) {
1160
763
  const interventions = [];
1161
764
  if (codes.has('canonical_git_invalid')) interventions.push('Fix git/safe.directory access before mutating this checkout.');
1162
765
  if (codes.has('write_set_overlap')) interventions.push('Resolve overlapping local annotation write sets before starting another owned-write workflow.');
1163
- if (codes.has('dirty_path_write_set_overlap')) interventions.push('Checkpoint, commit, stash, or explicitly classify dirty paths that overlap annotated write sets before owned-write transitions.');
766
+ if (codes.has('dirty_path_write_set_overlap')) interventions.push('Checkpoint, commit, stash, or explicitly classify dirty paths that overlap classified write sets before owned-write transitions.');
1164
767
  if (codes.has('canonical_dirty_behind_upstream')) interventions.push('Sync the canonical branch or preserve dirty canonical work before mutating a stale checkout.');
1165
768
  if (codes.has('canonical_branch_behind_upstream') || codes.has('canonical_branch_diverged_upstream') || codes.has('worktree_branch_behind_upstream') || codes.has('worktree_branch_diverged_upstream')) interventions.push('Review upstream divergence before relying on stale branch state for implementation decisions.');
1166
769
  if (codes.has('detached_candidate_worktree')) interventions.push('Classify detached worktree intent before using it for execution or cleanup decisions.');
1167
770
  if (codes.has('canonical_dirty')) interventions.push('Checkpoint or classify canonical dirty work before planning, cleanup, merge, or broad execution.');
1168
- if (codes.has('sibling_worktree_dirty') || codes.has('unannotated_candidate_worktree')) interventions.push('Review sibling worktree ownership and write set before starting overlapping implementation.');
771
+ if (codes.has('sibling_worktree_dirty') || codes.has('unclassified_candidate_worktree')) interventions.push('Review sibling worktree ownership and write set before starting overlapping implementation.');
1169
772
  if (codes.has('stale_annotation_missing_worktree') || codes.has('stale_annotation_branch_mismatch') || codes.has('stale_annotation_head_mismatch')) interventions.push('Refresh or remove stale local control-map annotations after reviewing repo truth.');
1170
- if (codes.has('planning_state_drift')) interventions.push('Review planning drift and rebaseline with session-fingerprint only after confirming the changes are intentional.');
1171
773
  if (interventions.length === 0) interventions.push('No control-map intervention required before read-only status work.');
1172
774
  return interventions;
1173
775
  }
@@ -1180,7 +782,9 @@ export function buildControlMap({
1180
782
  now = new Date(),
1181
783
  } = {}) {
1182
784
  const root = resolve(workspaceRoot || process.cwd());
1183
- const planning = planningDir || join(root, '.planning');
785
+ const planning = planningDir || resolveStateDir(root).dir;
786
+ const defaultAnnotationsPath = resolveAnnotationFilePath(root, planning).label;
787
+ const helperCommand = `node ${workspacePathLabel(root, join(planning, 'bin', 'gsdd.mjs'))} control-map --json`;
1184
788
  const discovered = discoverGitWorktrees(root, { includeIgnoredPaths, includeIgnoredCount: includeIgnoredPaths });
1185
789
  const annotations = loadAnnotations(root, planning, annotationPath);
1186
790
  const reconciled = reconcileAnnotations(discovered.worktrees, annotations);
@@ -1204,6 +808,7 @@ export function buildControlMap({
1204
808
  runtimeDirs,
1205
809
  workflowState,
1206
810
  gitErrors: discovered.errors,
811
+ helperCommand,
1207
812
  });
1208
813
 
1209
814
  return {
@@ -1212,7 +817,7 @@ export function buildControlMap({
1212
817
  operation: 'control-map',
1213
818
  repo_root_id: canonical.git_top_level || normalizeSlashes(root),
1214
819
  workspace_root: normalizeSlashes(root),
1215
- default_annotations_path: DEFAULT_ANNOTATIONS_RELATIVE_PATH,
820
+ default_annotations_path: defaultAnnotationsPath,
1216
821
  authority: AUTHORITY_ORDER,
1217
822
  canonical_worktree: canonical,
1218
823
  worktrees: reconciled.worktrees,
@@ -1223,79 +828,3 @@ export function buildControlMap({
1223
828
  interventions: buildInterventions(risks),
1224
829
  };
1225
830
  }
1226
-
1227
- function printHuman(map) {
1228
- const milestone = map.workflow_state.current_milestone
1229
- ? [map.workflow_state.current_milestone.version, map.workflow_state.current_milestone.title].filter(Boolean).join(' ')
1230
- : 'none';
1231
- const ignoredLabel = map.canonical_worktree.dirty.ignored_count_included
1232
- ? String(map.canonical_worktree.dirty.counts.ignored)
1233
- : 'not scanned';
1234
- console.log('gsdd control-map - computed workspace control map\n');
1235
- console.log(`Workspace: ${map.workspace_root}`);
1236
- console.log(`Authority: ${map.authority.join(' > ')}`);
1237
- console.log(`Canonical: ${map.canonical_worktree.branch || 'unknown'} @ ${map.canonical_worktree.head || 'unknown'}`);
1238
- console.log(`Workflow: milestone=${milestone}, phase=${map.workflow_state.current_phase || 'none'}, next=${map.workflow_state.next_phase || 'none'}`);
1239
- console.log(`Checkpoint: ${map.workflow_state.checkpoint.path} (${map.workflow_state.checkpoint.exists ? 'present' : 'missing'})`);
1240
- console.log(`Dirty buckets: ${map.canonical_worktree.dirty.counts.tracked} tracked, ${map.canonical_worktree.dirty.counts.untracked} untracked, ${ignoredLabel} ignored`);
1241
- console.log(`Worktrees: ${map.worktrees.length}`);
1242
- for (const worktree of map.worktrees) {
1243
- const marker = worktree.path === map.canonical_worktree.path ? '*' : '-';
1244
- const annotation = worktree.annotation ? ` owner=${worktree.annotation.runtime_owner || 'unspecified'} cleanup=${worktree.annotation.cleanup_state}` : '';
1245
- const ignoredCount = worktree.dirty.ignored_count_included ? String(worktree.dirty.counts.ignored) : 'not-scanned';
1246
- console.log(` ${marker} ${worktree.id} branch=${worktree.branch || 'unknown'} dirty=${worktree.dirty.counts.tracked}/${worktree.dirty.counts.untracked}/${ignoredCount}${annotation}`);
1247
- }
1248
- if (map.risks.length > 0) {
1249
- console.log('\nRisks:');
1250
- for (const risk of map.risks) {
1251
- console.log(` - [${risk.severity || 'info'}] ${risk.code}: ${risk.message}`);
1252
- if (risk.fix_hint) console.log(` Fix: ${risk.fix_hint}`);
1253
- }
1254
- }
1255
- console.log('\nInterventions:');
1256
- for (const intervention of map.interventions) console.log(` - ${intervention}`);
1257
- }
1258
-
1259
- export function cmdControlMap(...args) {
1260
- const jsonMode = args.includes('--json');
1261
- const contextArgs = args.filter((arg) => arg !== '--json');
1262
- const context = resolveWorkspaceContext(contextArgs);
1263
- if (context.invalid) {
1264
- console.error(context.error);
1265
- process.exitCode = 1;
1266
- return;
1267
- }
1268
-
1269
- if (context.args[0] === 'annotate') {
1270
- cmdControlMapAnnotate(context, context.args.slice(1));
1271
- return;
1272
- }
1273
-
1274
- const annotations = parseFlagValue(context.args, '--annotations');
1275
- if (annotations.invalid) {
1276
- console.error(CONTROL_MAP_USAGE);
1277
- process.exitCode = 1;
1278
- return;
1279
- }
1280
- const filteredArgs = context.args.filter((arg, index) => {
1281
- if (arg === '--annotations') return false;
1282
- if (index > 0 && context.args[index - 1] === '--annotations') return false;
1283
- if (arg === '--with-ignored') return false;
1284
- return true;
1285
- });
1286
- if (filteredArgs.length > 0) {
1287
- console.error(CONTROL_MAP_USAGE);
1288
- process.exitCode = 1;
1289
- return;
1290
- }
1291
-
1292
- const includeIgnoredPaths = context.args.includes('--with-ignored');
1293
- const map = buildControlMap({
1294
- workspaceRoot: context.workspaceRoot,
1295
- planningDir: context.planningDir,
1296
- annotationPath: annotations.value,
1297
- includeIgnoredPaths,
1298
- });
1299
- if (jsonMode) output(map);
1300
- else printHuman(map);
1301
- }
@@ -5,11 +5,15 @@ import {
5
5
  getRuntimeFreshnessRepairGuidance,
6
6
  summarizeRuntimeFreshnessIssues,
7
7
  } from './runtime-freshness.mjs';
8
- import { checkDrift } from './session-fingerprint.mjs';
9
8
 
10
- export const TRUTH_CHECK_IDS = ['W7', 'W8', 'W9', 'W10', 'W11', 'W12'];
9
+ export const TRUTH_CHECK_IDS = ['W7', 'W8', 'W9', 'W10', 'W11'];
10
+
11
+ function statePath(stateDirName, relativePath = '') {
12
+ return relativePath ? `${stateDirName}/${relativePath}` : stateDirName;
13
+ }
11
14
 
12
15
  export function runTruthChecks(planningDir, frameworkDir, actualCheckIds, options = {}) {
16
+ const stateDirName = options.stateDirName || '.work';
13
17
  const warnings = [];
14
18
  const designPath = join(frameworkDir, 'distilled', 'DESIGN.md');
15
19
  const readmePath = join(frameworkDir, 'distilled', 'README.md');
@@ -85,7 +89,7 @@ export function runTruthChecks(planningDir, frameworkDir, actualCheckIds, option
85
89
  id: 'W10',
86
90
  severity: 'WARN',
87
91
  message: `ROADMAP/SPEC requirement status drift (${mismatches.join('; ')})`,
88
- fix: 'Reconcile .planning/ROADMAP.md phase completion markers with .planning/SPEC.md requirement checkboxes',
92
+ fix: `Reconcile ${statePath(stateDirName, 'ROADMAP.md')} phase completion markers with ${statePath(stateDirName, 'SPEC.md')} requirement checkboxes`,
89
93
  });
90
94
  }
91
95
  }
@@ -99,16 +103,6 @@ export function runTruthChecks(planningDir, frameworkDir, actualCheckIds, option
99
103
  });
100
104
  }
101
105
 
102
- const drift = checkDrift(planningDir);
103
- if (drift.drifted) {
104
- warnings.push({
105
- id: 'W12',
106
- severity: 'WARN',
107
- message: `Planning state drifted since last recorded session (${drift.details.join('; ')})`,
108
- fix: 'Review the changed planning files. If the drift is intentional, rebaseline with `node .planning/bin/gsdd.mjs session-fingerprint write`, then rerun the blocked lifecycle preflight.',
109
- });
110
- }
111
-
112
106
  return warnings;
113
107
  }
114
108
 
@@ -156,6 +150,7 @@ function extractRepoLocalPaths(content) {
156
150
  'CHANGELOG.md',
157
151
  'SPEC.md',
158
152
  'package.json',
153
+ '.work/',
159
154
  '.planning/',
160
155
  '.internal-research/',
161
156
  '.agents/',