@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
|
@@ -58,7 +58,7 @@ describe("createWorktree", () => {
|
|
|
58
58
|
baseDir: worktreesDir,
|
|
59
59
|
agentName: "auth-login",
|
|
60
60
|
baseBranch: defaultBranch,
|
|
61
|
-
|
|
61
|
+
taskId: "bead-abc123",
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
expect(result.path).toBe(join(worktreesDir, "auth-login"));
|
|
@@ -71,7 +71,7 @@ describe("createWorktree", () => {
|
|
|
71
71
|
baseDir: worktreesDir,
|
|
72
72
|
agentName: "auth-login",
|
|
73
73
|
baseBranch: defaultBranch,
|
|
74
|
-
|
|
74
|
+
taskId: "bead-abc123",
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
expect(existsSync(result.path)).toBe(true);
|
|
@@ -85,7 +85,7 @@ describe("createWorktree", () => {
|
|
|
85
85
|
baseDir: worktreesDir,
|
|
86
86
|
agentName: "auth-login",
|
|
87
87
|
baseBranch: defaultBranch,
|
|
88
|
-
|
|
88
|
+
taskId: "bead-abc123",
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
const branchList = await git(repoDir, ["branch", "--list"]);
|
|
@@ -98,7 +98,7 @@ describe("createWorktree", () => {
|
|
|
98
98
|
baseDir: worktreesDir,
|
|
99
99
|
agentName: "auth-login",
|
|
100
100
|
baseBranch: defaultBranch,
|
|
101
|
-
|
|
101
|
+
taskId: "bead-abc123",
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
await expect(
|
|
@@ -107,7 +107,7 @@ describe("createWorktree", () => {
|
|
|
107
107
|
baseDir: worktreesDir,
|
|
108
108
|
agentName: "auth-login",
|
|
109
109
|
baseBranch: defaultBranch,
|
|
110
|
-
|
|
110
|
+
taskId: "bead-abc123",
|
|
111
111
|
}),
|
|
112
112
|
).rejects.toThrow(WorktreeError);
|
|
113
113
|
});
|
|
@@ -119,7 +119,7 @@ describe("createWorktree", () => {
|
|
|
119
119
|
baseDir: worktreesDir,
|
|
120
120
|
agentName: "auth-login",
|
|
121
121
|
baseBranch: defaultBranch,
|
|
122
|
-
|
|
122
|
+
taskId: "bead-abc123",
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
try {
|
|
@@ -128,7 +128,7 @@ describe("createWorktree", () => {
|
|
|
128
128
|
baseDir: worktreesDir,
|
|
129
129
|
agentName: "auth-login",
|
|
130
130
|
baseBranch: defaultBranch,
|
|
131
|
-
|
|
131
|
+
taskId: "bead-abc123",
|
|
132
132
|
});
|
|
133
133
|
// Should not reach here
|
|
134
134
|
expect(true).toBe(false);
|
|
@@ -174,7 +174,7 @@ describe("listWorktrees", () => {
|
|
|
174
174
|
baseDir: worktreesDir,
|
|
175
175
|
agentName: "auth-login",
|
|
176
176
|
baseBranch: defaultBranch,
|
|
177
|
-
|
|
177
|
+
taskId: "bead-abc",
|
|
178
178
|
});
|
|
179
179
|
|
|
180
180
|
await createWorktree({
|
|
@@ -182,7 +182,7 @@ describe("listWorktrees", () => {
|
|
|
182
182
|
baseDir: worktreesDir,
|
|
183
183
|
agentName: "data-sync",
|
|
184
184
|
baseBranch: defaultBranch,
|
|
185
|
-
|
|
185
|
+
taskId: "bead-xyz",
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
const entries = await listWorktrees(repoDir);
|
|
@@ -206,7 +206,7 @@ describe("listWorktrees", () => {
|
|
|
206
206
|
baseDir: worktreesDir,
|
|
207
207
|
agentName: "feature-worker",
|
|
208
208
|
baseBranch: defaultBranch,
|
|
209
|
-
|
|
209
|
+
taskId: "bead-123",
|
|
210
210
|
});
|
|
211
211
|
|
|
212
212
|
const entries = await listWorktrees(repoDir);
|
|
@@ -223,7 +223,7 @@ describe("listWorktrees", () => {
|
|
|
223
223
|
baseDir: worktreesDir,
|
|
224
224
|
agentName: "auth-login",
|
|
225
225
|
baseBranch: defaultBranch,
|
|
226
|
-
|
|
226
|
+
taskId: "bead-abc",
|
|
227
227
|
});
|
|
228
228
|
|
|
229
229
|
const entries = await listWorktrees(repoDir);
|
|
@@ -266,7 +266,7 @@ describe("isBranchMerged", () => {
|
|
|
266
266
|
baseDir: worktreesDir,
|
|
267
267
|
agentName: "feature-agent",
|
|
268
268
|
baseBranch: defaultBranch,
|
|
269
|
-
|
|
269
|
+
taskId: "bead-merged",
|
|
270
270
|
});
|
|
271
271
|
|
|
272
272
|
// Add a commit to the feature branch
|
|
@@ -285,7 +285,7 @@ describe("isBranchMerged", () => {
|
|
|
285
285
|
baseDir: worktreesDir,
|
|
286
286
|
agentName: "feature-agent",
|
|
287
287
|
baseBranch: defaultBranch,
|
|
288
|
-
|
|
288
|
+
taskId: "bead-unmerged",
|
|
289
289
|
});
|
|
290
290
|
|
|
291
291
|
// Add a commit to the feature branch (not merged)
|
|
@@ -302,7 +302,7 @@ describe("isBranchMerged", () => {
|
|
|
302
302
|
baseDir: worktreesDir,
|
|
303
303
|
agentName: "feature-agent",
|
|
304
304
|
baseBranch: defaultBranch,
|
|
305
|
-
|
|
305
|
+
taskId: "bead-same",
|
|
306
306
|
});
|
|
307
307
|
|
|
308
308
|
// The branch was created from defaultBranch with no additional commits,
|
|
@@ -334,7 +334,7 @@ describe("removeWorktree", () => {
|
|
|
334
334
|
baseDir: worktreesDir,
|
|
335
335
|
agentName: "auth-login",
|
|
336
336
|
baseBranch: defaultBranch,
|
|
337
|
-
|
|
337
|
+
taskId: "bead-abc",
|
|
338
338
|
});
|
|
339
339
|
|
|
340
340
|
expect(existsSync(wtPath)).toBe(true);
|
|
@@ -350,7 +350,7 @@ describe("removeWorktree", () => {
|
|
|
350
350
|
baseDir: worktreesDir,
|
|
351
351
|
agentName: "auth-login",
|
|
352
352
|
baseBranch: defaultBranch,
|
|
353
|
-
|
|
353
|
+
taskId: "bead-abc",
|
|
354
354
|
});
|
|
355
355
|
|
|
356
356
|
await removeWorktree(repoDir, wtPath);
|
|
@@ -365,7 +365,7 @@ describe("removeWorktree", () => {
|
|
|
365
365
|
baseDir: worktreesDir,
|
|
366
366
|
agentName: "auth-login",
|
|
367
367
|
baseBranch: defaultBranch,
|
|
368
|
-
|
|
368
|
+
taskId: "bead-abc",
|
|
369
369
|
});
|
|
370
370
|
|
|
371
371
|
await removeWorktree(repoDir, wtPath);
|
|
@@ -381,7 +381,7 @@ describe("removeWorktree", () => {
|
|
|
381
381
|
baseDir: worktreesDir,
|
|
382
382
|
agentName: "auth-login",
|
|
383
383
|
baseBranch: defaultBranch,
|
|
384
|
-
|
|
384
|
+
taskId: "bead-abc",
|
|
385
385
|
});
|
|
386
386
|
|
|
387
387
|
// Create an untracked file in the worktree
|
|
@@ -400,7 +400,7 @@ describe("removeWorktree", () => {
|
|
|
400
400
|
baseDir: worktreesDir,
|
|
401
401
|
agentName: "auth-login",
|
|
402
402
|
baseBranch: defaultBranch,
|
|
403
|
-
|
|
403
|
+
taskId: "bead-abc",
|
|
404
404
|
});
|
|
405
405
|
|
|
406
406
|
// Add a commit in the worktree so the branch diverges (making it "unmerged")
|
|
@@ -419,7 +419,7 @@ describe("removeWorktree", () => {
|
|
|
419
419
|
baseDir: worktreesDir,
|
|
420
420
|
agentName: "auth-login",
|
|
421
421
|
baseBranch: defaultBranch,
|
|
422
|
-
|
|
422
|
+
taskId: "bead-abc",
|
|
423
423
|
});
|
|
424
424
|
|
|
425
425
|
// Add a commit to make the branch unmerged
|
package/src/worktree/manager.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unlink } from "node:fs/promises";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
import { WorktreeError } from "../errors.ts";
|
|
3
4
|
|
|
@@ -38,7 +39,7 @@ async function runGit(
|
|
|
38
39
|
* Create a new git worktree for an agent.
|
|
39
40
|
*
|
|
40
41
|
* Creates a worktree at `{baseDir}/{agentName}` with a new branch
|
|
41
|
-
* named `overstory/{agentName}/{
|
|
42
|
+
* named `overstory/{agentName}/{taskId}` based on `baseBranch`.
|
|
42
43
|
*
|
|
43
44
|
* @returns The absolute worktree path and branch name.
|
|
44
45
|
*/
|
|
@@ -47,12 +48,12 @@ export async function createWorktree(options: {
|
|
|
47
48
|
baseDir: string;
|
|
48
49
|
agentName: string;
|
|
49
50
|
baseBranch: string;
|
|
50
|
-
|
|
51
|
+
taskId: string;
|
|
51
52
|
}): Promise<{ path: string; branch: string }> {
|
|
52
|
-
const { repoRoot, baseDir, agentName, baseBranch,
|
|
53
|
+
const { repoRoot, baseDir, agentName, baseBranch, taskId } = options;
|
|
53
54
|
|
|
54
55
|
const worktreePath = join(baseDir, agentName);
|
|
55
|
-
const branchName = `overstory/${agentName}/${
|
|
56
|
+
const branchName = `overstory/${agentName}/${taskId}`;
|
|
56
57
|
|
|
57
58
|
await runGit(repoRoot, ["worktree", "add", "-b", branchName, worktreePath, baseBranch], {
|
|
58
59
|
worktreePath,
|
|
@@ -196,3 +197,118 @@ export async function removeWorktree(
|
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Preserve .seeds/ changes from a branch into the canonical branch.
|
|
203
|
+
*
|
|
204
|
+
* Lead agent branches are never merged via the normal merge pipeline, so
|
|
205
|
+
* any .seeds/ issue files they create would be lost when the worktree is
|
|
206
|
+
* cleaned. This function extracts only the .seeds/ diff from the branch
|
|
207
|
+
* and applies it to the canonical branch via a patch.
|
|
208
|
+
*
|
|
209
|
+
* @returns `{ preserved: true }` if changes were found and committed,
|
|
210
|
+
* `{ preserved: false }` if there were no .seeds/ changes,
|
|
211
|
+
* `{ preserved: false, error: "..." }` if something went wrong.
|
|
212
|
+
*/
|
|
213
|
+
export async function preserveSeedsChanges(
|
|
214
|
+
repoRoot: string,
|
|
215
|
+
branch: string,
|
|
216
|
+
canonicalBranch: string,
|
|
217
|
+
agentName: string,
|
|
218
|
+
): Promise<{ preserved: boolean; error?: string }> {
|
|
219
|
+
// Step 1: Get the .seeds/ diff between canonical and the branch (three-dot diff).
|
|
220
|
+
// Three-dot diff shows changes introduced on branch since it diverged from canonicalBranch.
|
|
221
|
+
let diff: string;
|
|
222
|
+
try {
|
|
223
|
+
diff = await runGit(repoRoot, ["diff", `${canonicalBranch}...${branch}`, "--", ".seeds/"]);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
226
|
+
return { preserved: false, error: `Failed to compute .seeds/ diff: ${msg}` };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (diff.trim() === "") {
|
|
230
|
+
// No .seeds/ changes on this branch
|
|
231
|
+
return { preserved: false };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Step 2: Verify the repo root is currently on canonicalBranch.
|
|
235
|
+
let currentBranch: string;
|
|
236
|
+
try {
|
|
237
|
+
currentBranch = (await runGit(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
238
|
+
} catch (err) {
|
|
239
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
240
|
+
return { preserved: false, error: `Failed to determine current branch: ${msg}` };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (currentBranch !== canonicalBranch) {
|
|
244
|
+
return {
|
|
245
|
+
preserved: false,
|
|
246
|
+
error: `Repo root is on '${currentBranch}', expected '${canonicalBranch}'. Cannot apply patch.`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Step 3: Check that .seeds/ is clean in the canonical branch.
|
|
251
|
+
let statusOutput: string;
|
|
252
|
+
try {
|
|
253
|
+
statusOutput = await runGit(repoRoot, ["status", "--porcelain", "--", ".seeds/"]);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
256
|
+
return { preserved: false, error: `Failed to check .seeds/ status: ${msg}` };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (statusOutput.trim() !== "") {
|
|
260
|
+
return {
|
|
261
|
+
preserved: false,
|
|
262
|
+
error: `.seeds/ has uncommitted changes in canonical branch. Cannot apply patch safely.`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Step 4: Write diff to a temp file.
|
|
267
|
+
const tmpFile = join(repoRoot, ".overstory", `_seeds-patch-${Date.now()}.diff`);
|
|
268
|
+
try {
|
|
269
|
+
await Bun.write(tmpFile, diff);
|
|
270
|
+
|
|
271
|
+
// Step 5: Apply the patch with --index (stages changes).
|
|
272
|
+
try {
|
|
273
|
+
await runGit(repoRoot, ["apply", "--index", tmpFile]);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
276
|
+
// Revert any partial changes
|
|
277
|
+
try {
|
|
278
|
+
await runGit(repoRoot, ["reset", "HEAD", "--", ".seeds/"]);
|
|
279
|
+
await runGit(repoRoot, ["checkout", "--", ".seeds/"]);
|
|
280
|
+
} catch {
|
|
281
|
+
// Best-effort revert
|
|
282
|
+
}
|
|
283
|
+
return { preserved: false, error: `Failed to apply .seeds/ patch: ${msg}` };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Step 6: Commit the changes.
|
|
287
|
+
try {
|
|
288
|
+
await runGit(repoRoot, [
|
|
289
|
+
"commit",
|
|
290
|
+
"-m",
|
|
291
|
+
`chore: preserve .seeds/ changes from lead ${agentName}`,
|
|
292
|
+
]);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
295
|
+
// Revert any staged changes
|
|
296
|
+
try {
|
|
297
|
+
await runGit(repoRoot, ["reset", "HEAD", "--", ".seeds/"]);
|
|
298
|
+
await runGit(repoRoot, ["checkout", "--", ".seeds/"]);
|
|
299
|
+
} catch {
|
|
300
|
+
// Best-effort revert
|
|
301
|
+
}
|
|
302
|
+
return { preserved: false, error: `Failed to commit .seeds/ changes: ${msg}` };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { preserved: true };
|
|
306
|
+
} finally {
|
|
307
|
+
// Step 8: Always clean up the temp file.
|
|
308
|
+
try {
|
|
309
|
+
await unlink(tmpFile);
|
|
310
|
+
} catch {
|
|
311
|
+
// Ignore cleanup errors
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -210,15 +210,19 @@ describe("createSession", () => {
|
|
|
210
210
|
}
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
-
test("still creates session when which overstory
|
|
213
|
+
test("still creates session when which ov and which overstory both fail (uses fallback)", async () => {
|
|
214
214
|
let callCount = 0;
|
|
215
215
|
spawnSpy.mockImplementation(() => {
|
|
216
216
|
callCount++;
|
|
217
217
|
if (callCount === 1) {
|
|
218
|
+
// which ov fails
|
|
219
|
+
return mockSpawnResult("", "ov not found", 1);
|
|
220
|
+
}
|
|
221
|
+
if (callCount === 2) {
|
|
218
222
|
// which overstory fails
|
|
219
223
|
return mockSpawnResult("", "overstory not found", 1);
|
|
220
224
|
}
|
|
221
|
-
if (callCount ===
|
|
225
|
+
if (callCount === 3) {
|
|
222
226
|
// tmux new-session
|
|
223
227
|
return mockSpawnResult("", "", 0);
|
|
224
228
|
}
|
|
@@ -230,7 +234,8 @@ describe("createSession", () => {
|
|
|
230
234
|
expect(pid).toBe(5555);
|
|
231
235
|
|
|
232
236
|
// The tmux command should contain the original command
|
|
233
|
-
|
|
237
|
+
// Call 0: which ov, Call 1: which overstory, Call 2: tmux new-session
|
|
238
|
+
const tmuxCallArgs = spawnSpy.mock.calls[2] as unknown[];
|
|
234
239
|
const cmd = tmuxCallArgs[0] as string[];
|
|
235
240
|
const tmuxCmd = cmd[7] as string;
|
|
236
241
|
expect(tmuxCmd).toContain("echo test");
|
package/src/worktree/tmux.ts
CHANGED
|
@@ -13,30 +13,31 @@ import { AgentError } from "../errors.ts";
|
|
|
13
13
|
/**
|
|
14
14
|
* Detect the directory containing the overstory binary.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Tries `which ov` first (the short alias), then falls back to
|
|
17
|
+
* `which overstory` (the original name). Both are registered in
|
|
18
|
+
* package.json bin, but depending on how the tool was installed
|
|
19
|
+
* (bun link, npm link, global install), only one may be on PATH.
|
|
19
20
|
*
|
|
20
21
|
* Returns null if detection fails.
|
|
21
22
|
*/
|
|
22
23
|
async function detectOverstoryBinDir(): Promise<string | null> {
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
// Try both command names — the alias migration may leave only one resolvable
|
|
25
|
+
for (const cmdName of ["ov", "overstory"]) {
|
|
26
|
+
try {
|
|
27
|
+
const proc = Bun.spawn(["which", cmdName], {
|
|
28
|
+
stdout: "pipe",
|
|
29
|
+
stderr: "pipe",
|
|
30
|
+
});
|
|
31
|
+
const exitCode = await proc.exited;
|
|
32
|
+
if (exitCode === 0) {
|
|
33
|
+
const binPath = (await new Response(proc.stdout).text()).trim();
|
|
34
|
+
if (binPath.length > 0) {
|
|
35
|
+
return dirname(resolve(binPath));
|
|
36
|
+
}
|
|
36
37
|
}
|
|
38
|
+
} catch {
|
|
39
|
+
// which not available or command not on PATH — try next
|
|
37
40
|
}
|
|
38
|
-
} catch {
|
|
39
|
-
// which not available or overstory not on PATH
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
// Fallback: if process.argv[1] points to overstory's own entry point (src/index.ts),
|
package/templates/CLAUDE.md.tmpl
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# {{PROJECT_NAME}} — Overstory Orchestration
|
|
2
2
|
|
|
3
|
-
> Auto-generated by `
|
|
3
|
+
> Auto-generated by `ov init`. You may edit this file.
|
|
4
4
|
|
|
5
5
|
This project uses **overstory** for Claude Code agent orchestration. Your session
|
|
6
6
|
acts as the orchestrator: you decide what work to delegate, spawn worker agents,
|
|
@@ -10,34 +10,34 @@ monitor progress, and merge results.
|
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
# Spawn a worker agent
|
|
13
|
-
|
|
13
|
+
ov sling <bead-id> --capability <type> --name <agent-name> \
|
|
14
14
|
[--spec <path>] [--files file1,file2] [--parent <agent>] [--depth <n>]
|
|
15
15
|
|
|
16
16
|
# Check system status
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
ov status # Overview of all agents, worktrees, {{TRACKER_NAME}}
|
|
18
|
+
ov status --json # Machine-readable output
|
|
19
|
+
ov status --watch # Live updating
|
|
20
20
|
|
|
21
21
|
# Messaging (SQLite-backed, ~1-5ms per query)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
ov mail send --to <agent> --subject "..." --body "..."
|
|
23
|
+
ov mail check # Your inbox
|
|
24
|
+
ov mail list --unread # All unread messages
|
|
25
|
+
ov mail reply <id> --body "..."
|
|
26
26
|
|
|
27
27
|
# Merge completed work
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
ov merge --branch <name> # Merge a specific branch
|
|
29
|
+
ov merge --all # Merge all completed branches
|
|
30
|
+
ov merge --dry-run --branch <name> # Preview conflicts
|
|
31
31
|
|
|
32
32
|
# Worktree management
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
ov worktree list # Show all worktrees with status
|
|
34
|
+
ov worktree clean --completed # Remove finished worktrees
|
|
35
35
|
|
|
36
36
|
# Context and monitoring
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
ov prime # Reload context (config, mulch, recent activity)
|
|
38
|
+
ov watch --background # Start watchdog daemon
|
|
39
|
+
ov metrics # Performance summary
|
|
40
|
+
ov log <event> --agent <name> # Hook-driven event logging
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
## How to Spawn Agents
|
|
@@ -46,10 +46,10 @@ overstory log <event> --agent <name> # Hook-driven event logging
|
|
|
46
46
|
2. Choose a capability based on the task:
|
|
47
47
|
{{AGENT_DEFINITIONS}}
|
|
48
48
|
3. Assign exclusive file scope so agents do not conflict
|
|
49
|
-
4. Spawn: `
|
|
49
|
+
4. Spawn: `ov sling <bead-id> --capability <type> --name <unique-name> --files src/foo.ts,src/bar.ts`
|
|
50
50
|
|
|
51
51
|
Each spawned agent gets its own git worktree, branch, CLAUDE.md overlay, and
|
|
52
|
-
tmux session. Agents communicate via `
|
|
52
|
+
tmux session. Agents communicate via `ov mail` and report completion
|
|
53
53
|
by closing their {{TRACKER_NAME}} issue (`{{TRACKER_CLI}} close <id> --reason "summary"`).
|
|
54
54
|
|
|
55
55
|
## Hierarchical Delegation
|
|
@@ -58,10 +58,10 @@ You can spawn **team leads** that themselves spawn sub-workers:
|
|
|
58
58
|
|
|
59
59
|
```
|
|
60
60
|
Orchestrator (this session)
|
|
61
|
-
└──
|
|
62
|
-
├──
|
|
63
|
-
├──
|
|
64
|
-
└──
|
|
61
|
+
└── ov sling bd-xyz --capability lead --name build-lead
|
|
62
|
+
├── ov sling bd-abc --capability builder --name auth-login
|
|
63
|
+
├── ov sling bd-def --capability builder --name auth-signup
|
|
64
|
+
└── ov sling bd-ghi --capability builder --name auth-reset
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
Depth limit is configurable (default: 2). Leads use `--parent` and `--depth`
|
|
@@ -69,7 +69,7 @@ to track hierarchy.
|
|
|
69
69
|
|
|
70
70
|
## Checking Status
|
|
71
71
|
|
|
72
|
-
Run `
|
|
72
|
+
Run `ov status` to see:
|
|
73
73
|
- Active agents and their states (booting, working, stalled, zombie)
|
|
74
74
|
- Worktree locations and branches
|
|
75
75
|
- Beads issue progress
|
|
@@ -83,7 +83,7 @@ All merges target **{{CANONICAL_BRANCH}}**. Agents work on branches named
|
|
|
83
83
|
## Conventions
|
|
84
84
|
|
|
85
85
|
- Agents own files exclusively — no two agents modify the same file
|
|
86
|
-
- Use `
|
|
86
|
+
- Use `ov mail` for all inter-agent communication (not {{TRACKER_NAME}})
|
|
87
87
|
- Use `{{TRACKER_CLI}} close` to report task completion (not mail)
|
|
88
|
-
- Merge via `
|
|
88
|
+
- Merge via `ov merge`, not raw `git merge`
|
|
89
89
|
- Logs live in `.overstory/logs/` — never delete them manually
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0;
|
|
9
|
+
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0; ov prime --agent {{AGENT_NAME}}"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "command",
|
|
13
|
+
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0; ov mail check --inject --agent {{AGENT_NAME}}"
|
|
10
14
|
}
|
|
11
15
|
]
|
|
12
16
|
}
|
|
@@ -17,7 +21,7 @@
|
|
|
17
21
|
"hooks": [
|
|
18
22
|
{
|
|
19
23
|
"type": "command",
|
|
20
|
-
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0;
|
|
24
|
+
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0; ov mail check --inject --agent {{AGENT_NAME}}"
|
|
21
25
|
}
|
|
22
26
|
]
|
|
23
27
|
}
|
|
@@ -28,7 +32,7 @@
|
|
|
28
32
|
"hooks": [
|
|
29
33
|
{
|
|
30
34
|
"type": "command",
|
|
31
|
-
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0;
|
|
35
|
+
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0; ov log tool-start --agent {{AGENT_NAME}} --stdin"
|
|
32
36
|
}
|
|
33
37
|
]
|
|
34
38
|
},
|
|
@@ -37,7 +41,7 @@
|
|
|
37
41
|
"hooks": [
|
|
38
42
|
{
|
|
39
43
|
"type": "command",
|
|
40
|
-
"command": "read -r INPUT; CMD=$(echo \"$INPUT\" | sed 's/.*\"command\": *\"\\([^\"]*\\)\".*/\\1/'); if echo \"$CMD\" | grep -qE '\\bgit\\s+push\\b'; then echo '{\"decision\":\"block\",\"reason\":\"git push is blocked — use
|
|
44
|
+
"command": "read -r INPUT; CMD=$(echo \"$INPUT\" | sed 's/.*\"command\": *\"\\([^\"]*\\)\".*/\\1/'); if echo \"$CMD\" | grep -qE '\\bgit\\s+push\\b'; then echo '{\"decision\":\"block\",\"reason\":\"git push is blocked — use ov merge to integrate changes, push manually when ready\"}'; exit 0; fi;"
|
|
41
45
|
}
|
|
42
46
|
]
|
|
43
47
|
}
|
|
@@ -48,11 +52,11 @@
|
|
|
48
52
|
"hooks": [
|
|
49
53
|
{
|
|
50
54
|
"type": "command",
|
|
51
|
-
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0;
|
|
55
|
+
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0; ov log tool-end --agent {{AGENT_NAME}} --stdin"
|
|
52
56
|
},
|
|
53
57
|
{
|
|
54
58
|
"type": "command",
|
|
55
|
-
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0;
|
|
59
|
+
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0; ov mail check --inject --agent {{AGENT_NAME}} --debounce 500"
|
|
56
60
|
}
|
|
57
61
|
]
|
|
58
62
|
},
|
|
@@ -61,7 +65,7 @@
|
|
|
61
65
|
"hooks": [
|
|
62
66
|
{
|
|
63
67
|
"type": "command",
|
|
64
|
-
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0;
|
|
68
|
+
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0; ov mail check --inject --agent {{AGENT_NAME}} --debounce 30000"
|
|
65
69
|
}
|
|
66
70
|
]
|
|
67
71
|
},
|
|
@@ -70,7 +74,7 @@
|
|
|
70
74
|
"hooks": [
|
|
71
75
|
{
|
|
72
76
|
"type": "command",
|
|
73
|
-
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0; read -r INPUT; if echo \"$INPUT\" | grep -qE '\\bgit\\s+commit\\b'; then
|
|
77
|
+
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0; read -r INPUT; if echo \"$INPUT\" | grep -qE '\\bgit\\s+commit\\b'; then ml diff HEAD~1 >/dev/null 2>&1 || true; fi; exit 0;"
|
|
74
78
|
}
|
|
75
79
|
]
|
|
76
80
|
}
|
|
@@ -81,11 +85,11 @@
|
|
|
81
85
|
"hooks": [
|
|
82
86
|
{
|
|
83
87
|
"type": "command",
|
|
84
|
-
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0;
|
|
88
|
+
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0; ov log session-end --agent {{AGENT_NAME}} --stdin"
|
|
85
89
|
},
|
|
86
90
|
{
|
|
87
91
|
"type": "command",
|
|
88
|
-
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0;
|
|
92
|
+
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0; ml learn"
|
|
89
93
|
}
|
|
90
94
|
]
|
|
91
95
|
}
|
|
@@ -96,7 +100,7 @@
|
|
|
96
100
|
"hooks": [
|
|
97
101
|
{
|
|
98
102
|
"type": "command",
|
|
99
|
-
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0;
|
|
103
|
+
"command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0; ov prime --agent {{AGENT_NAME}} --compact"
|
|
100
104
|
}
|
|
101
105
|
]
|
|
102
106
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# DO NOT EDIT — Auto-generated by overstory
|
|
2
|
-
# This overlay was written by `
|
|
2
|
+
# This overlay was written by `ov sling`. Manual edits will be
|
|
3
3
|
# overwritten on the next spawn. Modify the template at
|
|
4
4
|
# templates/overlay.md.tmpl in the overstory repo instead.
|
|
5
5
|
|
|
@@ -50,26 +50,26 @@ Prime relevant domain knowledge before starting work:
|
|
|
50
50
|
|
|
51
51
|
## Communication
|
|
52
52
|
|
|
53
|
-
Use `
|
|
53
|
+
Use `ov mail` for all communication. Your address is **{{AGENT_NAME}}**.
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
# Check your inbox (do this regularly)
|
|
57
|
-
|
|
57
|
+
ov mail check --agent {{AGENT_NAME}}
|
|
58
58
|
|
|
59
59
|
# Send a status update to your parent
|
|
60
|
-
|
|
60
|
+
ov mail send --to {{PARENT_AGENT}} --subject "status" \
|
|
61
61
|
--body "Progress update here" --type status --agent {{AGENT_NAME}}
|
|
62
62
|
|
|
63
63
|
# Ask a question
|
|
64
|
-
|
|
64
|
+
ov mail send --to {{PARENT_AGENT}} --subject "question" \
|
|
65
65
|
--body "Your question here" --type question --priority high --agent {{AGENT_NAME}}
|
|
66
66
|
|
|
67
67
|
# Report completion
|
|
68
|
-
|
|
68
|
+
ov mail send --to {{PARENT_AGENT}} --subject "done" \
|
|
69
69
|
--body "Summary of what was done" --type result --agent {{AGENT_NAME}}
|
|
70
70
|
|
|
71
71
|
# Reply to a message
|
|
72
|
-
|
|
72
|
+
ov mail reply <message-id> --body "Your reply" --agent {{AGENT_NAME}}
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
## Spawning Sub-Workers
|