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
@@ -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
- }
@@ -102,14 +102,22 @@ function validateGlobalTools(tools) {
102
102
  return `ERROR: unsupported global install target(s): ${invalid.join(', ')}. Use --tools claude,opencode,codex,copilot or --tools all.`;
103
103
  }
104
104
 
105
+ function detectGlobalInstallTargets(roots) {
106
+ return GLOBAL_AGENT_IDS.filter((target) => existsSync(roots[target]));
107
+ }
108
+
105
109
  function displayPath(filePath) {
106
110
  return filePath.replace(/\\/g, '/');
107
111
  }
108
112
 
109
- async function resolveGlobalInstallTargets({ args, promptApi, output }) {
113
+ async function resolveGlobalInstallTargets({ args, promptApi, output, roots }) {
110
114
  const parsedTools = normalizeGlobalTools(parseGlobalToolsFlag(args));
111
115
  if (parsedTools.length > 0) return parsedTools;
112
116
 
117
+ if (args.includes('--auto')) {
118
+ return detectGlobalInstallTargets(roots);
119
+ }
120
+
113
121
  if (!process.stdin.isTTY) {
114
122
  return [];
115
123
  }
@@ -530,6 +538,7 @@ export function createCmdInstall(ctx) {
530
538
  return async function cmdInstall(...installArgs) {
531
539
  const globalFlag = installArgs.includes('--global') || installArgs.includes('-g');
532
540
  const localFlag = installArgs.includes('--local');
541
+ const autoFlag = installArgs.includes('--auto');
533
542
  const dryRun = installArgs.includes('--dry');
534
543
  const verifyRuntime = installArgs.includes('--verify-runtime');
535
544
  const liveRuntime = installArgs.includes('--live-runtime');
@@ -559,14 +568,19 @@ export function createCmdInstall(ctx) {
559
568
  return;
560
569
  }
561
570
 
571
+ const roots = resolveGlobalInstallRoots();
562
572
  const targets = await resolveGlobalInstallTargets({
563
573
  args: installArgs,
564
574
  promptApi: ctx.globalInstallPromptApi,
565
575
  output: process.stdout,
576
+ roots,
566
577
  });
567
578
 
568
579
  if (targets.length === 0) {
569
- console.error('ERROR: no global install targets selected. Use --tools claude,opencode,codex,copilot or run interactively.');
580
+ const autoHint = autoFlag
581
+ ? 'No supported agent homes were detected for --auto. Create an agent config home first or use --tools claude,opencode,codex,copilot explicitly.'
582
+ : 'Use --tools claude,opencode,codex,copilot or run interactively.';
583
+ console.error(`ERROR: no global install targets selected. ${autoHint}`);
570
584
  process.exitCode = 1;
571
585
  return;
572
586
  }
@@ -578,7 +592,6 @@ export function createCmdInstall(ctx) {
578
592
  return;
579
593
  }
580
594
 
581
- const roots = resolveGlobalInstallRoots();
582
595
  console.log(`Workspine global install - installing runtime surfaces${dryRun ? ' (dry run)' : ''}\n`);
583
596
 
584
597
  const reports = targets.map((target) => installTarget({