@os-eco/overstory-cli 0.6.4 → 0.6.6

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 (117) hide show
  1. package/README.md +61 -61
  2. package/agents/builder.md +16 -16
  3. package/agents/coordinator.md +57 -57
  4. package/agents/issue-reviews.md +71 -0
  5. package/agents/lead.md +43 -42
  6. package/agents/merger.md +15 -15
  7. package/agents/monitor.md +37 -37
  8. package/agents/pr-reviews.md +60 -0
  9. package/agents/prioritize.md +110 -0
  10. package/agents/release.md +56 -0
  11. package/agents/reviewer.md +15 -15
  12. package/agents/scout.md +18 -18
  13. package/agents/supervisor.md +78 -78
  14. package/package.json +1 -1
  15. package/src/agents/checkpoint.test.ts +2 -2
  16. package/src/agents/hooks-deployer.test.ts +59 -25
  17. package/src/agents/hooks-deployer.ts +24 -6
  18. package/src/agents/identity.test.ts +27 -27
  19. package/src/agents/identity.ts +10 -10
  20. package/src/agents/lifecycle.test.ts +6 -6
  21. package/src/agents/lifecycle.ts +2 -2
  22. package/src/agents/overlay.test.ts +14 -14
  23. package/src/agents/overlay.ts +14 -14
  24. package/src/commands/agents.test.ts +5 -5
  25. package/src/commands/agents.ts +10 -9
  26. package/src/commands/clean.test.ts +5 -5
  27. package/src/commands/clean.ts +5 -5
  28. package/src/commands/completions.test.ts +10 -10
  29. package/src/commands/completions.ts +26 -28
  30. package/src/commands/coordinator.test.ts +4 -4
  31. package/src/commands/coordinator.ts +13 -13
  32. package/src/commands/costs.test.ts +45 -45
  33. package/src/commands/costs.ts +1 -1
  34. package/src/commands/dashboard.ts +11 -11
  35. package/src/commands/doctor.ts +4 -4
  36. package/src/commands/errors.ts +1 -1
  37. package/src/commands/feed.ts +1 -1
  38. package/src/commands/group.ts +3 -3
  39. package/src/commands/hooks.test.ts +7 -7
  40. package/src/commands/hooks.ts +7 -7
  41. package/src/commands/init.test.ts +6 -2
  42. package/src/commands/init.ts +19 -19
  43. package/src/commands/inspect.test.ts +16 -16
  44. package/src/commands/inspect.ts +19 -19
  45. package/src/commands/log.test.ts +21 -21
  46. package/src/commands/log.ts +10 -10
  47. package/src/commands/logs.ts +1 -1
  48. package/src/commands/mail.test.ts +7 -7
  49. package/src/commands/mail.ts +28 -11
  50. package/src/commands/merge.test.ts +8 -8
  51. package/src/commands/merge.ts +15 -15
  52. package/src/commands/metrics.test.ts +7 -7
  53. package/src/commands/metrics.ts +3 -3
  54. package/src/commands/monitor.test.ts +5 -5
  55. package/src/commands/monitor.ts +5 -5
  56. package/src/commands/nudge.test.ts +1 -1
  57. package/src/commands/nudge.ts +1 -1
  58. package/src/commands/prime.test.ts +5 -5
  59. package/src/commands/prime.ts +8 -8
  60. package/src/commands/replay.ts +1 -1
  61. package/src/commands/run.test.ts +1 -1
  62. package/src/commands/run.ts +2 -2
  63. package/src/commands/sling.test.ts +89 -7
  64. package/src/commands/sling.ts +109 -18
  65. package/src/commands/spec.test.ts +2 -2
  66. package/src/commands/spec.ts +13 -14
  67. package/src/commands/status.test.ts +99 -3
  68. package/src/commands/status.ts +19 -20
  69. package/src/commands/stop.test.ts +1 -1
  70. package/src/commands/stop.ts +2 -2
  71. package/src/commands/supervisor.test.ts +10 -10
  72. package/src/commands/supervisor.ts +14 -14
  73. package/src/commands/trace.test.ts +7 -7
  74. package/src/commands/trace.ts +10 -10
  75. package/src/commands/watch.ts +5 -5
  76. package/src/commands/worktree.test.ts +208 -32
  77. package/src/commands/worktree.ts +56 -18
  78. package/src/doctor/consistency.test.ts +14 -14
  79. package/src/doctor/dependencies.test.ts +5 -5
  80. package/src/doctor/dependencies.ts +2 -2
  81. package/src/doctor/logs.ts +1 -1
  82. package/src/doctor/merge-queue.test.ts +4 -4
  83. package/src/doctor/structure.test.ts +1 -1
  84. package/src/doctor/structure.ts +1 -1
  85. package/src/doctor/version.test.ts +3 -3
  86. package/src/doctor/version.ts +1 -1
  87. package/src/e2e/init-sling-lifecycle.test.ts +8 -4
  88. package/src/errors.ts +1 -1
  89. package/src/index.ts +13 -11
  90. package/src/mail/broadcast.test.ts +1 -1
  91. package/src/mail/client.test.ts +7 -7
  92. package/src/mail/client.ts +2 -2
  93. package/src/mail/store.test.ts +3 -3
  94. package/src/merge/queue.test.ts +12 -12
  95. package/src/merge/queue.ts +2 -2
  96. package/src/merge/resolver.test.ts +159 -7
  97. package/src/merge/resolver.ts +46 -2
  98. package/src/metrics/store.test.ts +44 -44
  99. package/src/metrics/store.ts +2 -2
  100. package/src/metrics/summary.test.ts +35 -35
  101. package/src/mulch/client.test.ts +1 -1
  102. package/src/mulch/client.ts +1 -1
  103. package/src/sessions/compat.test.ts +3 -3
  104. package/src/sessions/compat.ts +1 -1
  105. package/src/sessions/store.test.ts +4 -4
  106. package/src/sessions/store.ts +2 -2
  107. package/src/types.ts +14 -14
  108. package/src/watchdog/daemon.test.ts +10 -10
  109. package/src/watchdog/daemon.ts +1 -1
  110. package/src/watchdog/health.test.ts +1 -1
  111. package/src/worktree/manager.test.ts +20 -20
  112. package/src/worktree/manager.ts +120 -4
  113. package/src/worktree/tmux.test.ts +8 -3
  114. package/src/worktree/tmux.ts +19 -18
  115. package/templates/CLAUDE.md.tmpl +27 -27
  116. package/templates/hooks.json.tmpl +15 -11
  117. package/templates/overlay.md.tmpl +7 -7
@@ -68,7 +68,7 @@ describe("worktreeCommand", () => {
68
68
  capability: "builder",
69
69
  worktreePath: join(tempDir, ".overstory", "worktrees", "test-agent"),
70
70
  branchName: "overstory/test-agent/task-1",
71
- beadId: "task-1",
71
+ taskId: "task-1",
72
72
  tmuxSession: "overstory-test-agent-fake", // FAKE tmux session name
73
73
  state: "working",
74
74
  pid: 12345,
@@ -156,7 +156,7 @@ describe("worktreeCommand", () => {
156
156
  capability: "builder",
157
157
  worktreePath,
158
158
  branchName: "overstory/test-agent/task-1",
159
- beadId: "task-1",
159
+ taskId: "task-1",
160
160
  tmuxSession: "overstory-test-agent",
161
161
  state: "working",
162
162
  pid: 12345,
@@ -173,7 +173,7 @@ describe("worktreeCommand", () => {
173
173
  await worktreeCommand(["list"]);
174
174
  const out = output();
175
175
 
176
- expect(out).toContain("🌳 Agent worktrees: 1");
176
+ expect(out).toContain("Agent worktrees: 1");
177
177
  expect(out).toContain("overstory/test-agent/task-1");
178
178
  expect(out).toContain("Agent: test-agent");
179
179
  expect(out).toContain("State: working");
@@ -203,7 +203,7 @@ describe("worktreeCommand", () => {
203
203
  capability: "builder",
204
204
  worktreePath,
205
205
  branchName: "overstory/test-agent/task-1",
206
- beadId: "task-1",
206
+ taskId: "task-1",
207
207
  tmuxSession: "overstory-test-agent",
208
208
  state: "working",
209
209
  pid: 12345,
@@ -226,7 +226,7 @@ describe("worktreeCommand", () => {
226
226
  head: string;
227
227
  agentName: string | null;
228
228
  state: string | null;
229
- beadId: string | null;
229
+ taskId: string | null;
230
230
  }>;
231
231
 
232
232
  expect(parsed).toHaveLength(1);
@@ -234,7 +234,7 @@ describe("worktreeCommand", () => {
234
234
  expect(parsed[0]?.branch).toBe("overstory/test-agent/task-1");
235
235
  expect(parsed[0]?.agentName).toBe("test-agent");
236
236
  expect(parsed[0]?.state).toBe("working");
237
- expect(parsed[0]?.beadId).toBe("task-1");
237
+ expect(parsed[0]?.taskId).toBe("task-1");
238
238
  });
239
239
 
240
240
  test("worktrees without sessions show unknown state", async () => {
@@ -291,7 +291,7 @@ describe("worktreeCommand", () => {
291
291
  capability: "builder",
292
292
  worktreePath,
293
293
  branchName: "overstory/completed-agent/task-done",
294
- beadId: "task-done",
294
+ taskId: "task-done",
295
295
  tmuxSession: "overstory-completed-agent",
296
296
  state: "completed",
297
297
  pid: 12345,
@@ -308,7 +308,7 @@ describe("worktreeCommand", () => {
308
308
  await worktreeCommand(["clean"]);
309
309
  const out = output();
310
310
 
311
- expect(out).toContain("šŸ—‘ļø Removed: overstory/completed-agent/task-done");
311
+ expect(out).toContain("Removed: overstory/completed-agent/task-done");
312
312
  expect(out).toContain("Cleaned 1 worktree");
313
313
 
314
314
  // Verify the worktree directory is gone
@@ -345,7 +345,7 @@ describe("worktreeCommand", () => {
345
345
  capability: "builder",
346
346
  worktreePath,
347
347
  branchName: "overstory/done-agent/task-x",
348
- beadId: "task-x",
348
+ taskId: "task-x",
349
349
  tmuxSession: "overstory-done-agent",
350
350
  state: "completed",
351
351
  pid: 12345,
@@ -383,7 +383,7 @@ describe("worktreeCommand", () => {
383
383
  capability: "builder",
384
384
  worktreePath: nonExistentPath,
385
385
  branchName: "overstory/ghost-agent/task-ghost",
386
- beadId: "task-ghost",
386
+ taskId: "task-ghost",
387
387
  tmuxSession: "overstory-ghost-agent",
388
388
  state: "zombie",
389
389
  pid: null,
@@ -437,7 +437,7 @@ describe("worktreeCommand", () => {
437
437
  capability: "builder",
438
438
  worktreePath,
439
439
  branchName: "overstory/stalled-agent/task-stuck",
440
- beadId: "task-stuck",
440
+ taskId: "task-stuck",
441
441
  tmuxSession: "overstory-stalled-agent",
442
442
  state: "stalled",
443
443
  pid: 12345,
@@ -468,7 +468,7 @@ describe("worktreeCommand", () => {
468
468
  baseDir: worktreesDir,
469
469
  agentName: "completed-agent",
470
470
  baseBranch: "main",
471
- beadId: "task-done",
471
+ taskId: "task-done",
472
472
  });
473
473
 
474
474
  const { path: workingPath } = await createWorktree({
@@ -476,7 +476,7 @@ describe("worktreeCommand", () => {
476
476
  baseDir: worktreesDir,
477
477
  agentName: "working-agent",
478
478
  baseBranch: "main",
479
- beadId: "task-wip",
479
+ taskId: "task-wip",
480
480
  });
481
481
 
482
482
  // Write sessions.db with both agents
@@ -486,7 +486,7 @@ describe("worktreeCommand", () => {
486
486
  agentName: "completed-agent",
487
487
  worktreePath: completedPath,
488
488
  branchName: "overstory/completed-agent/task-done",
489
- beadId: "task-done",
489
+ taskId: "task-done",
490
490
  tmuxSession: "overstory-completed-agent-fake",
491
491
  state: "completed",
492
492
  }),
@@ -495,7 +495,7 @@ describe("worktreeCommand", () => {
495
495
  agentName: "working-agent",
496
496
  worktreePath: workingPath,
497
497
  branchName: "overstory/working-agent/task-wip",
498
- beadId: "task-wip",
498
+ taskId: "task-wip",
499
499
  tmuxSession: "overstory-working-agent-fake",
500
500
  state: "working",
501
501
  pid: 12346,
@@ -522,7 +522,7 @@ describe("worktreeCommand", () => {
522
522
  baseDir: worktreesDir,
523
523
  agentName: "completed-agent",
524
524
  baseBranch: "main",
525
- beadId: "task-done",
525
+ taskId: "task-done",
526
526
  });
527
527
 
528
528
  const { path: workingPath } = await createWorktree({
@@ -530,7 +530,7 @@ describe("worktreeCommand", () => {
530
530
  baseDir: worktreesDir,
531
531
  agentName: "working-agent",
532
532
  baseBranch: "main",
533
- beadId: "task-wip",
533
+ taskId: "task-wip",
534
534
  });
535
535
 
536
536
  const { path: stalledPath } = await createWorktree({
@@ -538,7 +538,7 @@ describe("worktreeCommand", () => {
538
538
  baseDir: worktreesDir,
539
539
  agentName: "stalled-agent",
540
540
  baseBranch: "main",
541
- beadId: "task-stuck",
541
+ taskId: "task-stuck",
542
542
  });
543
543
 
544
544
  // Write sessions with different states
@@ -548,7 +548,7 @@ describe("worktreeCommand", () => {
548
548
  agentName: "completed-agent",
549
549
  worktreePath: completedPath,
550
550
  branchName: "overstory/completed-agent/task-done",
551
- beadId: "task-done",
551
+ taskId: "task-done",
552
552
  state: "completed",
553
553
  }),
554
554
  makeSession({
@@ -556,7 +556,7 @@ describe("worktreeCommand", () => {
556
556
  agentName: "working-agent",
557
557
  worktreePath: workingPath,
558
558
  branchName: "overstory/working-agent/task-wip",
559
- beadId: "task-wip",
559
+ taskId: "task-wip",
560
560
  state: "working",
561
561
  }),
562
562
  makeSession({
@@ -564,7 +564,7 @@ describe("worktreeCommand", () => {
564
564
  agentName: "stalled-agent",
565
565
  worktreePath: stalledPath,
566
566
  branchName: "overstory/stalled-agent/task-stuck",
567
- beadId: "task-stuck",
567
+ taskId: "task-stuck",
568
568
  state: "stalled",
569
569
  }),
570
570
  ]);
@@ -598,7 +598,7 @@ describe("worktreeCommand", () => {
598
598
  capability: "builder",
599
599
  worktreePath: path1,
600
600
  branchName: "overstory/agent-1/task-1",
601
- beadId: "task-1",
601
+ taskId: "task-1",
602
602
  tmuxSession: "overstory-agent-1",
603
603
  state: "completed",
604
604
  pid: 12345,
@@ -616,7 +616,7 @@ describe("worktreeCommand", () => {
616
616
  capability: "builder",
617
617
  worktreePath: path2,
618
618
  branchName: "overstory/agent-2/task-2",
619
- beadId: "task-2",
619
+ taskId: "task-2",
620
620
  tmuxSession: "overstory-agent-2",
621
621
  state: "completed",
622
622
  pid: 12346,
@@ -645,7 +645,7 @@ describe("worktreeCommand", () => {
645
645
  baseDir: worktreesDir,
646
646
  agentName: "unmerged-agent",
647
647
  baseBranch: "main",
648
- beadId: "task-unmerged",
648
+ taskId: "task-unmerged",
649
649
  });
650
650
 
651
651
  // Add an unmerged commit
@@ -657,7 +657,7 @@ describe("worktreeCommand", () => {
657
657
  agentName: "unmerged-agent",
658
658
  worktreePath: wtPath,
659
659
  branchName: "overstory/unmerged-agent/task-unmerged",
660
- beadId: "task-unmerged",
660
+ taskId: "task-unmerged",
661
661
  state: "completed",
662
662
  }),
663
663
  ]);
@@ -682,7 +682,7 @@ describe("worktreeCommand", () => {
682
682
  baseDir: worktreesDir,
683
683
  agentName: "unmerged-agent",
684
684
  baseBranch: "main",
685
- beadId: "task-force",
685
+ taskId: "task-force",
686
686
  });
687
687
 
688
688
  // Add an unmerged commit
@@ -694,7 +694,7 @@ describe("worktreeCommand", () => {
694
694
  agentName: "unmerged-agent",
695
695
  worktreePath: wtPath,
696
696
  branchName: "overstory/unmerged-agent/task-force",
697
- beadId: "task-force",
697
+ taskId: "task-force",
698
698
  state: "completed",
699
699
  }),
700
700
  ]);
@@ -704,7 +704,7 @@ describe("worktreeCommand", () => {
704
704
 
705
705
  // Worktree should be removed
706
706
  expect(existsSync(wtPath)).toBe(false);
707
- expect(out).toContain("šŸ—‘ļø Removed: overstory/unmerged-agent/task-force");
707
+ expect(out).toContain("Removed: overstory/unmerged-agent/task-force");
708
708
  });
709
709
 
710
710
  test("without --force, removes worktrees whose branches ARE merged", async () => {
@@ -716,7 +716,7 @@ describe("worktreeCommand", () => {
716
716
  baseDir: worktreesDir,
717
717
  agentName: "merged-agent",
718
718
  baseBranch: "main",
719
- beadId: "task-merged",
719
+ taskId: "task-merged",
720
720
  });
721
721
 
722
722
  // Add a commit and merge it into main
@@ -729,7 +729,7 @@ describe("worktreeCommand", () => {
729
729
  agentName: "merged-agent",
730
730
  worktreePath: wtPath,
731
731
  branchName: branch,
732
- beadId: "task-merged",
732
+ taskId: "task-merged",
733
733
  state: "completed",
734
734
  }),
735
735
  ]);
@@ -751,7 +751,7 @@ describe("worktreeCommand", () => {
751
751
  baseDir: worktreesDir,
752
752
  agentName: "unmerged-json-agent",
753
753
  baseBranch: "main",
754
- beadId: "task-json",
754
+ taskId: "task-json",
755
755
  });
756
756
 
757
757
  // Add an unmerged commit
@@ -763,7 +763,7 @@ describe("worktreeCommand", () => {
763
763
  agentName: "unmerged-json-agent",
764
764
  worktreePath: wtPath,
765
765
  branchName: "overstory/unmerged-json-agent/task-json",
766
- beadId: "task-json",
766
+ taskId: "task-json",
767
767
  state: "completed",
768
768
  }),
769
769
  ]);
@@ -782,5 +782,181 @@ describe("worktreeCommand", () => {
782
782
  expect(parsed.cleaned).toEqual([]);
783
783
  expect(parsed.skipped).toEqual(["overstory/unmerged-json-agent/task-json"]);
784
784
  });
785
+
786
+ test("lead worktree with .seeds/ changes preserves them to canonical before cleanup", async () => {
787
+ const worktreesDir = join(tempDir, ".overstory", "worktrees");
788
+ await mkdir(worktreesDir, { recursive: true });
789
+
790
+ const { path: wtPath, branch } = await createWorktree({
791
+ repoRoot: tempDir,
792
+ baseDir: worktreesDir,
793
+ agentName: "lead-with-seeds",
794
+ baseBranch: "main",
795
+ taskId: "task-lead-seeds",
796
+ });
797
+
798
+ // Commit a .seeds/ file in the lead worktree
799
+ await commitFile(
800
+ wtPath,
801
+ ".seeds/issues/test-issue.yaml",
802
+ "id: test-issue\ntitle: Test Issue\nstatus: open\n",
803
+ "seeds: add test issue",
804
+ );
805
+
806
+ writeSessionsToStore([
807
+ makeSession({
808
+ id: "session-lead-seeds",
809
+ agentName: "lead-with-seeds",
810
+ capability: "lead",
811
+ worktreePath: wtPath,
812
+ branchName: branch,
813
+ taskId: "task-lead-seeds",
814
+ state: "completed",
815
+ }),
816
+ ]);
817
+
818
+ await worktreeCommand(["clean"]);
819
+ const out = output();
820
+
821
+ // The worktree should be removed
822
+ expect(existsSync(wtPath)).toBe(false);
823
+
824
+ // The .seeds/ changes should have been preserved to main
825
+ const showProc = Bun.spawn(["git", "show", "main:.seeds/issues/test-issue.yaml"], {
826
+ cwd: tempDir,
827
+ stdout: "pipe",
828
+ stderr: "pipe",
829
+ });
830
+ const showOut = await new Response(showProc.stdout).text();
831
+ const showExit = await showProc.exited;
832
+ expect(showExit).toBe(0);
833
+ expect(showOut).toContain("test-issue");
834
+
835
+ // Output should mention preservation
836
+ expect(out).toContain("Preserved .seeds/");
837
+ });
838
+
839
+ test("lead worktree without .seeds/ changes cleans normally", async () => {
840
+ const worktreesDir = join(tempDir, ".overstory", "worktrees");
841
+ await mkdir(worktreesDir, { recursive: true });
842
+
843
+ const { path: wtPath, branch } = await createWorktree({
844
+ repoRoot: tempDir,
845
+ baseDir: worktreesDir,
846
+ agentName: "lead-no-seeds",
847
+ baseBranch: "main",
848
+ taskId: "task-lead-no-seeds",
849
+ });
850
+
851
+ // Commit a non-.seeds/ file
852
+ await commitFile(wtPath, "src/work.ts", "export const x = 1;", "non-seeds work");
853
+
854
+ writeSessionsToStore([
855
+ makeSession({
856
+ id: "session-lead-no-seeds",
857
+ agentName: "lead-no-seeds",
858
+ capability: "lead",
859
+ worktreePath: wtPath,
860
+ branchName: branch,
861
+ taskId: "task-lead-no-seeds",
862
+ state: "completed",
863
+ }),
864
+ ]);
865
+
866
+ await worktreeCommand(["clean"]);
867
+ const out = output();
868
+
869
+ // Worktree should be removed
870
+ expect(existsSync(wtPath)).toBe(false);
871
+ // Output should NOT mention .seeds/ preservation
872
+ expect(out).not.toContain("Preserved .seeds/");
873
+ // Should still report as cleaned
874
+ expect(out).toContain("Cleaned 1 worktree");
875
+ });
876
+
877
+ test("lead worktrees are cleaned without --force even with unmerged non-seeds changes", async () => {
878
+ const worktreesDir = join(tempDir, ".overstory", "worktrees");
879
+ await mkdir(worktreesDir, { recursive: true });
880
+
881
+ const { path: wtPath, branch } = await createWorktree({
882
+ repoRoot: tempDir,
883
+ baseDir: worktreesDir,
884
+ agentName: "lead-unmerged",
885
+ baseBranch: "main",
886
+ taskId: "task-lead-unmerged",
887
+ });
888
+
889
+ // Add unmerged non-.seeds/ commit
890
+ await commitFile(wtPath, "src/lead-work.ts", "export const y = 2;", "unmerged lead work");
891
+
892
+ writeSessionsToStore([
893
+ makeSession({
894
+ id: "session-lead-unmerged",
895
+ agentName: "lead-unmerged",
896
+ capability: "lead",
897
+ worktreePath: wtPath,
898
+ branchName: branch,
899
+ taskId: "task-lead-unmerged",
900
+ state: "completed",
901
+ }),
902
+ ]);
903
+
904
+ // Run clean WITHOUT --force — leads bypass merge check
905
+ await worktreeCommand(["clean"]);
906
+ const out = output();
907
+
908
+ // Lead worktree SHOULD be removed (not skipped)
909
+ expect(existsSync(wtPath)).toBe(false);
910
+ expect(out).toContain("Cleaned 1 worktree");
911
+ expect(out).not.toContain("Skipped");
912
+ });
913
+
914
+ test("--json output includes seedsPreserved array", async () => {
915
+ const worktreesDir = join(tempDir, ".overstory", "worktrees");
916
+ await mkdir(worktreesDir, { recursive: true });
917
+
918
+ const { path: wtPath, branch } = await createWorktree({
919
+ repoRoot: tempDir,
920
+ baseDir: worktreesDir,
921
+ agentName: "lead-seeds-json",
922
+ baseBranch: "main",
923
+ taskId: "task-seeds-json",
924
+ });
925
+
926
+ // Commit a .seeds/ file in the lead worktree
927
+ await commitFile(
928
+ wtPath,
929
+ ".seeds/issues/json-issue.yaml",
930
+ "id: json-issue\ntitle: JSON Issue\nstatus: open\n",
931
+ "seeds: add json issue",
932
+ );
933
+
934
+ writeSessionsToStore([
935
+ makeSession({
936
+ id: "session-lead-json",
937
+ agentName: "lead-seeds-json",
938
+ capability: "lead",
939
+ worktreePath: wtPath,
940
+ branchName: branch,
941
+ taskId: "task-seeds-json",
942
+ state: "completed",
943
+ }),
944
+ ]);
945
+
946
+ await worktreeCommand(["clean", "--json"]);
947
+ const out = output();
948
+
949
+ const parsed = JSON.parse(out.trim()) as {
950
+ cleaned: string[];
951
+ failed: string[];
952
+ skipped: string[];
953
+ pruned: number;
954
+ mailPurged: number;
955
+ seedsPreserved: string[];
956
+ };
957
+
958
+ expect(parsed.cleaned).toContain(branch);
959
+ expect(parsed.seedsPreserved).toContain(branch);
960
+ });
785
961
  });
786
962
  });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CLI command: overstory worktree list | clean [--completed] [--all]
2
+ * CLI command: ov worktree list | clean [--completed] [--all]
3
3
  *
4
4
  * List shows worktrees with agent status.
5
5
  * Clean removes worktree dirs, branch refs (if merged), and tmux sessions.
@@ -13,11 +13,16 @@ import { ValidationError } from "../errors.ts";
13
13
  import { createMailStore } from "../mail/store.ts";
14
14
  import { openSessionStore } from "../sessions/compat.ts";
15
15
  import type { AgentSession } from "../types.ts";
16
- import { isBranchMerged, listWorktrees, removeWorktree } from "../worktree/manager.ts";
16
+ import {
17
+ isBranchMerged,
18
+ listWorktrees,
19
+ preserveSeedsChanges,
20
+ removeWorktree,
21
+ } from "../worktree/manager.ts";
17
22
  import { isSessionAlive, killSession } from "../worktree/tmux.ts";
18
23
 
19
24
  /**
20
- * Handle `overstory worktree list`.
25
+ * Handle `ov worktree list`.
21
26
  */
22
27
  async function handleList(root: string, json: boolean): Promise<void> {
23
28
  const worktrees = await listWorktrees(root);
@@ -41,7 +46,7 @@ async function handleList(root: string, json: boolean): Promise<void> {
41
46
  head: wt.head,
42
47
  agentName: session?.agentName ?? null,
43
48
  state: session?.state ?? null,
44
- beadId: session?.beadId ?? null,
49
+ taskId: session?.taskId ?? null,
45
50
  };
46
51
  });
47
52
  process.stdout.write(`${JSON.stringify(entries, null, "\t")}\n`);
@@ -53,12 +58,12 @@ async function handleList(root: string, json: boolean): Promise<void> {
53
58
  return;
54
59
  }
55
60
 
56
- process.stdout.write(`🌳 Agent worktrees: ${overstoryWts.length}\n\n`);
61
+ process.stdout.write(`Agent worktrees: ${overstoryWts.length}\n\n`);
57
62
  for (const wt of overstoryWts) {
58
63
  const session = sessions.find((s) => s.worktreePath === wt.path);
59
64
  const state = session?.state ?? "unknown";
60
65
  const agent = session?.agentName ?? "?";
61
- const bead = session?.beadId ?? "?";
66
+ const bead = session?.taskId ?? "?";
62
67
  process.stdout.write(` ${wt.branch}\n`);
63
68
  process.stdout.write(` Agent: ${agent} | State: ${state} | Task: ${bead}\n`);
64
69
  process.stdout.write(` Path: ${wt.path}\n\n`);
@@ -66,7 +71,7 @@ async function handleList(root: string, json: boolean): Promise<void> {
66
71
  }
67
72
 
68
73
  /**
69
- * Handle `overstory worktree clean [--completed] [--all] [--force]`.
74
+ * Handle `ov worktree clean [--completed] [--all] [--force]`.
70
75
  */
71
76
  async function handleClean(
72
77
  opts: { all: boolean; force: boolean; completedOnly: boolean },
@@ -92,6 +97,7 @@ async function handleClean(
92
97
  const cleaned: string[] = [];
93
98
  const failed: string[] = [];
94
99
  const skipped: string[] = [];
100
+ const seedsPreserved: string[] = [];
95
101
 
96
102
  try {
97
103
  for (const wt of overstoryWts) {
@@ -102,8 +108,11 @@ async function handleClean(
102
108
  continue;
103
109
  }
104
110
 
105
- // Check if the branch has been merged into the canonical branch (unless --force)
106
- if (!force && wt.branch.length > 0) {
111
+ // Lead branches are never merged via the normal pipeline — skip merge check for leads.
112
+ const isLead = session?.capability === "lead";
113
+
114
+ // Check if the branch has been merged into the canonical branch (unless --force or lead)
115
+ if (!force && !isLead && wt.branch.length > 0) {
107
116
  let merged = false;
108
117
  try {
109
118
  merged = await isBranchMerged(root, wt.branch, canonicalBranch);
@@ -131,8 +140,8 @@ async function handleClean(
131
140
  }
132
141
  }
133
142
 
134
- // Warn about force-deleting unmerged branch
135
- if (force && wt.branch.length > 0) {
143
+ // Warn about force-deleting unmerged branch (non-lead only)
144
+ if (force && !isLead && wt.branch.length > 0) {
136
145
  let merged = false;
137
146
  try {
138
147
  merged = await isBranchMerged(root, wt.branch, canonicalBranch);
@@ -140,7 +149,30 @@ async function handleClean(
140
149
  merged = false;
141
150
  }
142
151
  if (!merged && !json) {
143
- process.stdout.write(`āš ļø Force-deleting unmerged branch: ${wt.branch}\n`);
152
+ process.stdout.write(`Warning: Force-deleting unmerged branch: ${wt.branch}\n`);
153
+ }
154
+ }
155
+
156
+ // Preserve .seeds/ changes from lead worktrees before removal.
157
+ // Lead branches are never merged, so .seeds/ files would otherwise be lost.
158
+ if (isLead && wt.branch.length > 0) {
159
+ const result = await preserveSeedsChanges(
160
+ root,
161
+ wt.branch,
162
+ canonicalBranch,
163
+ session?.agentName ?? "unknown-lead",
164
+ );
165
+ if (result.preserved) {
166
+ seedsPreserved.push(wt.branch);
167
+ if (!json) {
168
+ process.stdout.write(
169
+ `Preserved .seeds/ changes from lead ${session?.agentName ?? "unknown-lead"}\n`,
170
+ );
171
+ }
172
+ } else if (result.error) {
173
+ process.stderr.write(
174
+ `Warning: Failed to preserve .seeds/ from ${wt.branch}: ${result.error}\n`,
175
+ );
144
176
  }
145
177
  }
146
178
 
@@ -154,13 +186,13 @@ async function handleClean(
154
186
  cleaned.push(wt.branch);
155
187
 
156
188
  if (!json) {
157
- process.stdout.write(`šŸ—‘ļø Removed: ${wt.branch}\n`);
189
+ process.stdout.write(`Removed: ${wt.branch}\n`);
158
190
  }
159
191
  } catch (err) {
160
192
  failed.push(wt.branch);
161
193
  if (!json) {
162
194
  const msg = err instanceof Error ? err.message : String(err);
163
- process.stderr.write(`āš ļø Failed to remove ${wt.branch}: ${msg}\n`);
195
+ process.stderr.write(`Warning: Failed to remove ${wt.branch}: ${msg}\n`);
164
196
  }
165
197
  }
166
198
  }
@@ -210,13 +242,14 @@ async function handleClean(
210
242
 
211
243
  if (json) {
212
244
  process.stdout.write(
213
- `${JSON.stringify({ cleaned, failed, skipped, pruned: pruneCount, mailPurged })}\n`,
245
+ `${JSON.stringify({ cleaned, failed, skipped, pruned: pruneCount, mailPurged, seedsPreserved })}\n`,
214
246
  );
215
247
  } else if (
216
248
  cleaned.length === 0 &&
217
249
  pruneCount === 0 &&
218
250
  failed.length === 0 &&
219
- skipped.length === 0
251
+ skipped.length === 0 &&
252
+ seedsPreserved.length === 0
220
253
  ) {
221
254
  process.stdout.write("No worktrees to clean.\n");
222
255
  } else {
@@ -240,9 +273,14 @@ async function handleClean(
240
273
  `Pruned ${pruneCount} zombie session${pruneCount === 1 ? "" : "s"} from store.\n`,
241
274
  );
242
275
  }
276
+ if (seedsPreserved.length > 0) {
277
+ process.stdout.write(
278
+ `Preserved .seeds/ changes from ${seedsPreserved.length} lead${seedsPreserved.length === 1 ? "" : "s"}.\n`,
279
+ );
280
+ }
243
281
  if (skipped.length > 0) {
244
282
  process.stdout.write(
245
- `\nāš ļø Skipped ${skipped.length} worktree${skipped.length === 1 ? "" : "s"} with unmerged branches:\n`,
283
+ `\nWarning: Skipped ${skipped.length} worktree${skipped.length === 1 ? "" : "s"} with unmerged branches:\n`,
246
284
  );
247
285
  for (const branch of skipped) {
248
286
  process.stdout.write(` ${branch}\n`);
@@ -297,7 +335,7 @@ export function createWorktreeCommand(): Command {
297
335
  }
298
336
 
299
337
  /**
300
- * Entry point for `overstory worktree <subcommand> [flags]`.
338
+ * Entry point for `ov worktree <subcommand> [flags]`.
301
339
  *
302
340
  * Subcommands: list, clean.
303
341
  */