@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
|
@@ -761,7 +761,7 @@ describe("mailCommand", () => {
|
|
|
761
761
|
capability: "coordinator",
|
|
762
762
|
worktreePath: "/worktrees/orchestrator",
|
|
763
763
|
branchName: "main",
|
|
764
|
-
|
|
764
|
+
taskId: "bead-001",
|
|
765
765
|
tmuxSession: "overstory-test-orchestrator",
|
|
766
766
|
state: "working" as const,
|
|
767
767
|
pid: 12345,
|
|
@@ -779,7 +779,7 @@ describe("mailCommand", () => {
|
|
|
779
779
|
capability: "builder",
|
|
780
780
|
worktreePath: "/worktrees/builder-1",
|
|
781
781
|
branchName: "builder-1",
|
|
782
|
-
|
|
782
|
+
taskId: "bead-002",
|
|
783
783
|
tmuxSession: "overstory-test-builder-1",
|
|
784
784
|
state: "working" as const,
|
|
785
785
|
pid: 12346,
|
|
@@ -797,7 +797,7 @@ describe("mailCommand", () => {
|
|
|
797
797
|
capability: "builder",
|
|
798
798
|
worktreePath: "/worktrees/builder-2",
|
|
799
799
|
branchName: "builder-2",
|
|
800
|
-
|
|
800
|
+
taskId: "bead-003",
|
|
801
801
|
tmuxSession: "overstory-test-builder-2",
|
|
802
802
|
state: "working" as const,
|
|
803
803
|
pid: 12347,
|
|
@@ -815,7 +815,7 @@ describe("mailCommand", () => {
|
|
|
815
815
|
capability: "scout",
|
|
816
816
|
worktreePath: "/worktrees/scout-1",
|
|
817
817
|
branchName: "scout-1",
|
|
818
|
-
|
|
818
|
+
taskId: "bead-004",
|
|
819
819
|
tmuxSession: "overstory-test-scout-1",
|
|
820
820
|
state: "working" as const,
|
|
821
821
|
pid: 12348,
|
|
@@ -1135,7 +1135,7 @@ describe("mailCommand", () => {
|
|
|
1135
1135
|
| "monitor",
|
|
1136
1136
|
worktreePath: `/worktrees/${session.agentName}`,
|
|
1137
1137
|
branchName: session.agentName,
|
|
1138
|
-
|
|
1138
|
+
taskId: `bead-${idx}`,
|
|
1139
1139
|
tmuxSession: `overstory-test-${session.agentName}`,
|
|
1140
1140
|
state: "working" as const,
|
|
1141
1141
|
pid: 10000 + idx,
|
|
@@ -1175,7 +1175,7 @@ describe("mailCommand", () => {
|
|
|
1175
1175
|
]);
|
|
1176
1176
|
|
|
1177
1177
|
// Verify warning on stderr
|
|
1178
|
-
expect(stderrOutput).toContain("
|
|
1178
|
+
expect(stderrOutput).toContain("Warning:");
|
|
1179
1179
|
expect(stderrOutput).toContain("NO reviewer sessions found");
|
|
1180
1180
|
expect(stderrOutput).toContain("lead-1");
|
|
1181
1181
|
expect(stderrOutput).toContain("2 builder(s)");
|
|
@@ -1208,7 +1208,7 @@ describe("mailCommand", () => {
|
|
|
1208
1208
|
]);
|
|
1209
1209
|
|
|
1210
1210
|
// Verify note on stderr
|
|
1211
|
-
expect(stderrOutput).toContain("
|
|
1211
|
+
expect(stderrOutput).toContain("Note:");
|
|
1212
1212
|
expect(stderrOutput).toContain("Only 1 reviewer(s) for 3 builder(s)");
|
|
1213
1213
|
expect(stderrOutput).toContain("review-verified");
|
|
1214
1214
|
});
|
package/src/commands/mail.ts
CHANGED
|
@@ -373,7 +373,7 @@ async function handleSend(opts: SendOpts, cwd: string): Promise<void> {
|
|
|
373
373
|
);
|
|
374
374
|
} else {
|
|
375
375
|
process.stdout.write(
|
|
376
|
-
|
|
376
|
+
`Broadcast sent to ${recipients.length} recipient${recipients.length === 1 ? "" : "s"} (${to})\n`,
|
|
377
377
|
);
|
|
378
378
|
for (let i = 0; i < recipients.length; i++) {
|
|
379
379
|
const recipient = recipients[i];
|
|
@@ -429,7 +429,7 @@ async function handleSend(opts: SendOpts, cwd: string): Promise<void> {
|
|
|
429
429
|
if (opts.json) {
|
|
430
430
|
process.stdout.write(`${JSON.stringify({ id })}\n`);
|
|
431
431
|
} else {
|
|
432
|
-
process.stdout.write(
|
|
432
|
+
process.stdout.write(`Sent message ${id} to ${to}\n`);
|
|
433
433
|
}
|
|
434
434
|
|
|
435
435
|
// Auto-nudge: write a pending nudge marker instead of sending tmux keys.
|
|
@@ -449,11 +449,28 @@ async function handleSend(opts: SendOpts, cwd: string): Promise<void> {
|
|
|
449
449
|
});
|
|
450
450
|
if (!opts.json) {
|
|
451
451
|
process.stdout.write(
|
|
452
|
-
|
|
452
|
+
`Queued nudge for "${to}" (${nudgeReason}, delivered on next prompt)\n`,
|
|
453
453
|
);
|
|
454
454
|
}
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
+
// For dispatch messages, also send an immediate tmux nudge.
|
|
458
|
+
// Dispatch targets newly spawned agents that may be idle at the welcome
|
|
459
|
+
// screen where file-based nudges can't reach (no hook fires on idle agents).
|
|
460
|
+
// The I/O corruption concern (overstory-ii1o) only applies during active
|
|
461
|
+
// tool execution — newly spawned agents are idle, so sendKeys is safe.
|
|
462
|
+
if (type === "dispatch") {
|
|
463
|
+
try {
|
|
464
|
+
const { nudgeAgent } = await import("./nudge.ts");
|
|
465
|
+
const nudgeMessage = `[DISPATCH] ${subject}: ${body.slice(0, 500)}`;
|
|
466
|
+
// Small delay to let the agent's TUI stabilize after sling
|
|
467
|
+
await Bun.sleep(3_000);
|
|
468
|
+
await nudgeAgent(cwd, to, nudgeMessage, true); // force=true to skip debounce
|
|
469
|
+
} catch {
|
|
470
|
+
// Non-fatal: the file-based nudge is the fallback
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
457
474
|
// Reviewer coverage check for merge_ready (advisory warning)
|
|
458
475
|
if (type === "merge_ready") {
|
|
459
476
|
try {
|
|
@@ -469,13 +486,13 @@ async function handleSend(opts: SendOpts, cwd: string): Promise<void> {
|
|
|
469
486
|
);
|
|
470
487
|
if (myBuilders.length > 0 && myReviewers.length === 0) {
|
|
471
488
|
process.stderr.write(
|
|
472
|
-
`\
|
|
473
|
-
|
|
474
|
-
|
|
489
|
+
`\nWarning: merge_ready sent but NO reviewer sessions found for "${from}".\n` +
|
|
490
|
+
`${myBuilders.length} builder(s) completed without review. This violates the review-before-merge requirement.\n` +
|
|
491
|
+
`Spawn reviewers for each builder before merge. See REVIEW_SKIP in agents/lead.md.\n\n`,
|
|
475
492
|
);
|
|
476
493
|
} else if (myReviewers.length > 0 && myReviewers.length < myBuilders.length) {
|
|
477
494
|
process.stderr.write(
|
|
478
|
-
`\
|
|
495
|
+
`\nNote: Only ${myReviewers.length} reviewer(s) for ${myBuilders.length} builder(s). Ensure all builder work is review-verified.\n\n`,
|
|
479
496
|
);
|
|
480
497
|
}
|
|
481
498
|
} finally {
|
|
@@ -528,7 +545,7 @@ async function handleCheck(opts: CheckOpts, cwd: string): Promise<void> {
|
|
|
528
545
|
|
|
529
546
|
// Prepend a priority banner if there's a pending nudge
|
|
530
547
|
if (pendingNudge) {
|
|
531
|
-
const banner =
|
|
548
|
+
const banner = `PRIORITY: ${pendingNudge.reason} message from ${pendingNudge.from} — "${pendingNudge.subject}"\n\n`;
|
|
532
549
|
process.stdout.write(banner);
|
|
533
550
|
}
|
|
534
551
|
|
|
@@ -544,7 +561,7 @@ async function handleCheck(opts: CheckOpts, cwd: string): Promise<void> {
|
|
|
544
561
|
process.stdout.write("No new messages.\n");
|
|
545
562
|
} else {
|
|
546
563
|
process.stdout.write(
|
|
547
|
-
|
|
564
|
+
`${messages.length} new message${messages.length === 1 ? "" : "s"}:\n\n`,
|
|
548
565
|
);
|
|
549
566
|
for (const msg of messages) {
|
|
550
567
|
process.stdout.write(`${formatMessage(msg)}\n\n`);
|
|
@@ -617,7 +634,7 @@ function handleReply(id: string, opts: ReplyOpts, cwd: string): void {
|
|
|
617
634
|
if (opts.json) {
|
|
618
635
|
process.stdout.write(`${JSON.stringify({ id: replyId })}\n`);
|
|
619
636
|
} else {
|
|
620
|
-
process.stdout.write(
|
|
637
|
+
process.stdout.write(`Reply sent: ${replyId}\n`);
|
|
621
638
|
}
|
|
622
639
|
} finally {
|
|
623
640
|
client.close();
|
|
@@ -677,7 +694,7 @@ export async function mailCommand(args: string[]): Promise<void> {
|
|
|
677
694
|
const root = await resolveProjectRoot(process.cwd());
|
|
678
695
|
|
|
679
696
|
const program = new Command();
|
|
680
|
-
program.name("
|
|
697
|
+
program.name("ov mail").description("Agent messaging system").exitOverride();
|
|
681
698
|
|
|
682
699
|
program
|
|
683
700
|
.command("send")
|
|
@@ -207,7 +207,7 @@ merge:
|
|
|
207
207
|
|
|
208
208
|
const parsed = JSON.parse(output);
|
|
209
209
|
expect(parsed.agentName).toBe("my-builder");
|
|
210
|
-
expect(parsed.
|
|
210
|
+
expect(parsed.taskId).toBe("bead-xyz");
|
|
211
211
|
});
|
|
212
212
|
});
|
|
213
213
|
|
|
@@ -264,13 +264,13 @@ merge:
|
|
|
264
264
|
const queue = createMergeQueue(queuePath);
|
|
265
265
|
queue.enqueue({
|
|
266
266
|
branchName: branch1,
|
|
267
|
-
|
|
267
|
+
taskId: "bead-001",
|
|
268
268
|
agentName: "agent1",
|
|
269
269
|
filesModified: [`src/${branch1}.ts`],
|
|
270
270
|
});
|
|
271
271
|
queue.enqueue({
|
|
272
272
|
branchName: branch2,
|
|
273
|
-
|
|
273
|
+
taskId: "bead-002",
|
|
274
274
|
agentName: "agent2",
|
|
275
275
|
filesModified: [`src/${branch2}.ts`],
|
|
276
276
|
});
|
|
@@ -306,13 +306,13 @@ merge:
|
|
|
306
306
|
const queue = createMergeQueue(queuePath);
|
|
307
307
|
queue.enqueue({
|
|
308
308
|
branchName: branch1,
|
|
309
|
-
|
|
309
|
+
taskId: "bead-100",
|
|
310
310
|
agentName: "builder1",
|
|
311
311
|
filesModified: [`src/${branch1}.ts`],
|
|
312
312
|
});
|
|
313
313
|
queue.enqueue({
|
|
314
314
|
branchName: branch2,
|
|
315
|
-
|
|
315
|
+
taskId: "bead-200",
|
|
316
316
|
agentName: "builder2",
|
|
317
317
|
filesModified: [`src/${branch2}.ts`],
|
|
318
318
|
});
|
|
@@ -351,7 +351,7 @@ merge:
|
|
|
351
351
|
const queue = createMergeQueue(queuePath);
|
|
352
352
|
queue.enqueue({
|
|
353
353
|
branchName: branch1,
|
|
354
|
-
|
|
354
|
+
taskId: "bead-300",
|
|
355
355
|
agentName: "builder3",
|
|
356
356
|
filesModified: [`src/${branch1}.ts`],
|
|
357
357
|
});
|
|
@@ -449,13 +449,13 @@ merge:
|
|
|
449
449
|
const queue = createMergeQueue(queuePath);
|
|
450
450
|
queue.enqueue({
|
|
451
451
|
branchName: branch1,
|
|
452
|
-
|
|
452
|
+
taskId: "bead-into-all-1",
|
|
453
453
|
agentName: "agent1",
|
|
454
454
|
filesModified: [`src/${branch1}.ts`],
|
|
455
455
|
});
|
|
456
456
|
queue.enqueue({
|
|
457
457
|
branchName: branch2,
|
|
458
|
-
|
|
458
|
+
taskId: "bead-into-all-2",
|
|
459
459
|
agentName: "agent2",
|
|
460
460
|
filesModified: [`src/${branch2}.ts`],
|
|
461
461
|
});
|
package/src/commands/merge.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI command:
|
|
2
|
+
* CLI command: ov merge
|
|
3
3
|
*
|
|
4
4
|
* Merges agent branches back to the canonical branch using
|
|
5
5
|
* the merge queue and tiered conflict resolver.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* ov merge --branch <name> Merge a specific branch
|
|
9
|
+
* ov merge --all Merge all pending branches
|
|
10
|
+
* ov merge --dry-run Check for conflicts without merging
|
|
11
|
+
* ov merge --json Output results as JSON
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { join } from "node:path";
|
|
@@ -29,7 +29,7 @@ export interface MergeOptions {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Extract agent name from a branch following the overstory naming convention.
|
|
32
|
-
* Pattern: overstory/{agentName}/{
|
|
32
|
+
* Pattern: overstory/{agentName}/{taskId}
|
|
33
33
|
* Falls back to "unknown" if the pattern does not match.
|
|
34
34
|
*/
|
|
35
35
|
function parseAgentName(branchName: string): string {
|
|
@@ -41,8 +41,8 @@ function parseAgentName(branchName: string): string {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Extract
|
|
45
|
-
* Pattern: overstory/{agentName}/{
|
|
44
|
+
* Extract task ID from a branch following the overstory naming convention.
|
|
45
|
+
* Pattern: overstory/{agentName}/{taskId}
|
|
46
46
|
* Falls back to "unknown" if the pattern does not match.
|
|
47
47
|
*/
|
|
48
48
|
function parseBeadId(branchName: string): string {
|
|
@@ -89,7 +89,7 @@ function formatResult(result: MergeResult): string {
|
|
|
89
89
|
const statusIcon = result.success ? "Merged" : "Failed";
|
|
90
90
|
const lines: string[] = [
|
|
91
91
|
`Merging branch: ${result.entry.branchName}`,
|
|
92
|
-
` Agent: ${result.entry.agentName} | Task: ${result.entry.
|
|
92
|
+
` Agent: ${result.entry.agentName} | Task: ${result.entry.taskId}`,
|
|
93
93
|
` Files: ${result.entry.filesModified.length} modified`,
|
|
94
94
|
` Result: ${statusIcon} (tier: ${result.tier})`,
|
|
95
95
|
];
|
|
@@ -109,7 +109,7 @@ function formatResult(result: MergeResult): string {
|
|
|
109
109
|
function formatDryRun(entry: MergeEntry): string {
|
|
110
110
|
const lines: string[] = [
|
|
111
111
|
`[dry-run] Branch: ${entry.branchName}`,
|
|
112
|
-
` Agent: ${entry.agentName} | Task: ${entry.
|
|
112
|
+
` Agent: ${entry.agentName} | Task: ${entry.taskId}`,
|
|
113
113
|
` Status: ${entry.status}`,
|
|
114
114
|
` Files: ${entry.filesModified.length} modified`,
|
|
115
115
|
];
|
|
@@ -124,7 +124,7 @@ function formatDryRun(entry: MergeEntry): string {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
|
-
* Entry point for `
|
|
127
|
+
* Entry point for `ov merge [flags]`.
|
|
128
128
|
*
|
|
129
129
|
* @param opts - Command options
|
|
130
130
|
*/
|
|
@@ -136,7 +136,7 @@ export async function mergeCommand(opts: MergeOptions): Promise<void> {
|
|
|
136
136
|
const json = opts.json ?? false;
|
|
137
137
|
|
|
138
138
|
if (!branchName && !all) {
|
|
139
|
-
throw new ValidationError("Either --branch <name> or --all is required for
|
|
139
|
+
throw new ValidationError("Either --branch <name> or --all is required for ov merge", {
|
|
140
140
|
field: "branch|all",
|
|
141
141
|
});
|
|
142
142
|
}
|
|
@@ -176,7 +176,7 @@ export async function mergeCommand(opts: MergeOptions): Promise<void> {
|
|
|
176
176
|
/**
|
|
177
177
|
* Handle merging a specific branch.
|
|
178
178
|
* If the branch is not in the queue, creates a new entry by detecting
|
|
179
|
-
* agent name,
|
|
179
|
+
* agent name, task ID, and modified files from git.
|
|
180
180
|
*/
|
|
181
181
|
async function handleBranch(
|
|
182
182
|
branchName: string,
|
|
@@ -211,12 +211,12 @@ async function handleBranch(
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
const agentName = parseAgentName(branchName);
|
|
214
|
-
const
|
|
214
|
+
const taskId = parseBeadId(branchName);
|
|
215
215
|
const filesModified = await detectModifiedFiles(repoRoot, canonicalBranch, branchName);
|
|
216
216
|
|
|
217
217
|
entry = queue.enqueue({
|
|
218
218
|
branchName,
|
|
219
|
-
|
|
219
|
+
taskId,
|
|
220
220
|
agentName,
|
|
221
221
|
filesModified,
|
|
222
222
|
});
|
|
@@ -54,7 +54,7 @@ describe("metricsCommand", () => {
|
|
|
54
54
|
function makeSession(overrides: Partial<SessionMetrics> = {}): SessionMetrics {
|
|
55
55
|
return {
|
|
56
56
|
agentName: "test-agent",
|
|
57
|
-
|
|
57
|
+
taskId: "bead-001",
|
|
58
58
|
capability: "builder",
|
|
59
59
|
startedAt: new Date(Date.now() - 120_000).toISOString(),
|
|
60
60
|
completedAt: new Date().toISOString(),
|
|
@@ -151,7 +151,7 @@ describe("metricsCommand", () => {
|
|
|
151
151
|
const out = output();
|
|
152
152
|
|
|
153
153
|
// Check summary stats
|
|
154
|
-
expect(out).toContain("
|
|
154
|
+
expect(out).toContain("Session Metrics");
|
|
155
155
|
expect(out).toContain("Total sessions: 3");
|
|
156
156
|
expect(out).toContain("Completed: 2");
|
|
157
157
|
expect(out).toContain("Avg duration:");
|
|
@@ -177,7 +177,7 @@ describe("metricsCommand", () => {
|
|
|
177
177
|
store.recordSession(
|
|
178
178
|
makeSession({
|
|
179
179
|
agentName: "test-builder",
|
|
180
|
-
|
|
180
|
+
taskId: "bead-123",
|
|
181
181
|
capability: "builder",
|
|
182
182
|
}),
|
|
183
183
|
);
|
|
@@ -190,7 +190,7 @@ describe("metricsCommand", () => {
|
|
|
190
190
|
const parsed = JSON.parse(out.trim()) as { sessions: SessionMetrics[] };
|
|
191
191
|
expect(parsed.sessions).toHaveLength(1);
|
|
192
192
|
expect(parsed.sessions[0]?.agentName).toBe("test-builder");
|
|
193
|
-
expect(parsed.sessions[0]?.
|
|
193
|
+
expect(parsed.sessions[0]?.taskId).toBe("bead-123");
|
|
194
194
|
expect(parsed.sessions[0]?.capability).toBe("builder");
|
|
195
195
|
});
|
|
196
196
|
|
|
@@ -203,7 +203,7 @@ describe("metricsCommand", () => {
|
|
|
203
203
|
store.recordSession(
|
|
204
204
|
makeSession({
|
|
205
205
|
agentName: `agent-${i}`,
|
|
206
|
-
|
|
206
|
+
taskId: `bead-${i}`,
|
|
207
207
|
startedAt: new Date(Date.now() - (5 - i) * 1000).toISOString(),
|
|
208
208
|
}),
|
|
209
209
|
);
|
|
@@ -227,7 +227,7 @@ describe("metricsCommand", () => {
|
|
|
227
227
|
store.recordSession(
|
|
228
228
|
makeSession({
|
|
229
229
|
agentName: `agent-${i}`,
|
|
230
|
-
|
|
230
|
+
taskId: `bead-${i}`,
|
|
231
231
|
}),
|
|
232
232
|
);
|
|
233
233
|
}
|
|
@@ -363,7 +363,7 @@ describe("formatDuration helper", () => {
|
|
|
363
363
|
function makeSession(durationMs: number): SessionMetrics {
|
|
364
364
|
return {
|
|
365
365
|
agentName: "test-agent",
|
|
366
|
-
|
|
366
|
+
taskId: "bead-001",
|
|
367
367
|
capability: "builder",
|
|
368
368
|
startedAt: new Date(Date.now() - durationMs).toISOString(),
|
|
369
369
|
completedAt: new Date().toISOString(),
|
package/src/commands/metrics.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI command:
|
|
2
|
+
* CLI command: ov metrics [--last <n>] [--json]
|
|
3
3
|
*
|
|
4
4
|
* Shows metrics summary from SQLite store: session durations, success rates,
|
|
5
5
|
* merge tier distribution, agent utilization.
|
|
@@ -63,7 +63,7 @@ async function executeMetrics(opts: MetricsOpts): Promise<void> {
|
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
process.stdout.write("
|
|
66
|
+
process.stdout.write("Session Metrics\n");
|
|
67
67
|
process.stdout.write(`${"═".repeat(60)}\n\n`);
|
|
68
68
|
|
|
69
69
|
// Summary stats
|
|
@@ -107,7 +107,7 @@ async function executeMetrics(opts: MetricsOpts): Promise<void> {
|
|
|
107
107
|
const status = s.completedAt ? "done" : "running";
|
|
108
108
|
const duration = formatDuration(s.durationMs);
|
|
109
109
|
process.stdout.write(
|
|
110
|
-
` ${s.agentName} [${s.capability}] ${s.
|
|
110
|
+
` ${s.agentName} [${s.capability}] ${s.taskId} | ${status} | ${duration}\n`,
|
|
111
111
|
);
|
|
112
112
|
}
|
|
113
113
|
} finally {
|
|
@@ -39,14 +39,14 @@ describe("buildMonitorBeacon", () => {
|
|
|
39
39
|
expect(beacon).toContain("mulch prime");
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
test("contains startup instruction:
|
|
42
|
+
test("contains startup instruction: ov status --json", () => {
|
|
43
43
|
const beacon = buildMonitorBeacon();
|
|
44
|
-
expect(beacon).toContain("
|
|
44
|
+
expect(beacon).toContain("ov status --json");
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
test("contains startup instruction:
|
|
47
|
+
test("contains startup instruction: ov mail check --agent monitor", () => {
|
|
48
48
|
const beacon = buildMonitorBeacon();
|
|
49
|
-
expect(beacon).toContain("
|
|
49
|
+
expect(beacon).toContain("ov mail check --agent monitor");
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
test("contains startup instruction: patrol loop", () => {
|
|
@@ -86,7 +86,7 @@ describe("monitorCommand", () => {
|
|
|
86
86
|
stdoutSpy.mockRestore();
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
test("--help prints help text containing '
|
|
89
|
+
test("--help prints help text containing 'monitor'", async () => {
|
|
90
90
|
await monitorCommand(["--help"]);
|
|
91
91
|
const output = stdoutWrites.join("");
|
|
92
92
|
expect(output).toContain("monitor");
|
package/src/commands/monitor.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI command:
|
|
2
|
+
* CLI command: ov monitor start|stop|status
|
|
3
3
|
*
|
|
4
4
|
* Manages the persistent Tier 2 monitor agent lifecycle. The monitor runs
|
|
5
5
|
* at the project root (NOT in a worktree), continuously patrols the agent
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Unlike regular agents spawned by sling, the monitor:
|
|
10
10
|
* - Has no worktree (operates on the main working tree)
|
|
11
11
|
* - Has no bead assignment (it monitors, not implements)
|
|
12
|
-
* - Has no overlay CLAUDE.md (context comes via
|
|
12
|
+
* - Has no overlay CLAUDE.md (context comes via ov status + mail)
|
|
13
13
|
* - Persists across patrol cycles
|
|
14
14
|
*/
|
|
15
15
|
|
|
@@ -46,7 +46,7 @@ export function buildMonitorBeacon(): string {
|
|
|
46
46
|
const parts = [
|
|
47
47
|
`[OVERSTORY] ${MONITOR_NAME} (monitor/tier-2) ${timestamp}`,
|
|
48
48
|
"Depth: 0 | Parent: none | Role: continuous fleet patrol",
|
|
49
|
-
`Startup: run mulch prime, check fleet (
|
|
49
|
+
`Startup: run mulch prime, check fleet (ov status --json), check mail (ov mail check --agent ${MONITOR_NAME}), then begin patrol loop`,
|
|
50
50
|
];
|
|
51
51
|
return parts.join(" — ");
|
|
52
52
|
}
|
|
@@ -156,7 +156,7 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
|
|
|
156
156
|
capability: "monitor",
|
|
157
157
|
worktreePath: projectRoot, // Monitor uses project root, not a worktree
|
|
158
158
|
branchName: config.project.canonicalBranch, // Operates on canonical branch
|
|
159
|
-
|
|
159
|
+
taskId: "", // No specific bead assignment
|
|
160
160
|
tmuxSession,
|
|
161
161
|
state: "booting",
|
|
162
162
|
pid,
|
|
@@ -356,7 +356,7 @@ export function createMonitorCommand(): Command {
|
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
/**
|
|
359
|
-
* Entry point for `
|
|
359
|
+
* Entry point for `ov monitor <subcommand>`.
|
|
360
360
|
*/
|
|
361
361
|
export async function monitorCommand(args: string[]): Promise<void> {
|
|
362
362
|
const cmd = createMonitorCommand();
|
|
@@ -46,7 +46,7 @@ function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
|
|
|
46
46
|
capability: "builder",
|
|
47
47
|
worktreePath: "/tmp/wt",
|
|
48
48
|
branchName: "overstory/test-agent/task-1",
|
|
49
|
-
|
|
49
|
+
taskId: "task-1",
|
|
50
50
|
tmuxSession: "overstory-test-agent",
|
|
51
51
|
state: "working",
|
|
52
52
|
pid: 12345,
|
package/src/commands/nudge.ts
CHANGED
|
@@ -283,7 +283,7 @@ export async function nudgeAgent(
|
|
|
283
283
|
export async function nudgeCommand(args: string[]): Promise<void> {
|
|
284
284
|
const program = new Command();
|
|
285
285
|
program
|
|
286
|
-
.name("
|
|
286
|
+
.name("ov nudge")
|
|
287
287
|
.description("Send a text nudge to an agent")
|
|
288
288
|
.argument("<agent-name>", "Name of the agent to nudge")
|
|
289
289
|
.argument("[message...]", "Text to send (default: check mail prompt)")
|
|
@@ -134,7 +134,7 @@ expertiseDomains:
|
|
|
134
134
|
- typescript
|
|
135
135
|
- testing
|
|
136
136
|
recentTasks:
|
|
137
|
-
-
|
|
137
|
+
- taskId: task-001
|
|
138
138
|
summary: "Implemented feature X"
|
|
139
139
|
completedAt: "2026-01-10T12:00:00Z"
|
|
140
140
|
`,
|
|
@@ -161,7 +161,7 @@ recentTasks:
|
|
|
161
161
|
capability: "builder",
|
|
162
162
|
worktreePath: join(tempDir, ".overstory", "worktrees", "active-builder"),
|
|
163
163
|
branchName: "overstory/active-builder/task-001",
|
|
164
|
-
|
|
164
|
+
taskId: "task-001",
|
|
165
165
|
tmuxSession: "overstory-active-builder",
|
|
166
166
|
state: "working",
|
|
167
167
|
pid: 12345,
|
|
@@ -198,7 +198,7 @@ recentTasks:
|
|
|
198
198
|
capability: "builder",
|
|
199
199
|
worktreePath: join(tempDir, ".overstory", "worktrees", "completed-builder"),
|
|
200
200
|
branchName: "overstory/completed-builder/task-002",
|
|
201
|
-
|
|
201
|
+
taskId: "task-002",
|
|
202
202
|
tmuxSession: "overstory-completed-builder",
|
|
203
203
|
state: "completed",
|
|
204
204
|
pid: null,
|
|
@@ -233,7 +233,7 @@ recentTasks:
|
|
|
233
233
|
`${JSON.stringify(
|
|
234
234
|
{
|
|
235
235
|
agentName: "recovery-agent",
|
|
236
|
-
|
|
236
|
+
taskId: "task-003",
|
|
237
237
|
sessionId: "session-003",
|
|
238
238
|
timestamp: new Date().toISOString(),
|
|
239
239
|
progressSummary: "Implemented initial tests for prime command",
|
|
@@ -361,7 +361,7 @@ recentTasks: []
|
|
|
361
361
|
|
|
362
362
|
describe("Gitignore auto-heal", () => {
|
|
363
363
|
const expectedGitignore = `# Wildcard+whitelist: ignore everything, whitelist tracked files
|
|
364
|
-
# Auto-healed by
|
|
364
|
+
# Auto-healed by ov prime on each session start
|
|
365
365
|
*
|
|
366
366
|
!.gitignore
|
|
367
367
|
!config.yaml
|
package/src/commands/prime.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
2
|
+
* `ov prime` command.
|
|
3
3
|
*
|
|
4
4
|
* Loads context for the orchestrator or a specific agent and outputs it
|
|
5
5
|
* to stdout for injection into Claude Code's context via hooks.
|
|
@@ -24,7 +24,7 @@ import { getCurrentSessionName } from "../worktree/tmux.ts";
|
|
|
24
24
|
* Wildcard+whitelist pattern: ignore everything except tracked config files.
|
|
25
25
|
*/
|
|
26
26
|
const OVERSTORY_GITIGNORE = `# Wildcard+whitelist: ignore everything, whitelist tracked files
|
|
27
|
-
# Auto-healed by
|
|
27
|
+
# Auto-healed by ov prime on each session start
|
|
28
28
|
*
|
|
29
29
|
!.gitignore
|
|
30
30
|
!config.yaml
|
|
@@ -65,7 +65,7 @@ function formatMetrics(sessions: SessionMetrics[]): string {
|
|
|
65
65
|
const status = s.completedAt !== null ? "completed" : "in-progress";
|
|
66
66
|
const duration = s.durationMs > 0 ? ` (${Math.round(s.durationMs / 1000)}s)` : "";
|
|
67
67
|
const merge = s.mergeResult !== null ? ` [${s.mergeResult}]` : "";
|
|
68
|
-
lines.push(`- ${s.agentName} (${s.capability}): ${s.
|
|
68
|
+
lines.push(`- ${s.agentName} (${s.capability}): ${s.taskId} — ${status}${duration}${merge}`);
|
|
69
69
|
}
|
|
70
70
|
return lines.join("\n");
|
|
71
71
|
}
|
|
@@ -86,7 +86,7 @@ function formatIdentity(identity: AgentIdentity): string {
|
|
|
86
86
|
if (identity.recentTasks.length > 0) {
|
|
87
87
|
lines.push("Recent tasks:");
|
|
88
88
|
for (const task of identity.recentTasks) {
|
|
89
|
-
lines.push(` - ${task.
|
|
89
|
+
lines.push(` - ${task.taskId}: ${task.summary} (${task.completedAt})`);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -184,7 +184,7 @@ async function outputAgentContext(
|
|
|
184
184
|
const overstoryDir = join(config.project.root, ".overstory");
|
|
185
185
|
const { store } = openSessionStore(overstoryDir);
|
|
186
186
|
let sessionExists = false;
|
|
187
|
-
let boundSession: {
|
|
187
|
+
let boundSession: { taskId: string } | null = null;
|
|
188
188
|
try {
|
|
189
189
|
const agentSession = store.getByName(agentName);
|
|
190
190
|
sessionExists = agentSession !== null;
|
|
@@ -192,9 +192,9 @@ async function outputAgentContext(
|
|
|
192
192
|
agentSession &&
|
|
193
193
|
agentSession.state !== "completed" &&
|
|
194
194
|
agentSession.state !== "zombie" &&
|
|
195
|
-
agentSession.
|
|
195
|
+
agentSession.taskId
|
|
196
196
|
) {
|
|
197
|
-
boundSession = {
|
|
197
|
+
boundSession = { taskId: agentSession.taskId };
|
|
198
198
|
}
|
|
199
199
|
} finally {
|
|
200
200
|
store.close();
|
|
@@ -226,7 +226,7 @@ async function outputAgentContext(
|
|
|
226
226
|
// Activation context: if agent has a bound task, inject it
|
|
227
227
|
if (boundSession) {
|
|
228
228
|
sections.push("\n## Activation");
|
|
229
|
-
sections.push(`You have a bound task: **${boundSession.
|
|
229
|
+
sections.push(`You have a bound task: **${boundSession.taskId}**`);
|
|
230
230
|
sections.push("Read your overlay at `.claude/CLAUDE.md` and begin working immediately.");
|
|
231
231
|
sections.push("Do not wait for dispatch mail. Your assignment was bound at spawn time.");
|
|
232
232
|
}
|
package/src/commands/replay.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI command:
|
|
2
|
+
* CLI command: ov replay [--run <id>] [--agent <name>...] [--json]
|
|
3
3
|
* [--since <ts>] [--until <ts>] [--limit <n>]
|
|
4
4
|
*
|
|
5
5
|
* Shows an interleaved chronological replay of events across multiple agents.
|
package/src/commands/run.test.ts
CHANGED
|
@@ -68,7 +68,7 @@ function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
|
|
|
68
68
|
capability: "builder",
|
|
69
69
|
worktreePath: "/tmp/worktrees/test-agent",
|
|
70
70
|
branchName: "overstory/test-agent/task-1",
|
|
71
|
-
|
|
71
|
+
taskId: "task-1",
|
|
72
72
|
tmuxSession: "overstory-test-agent",
|
|
73
73
|
state: "working",
|
|
74
74
|
pid: 12345,
|
package/src/commands/run.ts
CHANGED
|
@@ -354,13 +354,13 @@ export function createRunCommand(): Command {
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
export async function runCommand(args: string[]): Promise<void> {
|
|
357
|
-
const program = new Command("
|
|
357
|
+
const program = new Command("ov").exitOverride().configureOutput({
|
|
358
358
|
writeOut: (str) => process.stdout.write(str),
|
|
359
359
|
writeErr: (str) => process.stderr.write(str),
|
|
360
360
|
});
|
|
361
361
|
program.addCommand(createRunCommand());
|
|
362
362
|
try {
|
|
363
|
-
await program.parseAsync(["node", "
|
|
363
|
+
await program.parseAsync(["node", "ov", "run", ...args]);
|
|
364
364
|
} catch (err: unknown) {
|
|
365
365
|
if (err instanceof CommanderError) {
|
|
366
366
|
if (err.code === "commander.helpDisplayed" || err.code === "commander.version") return;
|