@os-eco/overstory-cli 0.6.5 → 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 +60 -60
- 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/hooks-deployer.test.ts +23 -26
- package/src/agents/hooks-deployer.ts +9 -5
- package/src/agents/overlay.test.ts +5 -5
- package/src/agents/overlay.ts +11 -11
- package/src/commands/agents.ts +7 -6
- 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 +2 -2
- package/src/commands/coordinator.ts +12 -12
- package/src/commands/costs.ts +1 -1
- package/src/commands/dashboard.ts +8 -8
- 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.ts +19 -19
- package/src/commands/log.ts +3 -3
- package/src/commands/logs.ts +1 -1
- package/src/commands/mail.test.ts +2 -2
- package/src/commands/mail.ts +28 -11
- package/src/commands/merge.ts +7 -7
- package/src/commands/metrics.test.ts +1 -1
- package/src/commands/metrics.ts +2 -2
- package/src/commands/monitor.test.ts +5 -5
- package/src/commands/monitor.ts +4 -4
- package/src/commands/nudge.ts +1 -1
- package/src/commands/prime.test.ts +1 -1
- package/src/commands/prime.ts +2 -2
- package/src/commands/replay.ts +1 -1
- package/src/commands/run.ts +2 -2
- package/src/commands/sling.test.ts +84 -2
- package/src/commands/sling.ts +97 -9
- package/src/commands/spec.ts +8 -9
- package/src/commands/status.test.ts +2 -2
- package/src/commands/status.ts +2 -4
- package/src/commands/stop.ts +2 -2
- package/src/commands/supervisor.test.ts +1 -1
- package/src/commands/supervisor.ts +4 -4
- package/src/commands/trace.test.ts +2 -2
- package/src/commands/trace.ts +4 -4
- package/src/commands/watch.ts +5 -5
- package/src/commands/worktree.test.ts +3 -3
- package/src/commands/worktree.ts +11 -11
- 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/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 +6 -2
- package/src/index.ts +11 -9
- package/src/mail/client.test.ts +1 -1
- package/src/mail/client.ts +2 -2
- package/src/mulch/client.ts +1 -1
- 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
|
@@ -22,13 +22,13 @@ const SAMPLE_HOOKS = {
|
|
|
22
22
|
SessionStart: [
|
|
23
23
|
{
|
|
24
24
|
matcher: "",
|
|
25
|
-
hooks: [{ type: "command", command: "
|
|
25
|
+
hooks: [{ type: "command", command: "ov prime --agent orchestrator" }],
|
|
26
26
|
},
|
|
27
27
|
],
|
|
28
28
|
Stop: [
|
|
29
29
|
{
|
|
30
30
|
matcher: "",
|
|
31
|
-
hooks: [{ type: "command", command: "
|
|
31
|
+
hooks: [{ type: "command", command: "ov log session-end --agent orchestrator" }],
|
|
32
32
|
},
|
|
33
33
|
],
|
|
34
34
|
},
|
|
@@ -106,7 +106,7 @@ describe("hooks install", () => {
|
|
|
106
106
|
const content = await Bun.file(targetPath).text();
|
|
107
107
|
const parsed = JSON.parse(content) as Record<string, unknown>;
|
|
108
108
|
expect(parsed.hooks).toBeDefined();
|
|
109
|
-
expect(content).toContain("
|
|
109
|
+
expect(content).toContain("ov prime");
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
test("preserves existing non-hooks keys in settings.local.json", async () => {
|
|
@@ -182,7 +182,7 @@ describe("hooks install", () => {
|
|
|
182
182
|
// Existing user hook is preserved
|
|
183
183
|
expect(content).toContain("user-hook");
|
|
184
184
|
// Overstory hooks are added
|
|
185
|
-
expect(content).toContain("
|
|
185
|
+
expect(content).toContain("ov prime");
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
test("throws when .overstory/hooks.json does not exist", async () => {
|
|
@@ -284,7 +284,7 @@ describe("hooks install merge behavior", () => {
|
|
|
284
284
|
// User's PreToolUse hook preserved
|
|
285
285
|
expect(content).toContain("user-write-hook");
|
|
286
286
|
// Overstory's SessionStart hook added
|
|
287
|
-
expect(content).toContain("
|
|
287
|
+
expect(content).toContain("ov prime");
|
|
288
288
|
// Both event types present
|
|
289
289
|
expect(parsed.hooks.PreToolUse).toBeDefined();
|
|
290
290
|
expect(parsed.hooks.SessionStart).toBeDefined();
|
|
@@ -361,7 +361,7 @@ describe("hooks install merge behavior", () => {
|
|
|
361
361
|
const parsed = JSON.parse(content) as { hooks: Record<string, unknown[]> };
|
|
362
362
|
expect(parsed.hooks.SessionStart).toBeDefined();
|
|
363
363
|
expect(parsed.hooks.Stop).toBeDefined();
|
|
364
|
-
expect(content).toContain("
|
|
364
|
+
expect(content).toContain("ov prime");
|
|
365
365
|
});
|
|
366
366
|
|
|
367
367
|
describe("mergeHooksByEventType unit tests", () => {
|
|
@@ -424,7 +424,7 @@ describe("hooks status", () => {
|
|
|
424
424
|
const output = await captureStdout(() => hooksCommand(["status"]));
|
|
425
425
|
expect(output).toContain("present");
|
|
426
426
|
expect(output).toContain("no");
|
|
427
|
-
expect(output).toContain("
|
|
427
|
+
expect(output).toContain("ov hooks install");
|
|
428
428
|
});
|
|
429
429
|
|
|
430
430
|
test("reports installed:true when hooks present in .claude/", async () => {
|
package/src/commands/hooks.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI command:
|
|
2
|
+
* CLI command: ov hooks install|uninstall|status
|
|
3
3
|
*
|
|
4
4
|
* Manages orchestrator hooks in .claude/settings.local.json.
|
|
5
|
-
* Hooks are sourced from .overstory/hooks.json (generated by
|
|
5
|
+
* Hooks are sourced from .overstory/hooks.json (generated by ov init).
|
|
6
6
|
*
|
|
7
7
|
* This keeps the canonical hook configuration in .overstory/ while placing
|
|
8
8
|
* a minimal copy in .claude/ only when the user explicitly opts in.
|
|
9
|
-
* Running `
|
|
10
|
-
* run `
|
|
9
|
+
* Running `ov init` alone does NOT modify .claude/ — the user must
|
|
10
|
+
* run `ov hooks install` as a separate step.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { mkdir, unlink } from "node:fs/promises";
|
|
@@ -80,7 +80,7 @@ async function installHooks(force: boolean): Promise<void> {
|
|
|
80
80
|
const sourcePath = join(projectRoot, ".overstory", "hooks.json");
|
|
81
81
|
const sourceFile = Bun.file(sourcePath);
|
|
82
82
|
if (!(await sourceFile.exists())) {
|
|
83
|
-
throw new ValidationError("No hooks.json found in .overstory/. Run '
|
|
83
|
+
throw new ValidationError("No hooks.json found in .overstory/. Run 'ov init' first.", {
|
|
84
84
|
field: "source",
|
|
85
85
|
});
|
|
86
86
|
}
|
|
@@ -199,7 +199,7 @@ async function statusHooks(json: boolean): Promise<void> {
|
|
|
199
199
|
`Hooks installed (.claude/settings.local.json): ${installed ? "yes" : "no"}\n`,
|
|
200
200
|
);
|
|
201
201
|
if (!installed && sourceExists) {
|
|
202
|
-
process.stdout.write(`\nRun '
|
|
202
|
+
process.stdout.write(`\nRun 'ov hooks install' to install.\n`);
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
}
|
|
@@ -234,7 +234,7 @@ export function createHooksCommand(): Command {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
/**
|
|
237
|
-
* Entry point for `
|
|
237
|
+
* Entry point for `ov hooks <subcommand>`.
|
|
238
238
|
*/
|
|
239
239
|
export async function hooksCommand(args: string[]): Promise<void> {
|
|
240
240
|
const cmd = createHooksCommand();
|
|
@@ -20,6 +20,10 @@ const AGENT_DEF_FILES = [
|
|
|
20
20
|
"supervisor.md",
|
|
21
21
|
"coordinator.md",
|
|
22
22
|
"monitor.md",
|
|
23
|
+
"issue-reviews.md",
|
|
24
|
+
"pr-reviews.md",
|
|
25
|
+
"prioritize.md",
|
|
26
|
+
"release.md",
|
|
23
27
|
];
|
|
24
28
|
|
|
25
29
|
/** Resolve the source agents directory (same logic as init.ts). */
|
|
@@ -46,7 +50,7 @@ describe("initCommand: agent-defs deployment", () => {
|
|
|
46
50
|
await cleanupTempDir(tempDir);
|
|
47
51
|
});
|
|
48
52
|
|
|
49
|
-
test("creates .overstory/agent-defs/ with all
|
|
53
|
+
test("creates .overstory/agent-defs/ with all 12 agent definition files", async () => {
|
|
50
54
|
await initCommand({});
|
|
51
55
|
|
|
52
56
|
const agentDefsDir = join(tempDir, ".overstory", "agent-defs");
|
|
@@ -100,7 +104,7 @@ describe("initCommand: agent-defs deployment", () => {
|
|
|
100
104
|
const stopHooks = parsed.hooks.Stop[0].hooks;
|
|
101
105
|
|
|
102
106
|
expect(stopHooks.length).toBe(2);
|
|
103
|
-
expect(stopHooks[0].command).toContain("
|
|
107
|
+
expect(stopHooks[0].command).toContain("ov log session-end");
|
|
104
108
|
expect(stopHooks[1].command).toBe("mulch learn");
|
|
105
109
|
});
|
|
106
110
|
|
package/src/commands/init.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI command:
|
|
2
|
+
* CLI command: ov init [--force]
|
|
3
3
|
*
|
|
4
4
|
* Scaffolds the `.overstory/` directory in the current project with:
|
|
5
5
|
* - config.yaml (serialized from DEFAULT_CONFIG)
|
|
@@ -296,7 +296,7 @@ function buildHooksJson(): string {
|
|
|
296
296
|
hooks: [
|
|
297
297
|
{
|
|
298
298
|
type: "command",
|
|
299
|
-
command: "
|
|
299
|
+
command: "ov prime --agent orchestrator",
|
|
300
300
|
},
|
|
301
301
|
],
|
|
302
302
|
},
|
|
@@ -307,7 +307,7 @@ function buildHooksJson(): string {
|
|
|
307
307
|
hooks: [
|
|
308
308
|
{
|
|
309
309
|
type: "command",
|
|
310
|
-
command: "
|
|
310
|
+
command: "ov mail check --inject --agent orchestrator",
|
|
311
311
|
},
|
|
312
312
|
],
|
|
313
313
|
},
|
|
@@ -328,7 +328,7 @@ function buildHooksJson(): string {
|
|
|
328
328
|
hooks: [
|
|
329
329
|
{
|
|
330
330
|
type: "command",
|
|
331
|
-
command: `${toolNameExtract}
|
|
331
|
+
command: `${toolNameExtract} ov log tool-start --agent orchestrator --tool-name "$TOOL_NAME"`,
|
|
332
332
|
},
|
|
333
333
|
],
|
|
334
334
|
},
|
|
@@ -339,7 +339,7 @@ function buildHooksJson(): string {
|
|
|
339
339
|
hooks: [
|
|
340
340
|
{
|
|
341
341
|
type: "command",
|
|
342
|
-
command: `${toolNameExtract}
|
|
342
|
+
command: `${toolNameExtract} ov log tool-end --agent orchestrator --tool-name "$TOOL_NAME"`,
|
|
343
343
|
},
|
|
344
344
|
],
|
|
345
345
|
},
|
|
@@ -360,7 +360,7 @@ function buildHooksJson(): string {
|
|
|
360
360
|
hooks: [
|
|
361
361
|
{
|
|
362
362
|
type: "command",
|
|
363
|
-
command: "
|
|
363
|
+
command: "ov log session-end --agent orchestrator",
|
|
364
364
|
},
|
|
365
365
|
{
|
|
366
366
|
type: "command",
|
|
@@ -375,7 +375,7 @@ function buildHooksJson(): string {
|
|
|
375
375
|
hooks: [
|
|
376
376
|
{
|
|
377
377
|
type: "command",
|
|
378
|
-
command: "
|
|
378
|
+
command: "ov prime --agent orchestrator --compact",
|
|
379
379
|
},
|
|
380
380
|
],
|
|
381
381
|
},
|
|
@@ -450,11 +450,11 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
450
450
|
/**
|
|
451
451
|
* Content for .overstory/.gitignore — runtime state that should not be tracked.
|
|
452
452
|
* Uses wildcard+whitelist pattern: ignore everything, whitelist tracked files.
|
|
453
|
-
* Auto-healed by
|
|
453
|
+
* Auto-healed by ov prime on each session start.
|
|
454
454
|
* Config files (config.yaml, agent-manifest.json, hooks.json) remain tracked.
|
|
455
455
|
*/
|
|
456
456
|
export const OVERSTORY_GITIGNORE = `# Wildcard+whitelist: ignore everything, whitelist tracked files
|
|
457
|
-
# Auto-healed by
|
|
457
|
+
# Auto-healed by ov prime on each session start
|
|
458
458
|
*
|
|
459
459
|
!.gitignore
|
|
460
460
|
!config.yaml
|
|
@@ -476,13 +476,13 @@ Overstory turns a single Claude Code session into a multi-agent team by spawning
|
|
|
476
476
|
|
|
477
477
|
## Key Commands
|
|
478
478
|
|
|
479
|
-
- \`
|
|
480
|
-
- \`
|
|
481
|
-
- \`
|
|
482
|
-
- \`
|
|
483
|
-
- \`
|
|
484
|
-
- \`
|
|
485
|
-
- \`
|
|
479
|
+
- \`ov init\` — Initialize this directory
|
|
480
|
+
- \`ov status\` — Show active agents and state
|
|
481
|
+
- \`ov sling <id>\` — Spawn a worker agent
|
|
482
|
+
- \`ov mail check\` — Check agent messages
|
|
483
|
+
- \`ov merge\` — Merge agent work back
|
|
484
|
+
- \`ov dashboard\` — Live TUI monitoring
|
|
485
|
+
- \`ov doctor\` — Run health checks
|
|
486
486
|
|
|
487
487
|
## Structure
|
|
488
488
|
|
|
@@ -526,7 +526,7 @@ function printCreated(relativePath: string): void {
|
|
|
526
526
|
}
|
|
527
527
|
|
|
528
528
|
/**
|
|
529
|
-
* Entry point for `
|
|
529
|
+
* Entry point for `ov init [--force]`.
|
|
530
530
|
*
|
|
531
531
|
* Scaffolds the .overstory/ directory structure in the current working directory.
|
|
532
532
|
*
|
|
@@ -636,6 +636,6 @@ export async function initCommand(opts: InitOptions): Promise<void> {
|
|
|
636
636
|
}
|
|
637
637
|
|
|
638
638
|
process.stdout.write("\nDone.\n");
|
|
639
|
-
process.stdout.write(" Next: run `
|
|
640
|
-
process.stdout.write(" Then: run `
|
|
639
|
+
process.stdout.write(" Next: run `ov hooks install` to enable Claude Code hooks.\n");
|
|
640
|
+
process.stdout.write(" Then: run `ov status` to see the current state.\n");
|
|
641
641
|
}
|
package/src/commands/inspect.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI command:
|
|
2
|
+
* CLI command: ov inspect <agent-name>
|
|
3
3
|
*
|
|
4
4
|
* Deep per-agent inspection aggregating data from EventStore, SessionStore,
|
|
5
5
|
* MetricsStore, and tmux capture-pane.
|
|
@@ -35,15 +35,15 @@ function formatDuration(ms: number): string {
|
|
|
35
35
|
function getStateIcon(state: AgentSession["state"]): string {
|
|
36
36
|
switch (state) {
|
|
37
37
|
case "booting":
|
|
38
|
-
return
|
|
38
|
+
return color.green("-");
|
|
39
39
|
case "working":
|
|
40
|
-
return
|
|
40
|
+
return color.cyan(">");
|
|
41
41
|
case "stalled":
|
|
42
|
-
return
|
|
42
|
+
return color.yellow("!");
|
|
43
43
|
case "completed":
|
|
44
|
-
return
|
|
44
|
+
return color.dim("x");
|
|
45
45
|
case "zombie":
|
|
46
|
-
return
|
|
46
|
+
return color.dim("x");
|
|
47
47
|
default:
|
|
48
48
|
return "?";
|
|
49
49
|
}
|
|
@@ -252,31 +252,31 @@ export function printInspectData(data: InspectData): void {
|
|
|
252
252
|
const w = process.stdout.write.bind(process.stdout);
|
|
253
253
|
const { session } = data;
|
|
254
254
|
|
|
255
|
-
w(`\
|
|
255
|
+
w(`\nAgent Inspection: ${session.agentName}\n`);
|
|
256
256
|
w(`${"═".repeat(80)}\n\n`);
|
|
257
257
|
|
|
258
258
|
// Agent state and metadata
|
|
259
259
|
const stateIcon = getStateIcon(session.state);
|
|
260
260
|
w(`${stateIcon} State: ${session.state}\n`);
|
|
261
|
-
w(
|
|
262
|
-
w(
|
|
263
|
-
w(
|
|
264
|
-
w(
|
|
261
|
+
w(`Last activity: ${formatDuration(data.timeSinceLastActivity)} ago\n`);
|
|
262
|
+
w(`Task: ${session.taskId}\n`);
|
|
263
|
+
w(`Capability: ${session.capability}\n`);
|
|
264
|
+
w(`Branch: ${session.branchName}\n`);
|
|
265
265
|
if (session.parentAgent) {
|
|
266
|
-
w(
|
|
266
|
+
w(`Parent: ${session.parentAgent} (depth: ${session.depth})\n`);
|
|
267
267
|
}
|
|
268
|
-
w(
|
|
269
|
-
w(
|
|
268
|
+
w(`Started: ${session.startedAt}\n`);
|
|
269
|
+
w(`Tmux: ${session.tmuxSession}\n`);
|
|
270
270
|
w("\n");
|
|
271
271
|
|
|
272
272
|
// Current file
|
|
273
273
|
if (data.currentFile) {
|
|
274
|
-
w(
|
|
274
|
+
w(`Current file: ${data.currentFile}\n\n`);
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
// Token usage
|
|
278
278
|
if (data.tokenUsage) {
|
|
279
|
-
w("
|
|
279
|
+
w("Token Usage\n");
|
|
280
280
|
w(`${"─".repeat(80)}\n`);
|
|
281
281
|
w(` Input: ${data.tokenUsage.inputTokens.toLocaleString()}\n`);
|
|
282
282
|
w(` Output: ${data.tokenUsage.outputTokens.toLocaleString()}\n`);
|
|
@@ -293,7 +293,7 @@ export function printInspectData(data: InspectData): void {
|
|
|
293
293
|
|
|
294
294
|
// Tool usage statistics (top 10)
|
|
295
295
|
if (data.toolStats.length > 0) {
|
|
296
|
-
w("
|
|
296
|
+
w("Tool Usage (Top 10)\n");
|
|
297
297
|
w(`${"─".repeat(80)}\n`);
|
|
298
298
|
const top10 = data.toolStats.slice(0, 10);
|
|
299
299
|
for (const stat of top10) {
|
|
@@ -306,7 +306,7 @@ export function printInspectData(data: InspectData): void {
|
|
|
306
306
|
|
|
307
307
|
// Recent tool calls
|
|
308
308
|
if (data.recentToolCalls.length > 0) {
|
|
309
|
-
w(
|
|
309
|
+
w(`Recent Tool Calls (last ${data.recentToolCalls.length})\n`);
|
|
310
310
|
w(`${"─".repeat(80)}\n`);
|
|
311
311
|
for (const call of data.recentToolCalls) {
|
|
312
312
|
const time = new Date(call.timestamp).toLocaleTimeString();
|
|
@@ -322,7 +322,7 @@ export function printInspectData(data: InspectData): void {
|
|
|
322
322
|
|
|
323
323
|
// tmux output
|
|
324
324
|
if (data.tmuxOutput) {
|
|
325
|
-
w("
|
|
325
|
+
w("Live Tmux Output\n");
|
|
326
326
|
w(`${"─".repeat(80)}\n`);
|
|
327
327
|
w(`${data.tmuxOutput}\n`);
|
|
328
328
|
w(`${"─".repeat(80)}\n`);
|
package/src/commands/log.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI command:
|
|
2
|
+
* CLI command: ov log <event> --agent <name> [--stdin]
|
|
3
3
|
*
|
|
4
4
|
* Called by Pre/PostToolUse and Stop hooks.
|
|
5
5
|
* Events: tool-start, tool-end, session-end.
|
|
@@ -542,7 +542,7 @@ async function runLog(opts: {
|
|
|
542
542
|
if (agentSession) {
|
|
543
543
|
// Auto-complete the current run when the coordinator exits.
|
|
544
544
|
// This handles the case where the user closes the tmux window
|
|
545
|
-
// without running `
|
|
545
|
+
// without running `ov coordinator stop`.
|
|
546
546
|
if (agentSession.capability === "coordinator") {
|
|
547
547
|
try {
|
|
548
548
|
const currentRunPath = join(config.project.root, ".overstory", "current-run.txt");
|
|
@@ -710,7 +710,7 @@ export function createLogCommand(): Command {
|
|
|
710
710
|
}
|
|
711
711
|
|
|
712
712
|
/**
|
|
713
|
-
* Entry point for `
|
|
713
|
+
* Entry point for `ov log <event> --agent <name>`.
|
|
714
714
|
*/
|
|
715
715
|
export async function logCommand(args: string[]): Promise<void> {
|
|
716
716
|
const cmd = createLogCommand();
|
package/src/commands/logs.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI command:
|
|
2
|
+
* CLI command: ov logs [--agent <name>] [--level <level>] [--since <time>] [--until <time>] [--limit <n>] [--follow] [--json]
|
|
3
3
|
*
|
|
4
4
|
* Queries NDJSON log files from .overstory/logs/{agent-name}/{session-timestamp}/events.ndjson
|
|
5
5
|
* and presents a unified timeline view.
|
|
@@ -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")
|
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";
|
|
@@ -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
|
}
|
|
@@ -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:");
|
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
|
|
@@ -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
|
}
|
|
@@ -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();
|
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)")
|
|
@@ -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
|