@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.
- package/README.md +61 -61
- package/agents/builder.md +16 -16
- package/agents/coordinator.md +57 -57
- package/agents/issue-reviews.md +71 -0
- package/agents/lead.md +43 -42
- package/agents/merger.md +15 -15
- package/agents/monitor.md +37 -37
- package/agents/pr-reviews.md +60 -0
- package/agents/prioritize.md +110 -0
- package/agents/release.md +56 -0
- package/agents/reviewer.md +15 -15
- package/agents/scout.md +18 -18
- package/agents/supervisor.md +78 -78
- package/package.json +1 -1
- package/src/agents/checkpoint.test.ts +2 -2
- package/src/agents/hooks-deployer.test.ts +59 -25
- package/src/agents/hooks-deployer.ts +24 -6
- package/src/agents/identity.test.ts +27 -27
- package/src/agents/identity.ts +10 -10
- package/src/agents/lifecycle.test.ts +6 -6
- package/src/agents/lifecycle.ts +2 -2
- package/src/agents/overlay.test.ts +14 -14
- package/src/agents/overlay.ts +14 -14
- package/src/commands/agents.test.ts +5 -5
- package/src/commands/agents.ts +10 -9
- package/src/commands/clean.test.ts +5 -5
- package/src/commands/clean.ts +5 -5
- package/src/commands/completions.test.ts +10 -10
- package/src/commands/completions.ts +26 -28
- package/src/commands/coordinator.test.ts +4 -4
- package/src/commands/coordinator.ts +13 -13
- package/src/commands/costs.test.ts +45 -45
- package/src/commands/costs.ts +1 -1
- package/src/commands/dashboard.ts +11 -11
- package/src/commands/doctor.ts +4 -4
- package/src/commands/errors.ts +1 -1
- package/src/commands/feed.ts +1 -1
- package/src/commands/group.ts +3 -3
- package/src/commands/hooks.test.ts +7 -7
- package/src/commands/hooks.ts +7 -7
- package/src/commands/init.test.ts +6 -2
- package/src/commands/init.ts +19 -19
- package/src/commands/inspect.test.ts +16 -16
- package/src/commands/inspect.ts +19 -19
- package/src/commands/log.test.ts +21 -21
- package/src/commands/log.ts +10 -10
- package/src/commands/logs.ts +1 -1
- package/src/commands/mail.test.ts +7 -7
- package/src/commands/mail.ts +28 -11
- package/src/commands/merge.test.ts +8 -8
- package/src/commands/merge.ts +15 -15
- package/src/commands/metrics.test.ts +7 -7
- package/src/commands/metrics.ts +3 -3
- package/src/commands/monitor.test.ts +5 -5
- package/src/commands/monitor.ts +5 -5
- package/src/commands/nudge.test.ts +1 -1
- package/src/commands/nudge.ts +1 -1
- package/src/commands/prime.test.ts +5 -5
- package/src/commands/prime.ts +8 -8
- package/src/commands/replay.ts +1 -1
- package/src/commands/run.test.ts +1 -1
- package/src/commands/run.ts +2 -2
- package/src/commands/sling.test.ts +89 -7
- package/src/commands/sling.ts +109 -18
- package/src/commands/spec.test.ts +2 -2
- package/src/commands/spec.ts +13 -14
- package/src/commands/status.test.ts +99 -3
- package/src/commands/status.ts +19 -20
- package/src/commands/stop.test.ts +1 -1
- package/src/commands/stop.ts +2 -2
- package/src/commands/supervisor.test.ts +10 -10
- package/src/commands/supervisor.ts +14 -14
- package/src/commands/trace.test.ts +7 -7
- package/src/commands/trace.ts +10 -10
- package/src/commands/watch.ts +5 -5
- package/src/commands/worktree.test.ts +208 -32
- package/src/commands/worktree.ts +56 -18
- package/src/doctor/consistency.test.ts +14 -14
- package/src/doctor/dependencies.test.ts +5 -5
- package/src/doctor/dependencies.ts +2 -2
- package/src/doctor/logs.ts +1 -1
- package/src/doctor/merge-queue.test.ts +4 -4
- package/src/doctor/structure.test.ts +1 -1
- package/src/doctor/structure.ts +1 -1
- package/src/doctor/version.test.ts +3 -3
- package/src/doctor/version.ts +1 -1
- package/src/e2e/init-sling-lifecycle.test.ts +8 -4
- package/src/errors.ts +1 -1
- package/src/index.ts +13 -11
- package/src/mail/broadcast.test.ts +1 -1
- package/src/mail/client.test.ts +7 -7
- package/src/mail/client.ts +2 -2
- package/src/mail/store.test.ts +3 -3
- package/src/merge/queue.test.ts +12 -12
- package/src/merge/queue.ts +2 -2
- package/src/merge/resolver.test.ts +159 -7
- package/src/merge/resolver.ts +46 -2
- package/src/metrics/store.test.ts +44 -44
- package/src/metrics/store.ts +2 -2
- package/src/metrics/summary.test.ts +35 -35
- package/src/mulch/client.test.ts +1 -1
- package/src/mulch/client.ts +1 -1
- package/src/sessions/compat.test.ts +3 -3
- package/src/sessions/compat.ts +1 -1
- package/src/sessions/store.test.ts +4 -4
- package/src/sessions/store.ts +2 -2
- package/src/types.ts +14 -14
- package/src/watchdog/daemon.test.ts +10 -10
- package/src/watchdog/daemon.ts +1 -1
- package/src/watchdog/health.test.ts +1 -1
- package/src/worktree/manager.test.ts +20 -20
- package/src/worktree/manager.ts +120 -4
- package/src/worktree/tmux.test.ts +8 -3
- package/src/worktree/tmux.ts +19 -18
- package/templates/CLAUDE.md.tmpl +27 -27
- package/templates/hooks.json.tmpl +15 -11
- 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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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]?.
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/commands/worktree.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI command:
|
|
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 {
|
|
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 `
|
|
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
|
-
|
|
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(
|
|
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?.
|
|
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 `
|
|
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
|
-
//
|
|
106
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
`\
|
|
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 `
|
|
338
|
+
* Entry point for `ov worktree <subcommand> [flags]`.
|
|
301
339
|
*
|
|
302
340
|
* Subcommands: list, clean.
|
|
303
341
|
*/
|