@os-eco/overstory-cli 0.7.2 → 0.7.3
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 +1 -1
- package/package.json +1 -1
- package/src/agents/hooks-deployer.test.ts +6 -5
- package/src/agents/identity.test.ts +3 -2
- package/src/agents/manifest.test.ts +4 -3
- package/src/agents/overlay.test.ts +3 -2
- package/src/commands/agents.test.ts +5 -4
- package/src/commands/completions.test.ts +8 -5
- package/src/commands/completions.ts +37 -1
- package/src/commands/costs.test.ts +4 -3
- package/src/commands/dashboard.test.ts +265 -6
- package/src/commands/dashboard.ts +367 -64
- package/src/commands/doctor.test.ts +3 -2
- package/src/commands/errors.test.ts +3 -2
- package/src/commands/feed.test.ts +3 -2
- package/src/commands/feed.ts +2 -29
- package/src/commands/inspect.test.ts +3 -2
- package/src/commands/log.test.ts +248 -8
- package/src/commands/log.ts +193 -110
- package/src/commands/logs.test.ts +3 -2
- package/src/commands/mail.test.ts +3 -2
- package/src/commands/metrics.test.ts +4 -3
- package/src/commands/nudge.test.ts +3 -2
- package/src/commands/prime.test.ts +2 -2
- package/src/commands/replay.test.ts +3 -2
- package/src/commands/run.test.ts +2 -1
- package/src/commands/sling.test.ts +127 -0
- package/src/commands/sling.ts +101 -3
- package/src/commands/status.test.ts +8 -8
- package/src/commands/trace.test.ts +3 -2
- package/src/commands/watch.test.ts +3 -2
- package/src/config.test.ts +3 -3
- package/src/doctor/agents.test.ts +3 -2
- package/src/doctor/logs.test.ts +3 -2
- package/src/doctor/structure.test.ts +3 -2
- package/src/index.ts +3 -1
- package/src/logging/color.ts +1 -1
- package/src/logging/format.test.ts +110 -0
- package/src/logging/format.ts +42 -1
- package/src/logging/logger.test.ts +3 -2
- package/src/mail/client.test.ts +3 -2
- package/src/mail/store.test.ts +3 -2
- package/src/merge/queue.test.ts +3 -2
- package/src/merge/resolver.test.ts +39 -0
- package/src/merge/resolver.ts +1 -1
- package/src/mulch/client.test.ts +63 -2
- package/src/mulch/client.ts +62 -1
- package/src/runtimes/claude.test.ts +4 -3
- package/src/runtimes/pi-guards.test.ts +26 -2
- package/src/runtimes/pi-guards.ts +3 -3
- package/src/schema-consistency.test.ts +4 -2
- package/src/sessions/compat.test.ts +3 -2
- package/src/sessions/store.test.ts +3 -2
- package/src/test-helpers.ts +20 -1
- package/src/watchdog/daemon.test.ts +4 -3
- package/src/watchdog/triage.test.ts +3 -2
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@ Every command supports `--json` where noted. Global flags: `-q`/`--quiet`, `--ti
|
|
|
75
75
|
| Command | Description |
|
|
76
76
|
|---------|-------------|
|
|
77
77
|
| `ov init` | Initialize `.overstory/` in current project (`--yes`, `--name`) |
|
|
78
|
-
| `ov sling <task-id>` | Spawn a worker agent (`--capability`, `--name`, `--spec`, `--files`, `--parent`, `--depth`, `--skip-scout`, `--skip-review`, `--max-agents`, `--dispatch-max-agents`, `--skip-task-check`, `--runtime`, `--json`) |
|
|
78
|
+
| `ov sling <task-id>` | Spawn a worker agent (`--capability`, `--name`, `--spec`, `--files`, `--parent`, `--depth`, `--skip-scout`, `--skip-review`, `--max-agents`, `--dispatch-max-agents`, `--skip-task-check`, `--no-scout-check`, `--runtime`, `--json`) |
|
|
79
79
|
| `ov stop <agent-name>` | Terminate a running agent (`--clean-worktree`, `--json`) |
|
|
80
80
|
| `ov prime` | Load context for orchestrator/agent (`--agent`, `--compact`) |
|
|
81
81
|
| `ov spec write <task-id>` | Write a task specification (`--body`) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@os-eco/overstory-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Multi-agent orchestration for Claude Code — spawn worker agents in git worktrees via tmux, coordinate through SQLite mail, merge with tiered conflict resolution",
|
|
5
5
|
"author": "Jaymin West",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtemp
|
|
2
|
+
import { mkdtemp } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { AgentError } from "../errors.ts";
|
|
6
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
6
7
|
import {
|
|
7
8
|
buildBashFileGuardScript,
|
|
8
9
|
buildBashPathBoundaryScript,
|
|
@@ -26,7 +27,7 @@ describe("deployHooks", () => {
|
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
afterEach(async () => {
|
|
29
|
-
await
|
|
30
|
+
await cleanupTempDir(tempDir);
|
|
30
31
|
});
|
|
31
32
|
|
|
32
33
|
test("creates .claude/settings.local.json in worktree directory", async () => {
|
|
@@ -1482,7 +1483,7 @@ describe("structural enforcement integration", () => {
|
|
|
1482
1483
|
});
|
|
1483
1484
|
|
|
1484
1485
|
afterEach(async () => {
|
|
1485
|
-
await
|
|
1486
|
+
await cleanupTempDir(tempDir);
|
|
1486
1487
|
});
|
|
1487
1488
|
|
|
1488
1489
|
test("non-implementation agents have more guards than implementation agents", async () => {
|
|
@@ -2055,7 +2056,7 @@ describe("bash path boundary integration", () => {
|
|
|
2055
2056
|
});
|
|
2056
2057
|
|
|
2057
2058
|
afterEach(async () => {
|
|
2058
|
-
await
|
|
2059
|
+
await cleanupTempDir(tempDir);
|
|
2059
2060
|
});
|
|
2060
2061
|
|
|
2061
2062
|
test("builder gets Bash path boundary guard in deployed hooks", async () => {
|
|
@@ -2249,7 +2250,7 @@ describe("PATH prefix in deployed hooks", () => {
|
|
|
2249
2250
|
});
|
|
2250
2251
|
|
|
2251
2252
|
afterEach(async () => {
|
|
2252
|
-
await
|
|
2253
|
+
await cleanupTempDir(tempDir);
|
|
2253
2254
|
});
|
|
2254
2255
|
|
|
2255
2256
|
test("SessionStart hook commands include PATH prefix", async () => {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdir, mkdtemp
|
|
2
|
+
import { mkdir, mkdtemp } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { AgentError } from "../errors.ts";
|
|
6
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
6
7
|
import type { AgentIdentity } from "../types.ts";
|
|
7
8
|
import { createIdentity, loadIdentity, updateIdentity } from "./identity.ts";
|
|
8
9
|
|
|
@@ -14,7 +15,7 @@ describe("identity", () => {
|
|
|
14
15
|
});
|
|
15
16
|
|
|
16
17
|
afterEach(async () => {
|
|
17
|
-
await
|
|
18
|
+
await cleanupTempDir(tempDir);
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
describe("createIdentity", () => {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdir, mkdtemp
|
|
2
|
+
import { mkdir, mkdtemp } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { AgentError } from "../errors.ts";
|
|
6
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
6
7
|
import type { AgentManifest, OverstoryConfig } from "../types.ts";
|
|
7
8
|
import { createManifestLoader, resolveModel, resolveProviderEnv } from "./manifest.ts";
|
|
8
9
|
|
|
@@ -41,7 +42,7 @@ describe("createManifestLoader", () => {
|
|
|
41
42
|
});
|
|
42
43
|
|
|
43
44
|
afterEach(async () => {
|
|
44
|
-
await
|
|
45
|
+
await cleanupTempDir(tempDir);
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
/** Write the manifest JSON and create matching .md files. */
|
|
@@ -806,7 +807,7 @@ describe("manifest validation accepts arbitrary model strings", () => {
|
|
|
806
807
|
});
|
|
807
808
|
|
|
808
809
|
afterEach(async () => {
|
|
809
|
-
await
|
|
810
|
+
await cleanupTempDir(tempDir);
|
|
810
811
|
});
|
|
811
812
|
|
|
812
813
|
test("accepts provider-prefixed model string", async () => {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdir, mkdtemp
|
|
2
|
+
import { mkdir, mkdtemp } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { AgentError } from "../errors.ts";
|
|
6
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
6
7
|
import type { OverlayConfig, QualityGate } from "../types.ts";
|
|
7
8
|
import {
|
|
8
9
|
formatQualityGatesBash,
|
|
@@ -584,7 +585,7 @@ describe("writeOverlay", () => {
|
|
|
584
585
|
});
|
|
585
586
|
|
|
586
587
|
afterEach(async () => {
|
|
587
|
-
await
|
|
588
|
+
await cleanupTempDir(tempDir);
|
|
588
589
|
});
|
|
589
590
|
|
|
590
591
|
test("creates .claude/CLAUDE.md in worktree directory", async () => {
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
|
|
6
|
-
import { mkdtemp
|
|
6
|
+
import { mkdtemp } from "node:fs/promises";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { createSessionStore } from "../sessions/store.ts";
|
|
10
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
10
11
|
import type { AgentSession } from "../types.ts";
|
|
11
12
|
import { agentsCommand, discoverAgents, extractFileScope } from "./agents.ts";
|
|
12
13
|
|
|
@@ -65,7 +66,7 @@ Some expertise here.
|
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
afterEach(async () => {
|
|
68
|
-
await
|
|
69
|
+
await cleanupTempDir(tempDir);
|
|
69
70
|
});
|
|
70
71
|
});
|
|
71
72
|
|
|
@@ -235,7 +236,7 @@ describe("discoverAgents", () => {
|
|
|
235
236
|
});
|
|
236
237
|
|
|
237
238
|
afterEach(async () => {
|
|
238
|
-
await
|
|
239
|
+
await cleanupTempDir(tempDir);
|
|
239
240
|
});
|
|
240
241
|
});
|
|
241
242
|
|
|
@@ -322,6 +323,6 @@ logging:
|
|
|
322
323
|
afterEach(async () => {
|
|
323
324
|
process.stdout.write = originalStdoutWrite;
|
|
324
325
|
process.chdir(originalCwd);
|
|
325
|
-
await
|
|
326
|
+
await cleanupTempDir(tempDir);
|
|
326
327
|
});
|
|
327
328
|
});
|
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
} from "./completions.ts";
|
|
13
13
|
|
|
14
14
|
describe("COMMANDS array", () => {
|
|
15
|
-
it("should have exactly
|
|
16
|
-
expect(COMMANDS).toHaveLength(
|
|
15
|
+
it("should have exactly 33 commands", () => {
|
|
16
|
+
expect(COMMANDS).toHaveLength(33);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
it("should include all expected command names", () => {
|
|
@@ -48,6 +48,9 @@ describe("COMMANDS array", () => {
|
|
|
48
48
|
expect(names).toContain("feed");
|
|
49
49
|
expect(names).toContain("logs");
|
|
50
50
|
expect(names).toContain("stop");
|
|
51
|
+
expect(names).toContain("ecosystem");
|
|
52
|
+
expect(names).toContain("upgrade");
|
|
53
|
+
expect(names).toContain("completions");
|
|
51
54
|
});
|
|
52
55
|
});
|
|
53
56
|
|
|
@@ -59,7 +62,7 @@ describe("generateBash", () => {
|
|
|
59
62
|
expect(script).toContain("_init_completion");
|
|
60
63
|
});
|
|
61
64
|
|
|
62
|
-
it("should include all
|
|
65
|
+
it("should include all 33 command names", () => {
|
|
63
66
|
const script = generateBash();
|
|
64
67
|
for (const cmd of COMMANDS) {
|
|
65
68
|
expect(script).toContain(cmd.name);
|
|
@@ -93,7 +96,7 @@ describe("generateZsh", () => {
|
|
|
93
96
|
expect(script).toContain("_arguments");
|
|
94
97
|
});
|
|
95
98
|
|
|
96
|
-
it("should include all
|
|
99
|
+
it("should include all 33 command names", () => {
|
|
97
100
|
const script = generateZsh();
|
|
98
101
|
for (const cmd of COMMANDS) {
|
|
99
102
|
expect(script).toContain(cmd.name);
|
|
@@ -123,7 +126,7 @@ describe("generateFish", () => {
|
|
|
123
126
|
expect(script).toContain("__fish_use_subcommand");
|
|
124
127
|
});
|
|
125
128
|
|
|
126
|
-
it("should include all
|
|
129
|
+
it("should include all 33 command names", () => {
|
|
127
130
|
const script = generateFish();
|
|
128
131
|
for (const cmd of COMMANDS) {
|
|
129
132
|
expect(script).toContain(cmd.name);
|
|
@@ -58,6 +58,9 @@ export const COMMANDS: readonly CommandDef[] = [
|
|
|
58
58
|
desc: "Initialize .overstory/ in current project",
|
|
59
59
|
flags: [
|
|
60
60
|
{ name: "--force", desc: "Overwrite existing configuration" },
|
|
61
|
+
{ name: "--yes", desc: "Accept all defaults without prompting" },
|
|
62
|
+
{ name: "-y", desc: "Alias for --yes" },
|
|
63
|
+
{ name: "--name", desc: "Project name", takesValue: true },
|
|
61
64
|
{ name: "--help", desc: "Show help" },
|
|
62
65
|
],
|
|
63
66
|
},
|
|
@@ -76,7 +79,13 @@ export const COMMANDS: readonly CommandDef[] = [
|
|
|
76
79
|
{ name: "--files", desc: "Exclusive file scope (comma-separated)", takesValue: true },
|
|
77
80
|
{ name: "--parent", desc: "Parent agent name", takesValue: true },
|
|
78
81
|
{ name: "--depth", desc: "Current hierarchy depth", takesValue: true },
|
|
82
|
+
{ name: "--skip-scout", desc: "Skip scout phase for lead agents" },
|
|
83
|
+
{ name: "--skip-review", desc: "Skip review phase for lead agents" },
|
|
84
|
+
{ name: "--skip-task-check", desc: "Skip task existence validation" },
|
|
79
85
|
{ name: "--force-hierarchy", desc: "Bypass hierarchy validation" },
|
|
86
|
+
{ name: "--max-agents", desc: "Max children per lead", takesValue: true },
|
|
87
|
+
{ name: "--dispatch-max-agents", desc: "Per-lead max agents ceiling", takesValue: true },
|
|
88
|
+
{ name: "--runtime", desc: "Runtime adapter", takesValue: true },
|
|
80
89
|
{ name: "--json", desc: "JSON output" },
|
|
81
90
|
{ name: "--help", desc: "Show help" },
|
|
82
91
|
],
|
|
@@ -107,6 +116,7 @@ export const COMMANDS: readonly CommandDef[] = [
|
|
|
107
116
|
{ name: "--json", desc: "JSON output" },
|
|
108
117
|
{ name: "--verbose", desc: "Extra per-agent detail" },
|
|
109
118
|
{ name: "--agent", desc: "Filter by agent", takesValue: true },
|
|
119
|
+
{ name: "--all", desc: "Show sessions from all runs" },
|
|
110
120
|
{ name: "--watch", desc: "Watch mode" },
|
|
111
121
|
{ name: "--interval", desc: "Poll interval in ms", takesValue: true },
|
|
112
122
|
{ name: "--help", desc: "Show help" },
|
|
@@ -138,6 +148,7 @@ export const COMMANDS: readonly CommandDef[] = [
|
|
|
138
148
|
flags: [
|
|
139
149
|
{ name: "--branch", desc: "Specific branch to merge", takesValue: true },
|
|
140
150
|
{ name: "--all", desc: "All completed branches" },
|
|
151
|
+
{ name: "--into", desc: "Target branch to merge into", takesValue: true },
|
|
141
152
|
{ name: "--dry-run", desc: "Check for conflicts only" },
|
|
142
153
|
{ name: "--json", desc: "JSON output" },
|
|
143
154
|
{ name: "--help", desc: "Show help" },
|
|
@@ -190,8 +201,10 @@ export const COMMANDS: readonly CommandDef[] = [
|
|
|
190
201
|
"merge",
|
|
191
202
|
"logs",
|
|
192
203
|
"version",
|
|
204
|
+
"ecosystem",
|
|
193
205
|
],
|
|
194
206
|
},
|
|
207
|
+
{ name: "--fix", desc: "Attempt to auto-fix issues" },
|
|
195
208
|
{ name: "--help", desc: "Show help" },
|
|
196
209
|
],
|
|
197
210
|
},
|
|
@@ -606,6 +619,29 @@ export const COMMANDS: readonly CommandDef[] = [
|
|
|
606
619
|
},
|
|
607
620
|
],
|
|
608
621
|
},
|
|
622
|
+
{
|
|
623
|
+
name: "ecosystem",
|
|
624
|
+
desc: "Show a summary dashboard of all installed os-eco tools",
|
|
625
|
+
flags: [
|
|
626
|
+
{ name: "--json", desc: "JSON output" },
|
|
627
|
+
{ name: "--help", desc: "Show help" },
|
|
628
|
+
],
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: "upgrade",
|
|
632
|
+
desc: "Upgrade overstory to the latest version",
|
|
633
|
+
flags: [
|
|
634
|
+
{ name: "--check", desc: "Compare current vs latest without installing" },
|
|
635
|
+
{ name: "--all", desc: "Upgrade all os-eco tools" },
|
|
636
|
+
{ name: "--json", desc: "JSON output" },
|
|
637
|
+
{ name: "--help", desc: "Show help" },
|
|
638
|
+
],
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: "completions",
|
|
642
|
+
desc: "Generate shell completions",
|
|
643
|
+
flags: [{ name: "--help", desc: "Show help" }],
|
|
644
|
+
},
|
|
609
645
|
] as const;
|
|
610
646
|
|
|
611
647
|
export function generateBash(): string {
|
|
@@ -618,7 +654,7 @@ export function generateBash(): string {
|
|
|
618
654
|
" local cur prev words cword",
|
|
619
655
|
" _init_completion || return",
|
|
620
656
|
"",
|
|
621
|
-
" local commands='init sling prime stop status dashboard inspect merge nudge clean doctor log logs watch trace errors feed replay costs metrics spec coordinator supervisor hooks monitor mail group worktree run'",
|
|
657
|
+
" local commands='agents init sling prime stop status dashboard inspect merge nudge clean doctor log logs watch trace errors feed replay costs metrics spec coordinator supervisor hooks monitor mail group worktree run ecosystem upgrade completions'",
|
|
622
658
|
"",
|
|
623
659
|
" # Top-level completion",
|
|
624
660
|
" if [[ $cword -eq 1 ]]; then",
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
-
import { mkdir, mkdtemp
|
|
12
|
+
import { mkdir, mkdtemp } from "node:fs/promises";
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { ValidationError } from "../errors.ts";
|
|
16
16
|
import { createMetricsStore } from "../metrics/store.ts";
|
|
17
17
|
import { createSessionStore } from "../sessions/store.ts";
|
|
18
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
18
19
|
import type { SessionMetrics } from "../types.ts";
|
|
19
20
|
import { costsCommand } from "./costs.ts";
|
|
20
21
|
|
|
@@ -72,7 +73,7 @@ describe("costsCommand", () => {
|
|
|
72
73
|
afterEach(async () => {
|
|
73
74
|
process.stdout.write = originalWrite;
|
|
74
75
|
process.chdir(originalCwd);
|
|
75
|
-
await
|
|
76
|
+
await cleanupTempDir(tempDir);
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
function output(): string {
|
|
@@ -1052,7 +1053,7 @@ describe("costsCommand", () => {
|
|
|
1052
1053
|
|
|
1053
1054
|
afterEach(async () => {
|
|
1054
1055
|
process.env.HOME = originalHome;
|
|
1055
|
-
await
|
|
1056
|
+
await cleanupTempDir(tempHome);
|
|
1056
1057
|
});
|
|
1057
1058
|
|
|
1058
1059
|
test("--self shows orchestrator cost when transcript exists", async () => {
|
|
@@ -7,18 +7,27 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
-
import { mkdir, mkdtemp
|
|
10
|
+
import { mkdir, mkdtemp } from "node:fs/promises";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { ValidationError } from "../errors.ts";
|
|
14
|
+
import { createEventStore } from "../events/store.ts";
|
|
15
|
+
import { color } from "../logging/color.ts";
|
|
14
16
|
import { createSessionStore } from "../sessions/store.ts";
|
|
17
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
18
|
+
import type { DashboardStores } from "./dashboard.ts";
|
|
15
19
|
import {
|
|
16
20
|
closeDashboardStores,
|
|
21
|
+
computeAgentPanelHeight,
|
|
17
22
|
dashboardCommand,
|
|
23
|
+
dimBox,
|
|
18
24
|
filterAgentsByRun,
|
|
19
25
|
horizontalLine,
|
|
20
26
|
openDashboardStores,
|
|
21
27
|
pad,
|
|
28
|
+
renderAgentPanel,
|
|
29
|
+
renderFeedPanel,
|
|
30
|
+
renderTasksPanel,
|
|
22
31
|
truncate,
|
|
23
32
|
} from "./dashboard.ts";
|
|
24
33
|
|
|
@@ -40,7 +49,7 @@ describe("dashboardCommand", () => {
|
|
|
40
49
|
|
|
41
50
|
afterEach(async () => {
|
|
42
51
|
process.stdout.write = originalWrite;
|
|
43
|
-
await
|
|
52
|
+
await cleanupTempDir(tempDir);
|
|
44
53
|
});
|
|
45
54
|
|
|
46
55
|
function output(): string {
|
|
@@ -203,6 +212,202 @@ describe("filterAgentsByRun", () => {
|
|
|
203
212
|
});
|
|
204
213
|
});
|
|
205
214
|
|
|
215
|
+
describe("dimBox", () => {
|
|
216
|
+
test("dimBox.vertical equals color.dim(│)", () => {
|
|
217
|
+
expect(dimBox.vertical).toBe(color.dim("│"));
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("dimBox.horizontal equals color.dim(─)", () => {
|
|
221
|
+
expect(dimBox.horizontal).toBe(color.dim("─"));
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("dimBox.tee equals color.dim(├)", () => {
|
|
225
|
+
expect(dimBox.tee).toBe(color.dim("├"));
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("dimBox.teeRight equals color.dim(┤)", () => {
|
|
229
|
+
expect(dimBox.teeRight).toBe(color.dim("┤"));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("dimBox values equal color.dim() applied to their characters", () => {
|
|
233
|
+
// dimBox values are always equal to color.dim(char) regardless of whether
|
|
234
|
+
// Chalk emits ANSI codes (it may suppress them in non-TTY / NO_COLOR envs).
|
|
235
|
+
expect(dimBox.topLeft).toBe(color.dim("┌"));
|
|
236
|
+
expect(dimBox.topRight).toBe(color.dim("┐"));
|
|
237
|
+
expect(dimBox.bottomLeft).toBe(color.dim("└"));
|
|
238
|
+
expect(dimBox.bottomRight).toBe(color.dim("┘"));
|
|
239
|
+
expect(dimBox.cross).toBe(color.dim("┼"));
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("computeAgentPanelHeight", () => {
|
|
244
|
+
test("0 agents: clamps to minimum 8", () => {
|
|
245
|
+
// max(8, min(floor(30*0.5), 0+4)) = max(8, min(15,4)) = max(8,4) = 8
|
|
246
|
+
expect(computeAgentPanelHeight(30, 0)).toBe(8);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("4 agents: still clamps to minimum 8", () => {
|
|
250
|
+
// max(8, min(15, 4+4)) = max(8, 8) = 8
|
|
251
|
+
expect(computeAgentPanelHeight(30, 4)).toBe(8);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("20 agents with height 30: clamps to floor(height*0.5)", () => {
|
|
255
|
+
// max(8, min(15, 24)) = max(8,15) = 15
|
|
256
|
+
expect(computeAgentPanelHeight(30, 20)).toBe(15);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("10 agents with height 30: grows with agent count", () => {
|
|
260
|
+
// max(8, min(15, 14)) = max(8,14) = 14
|
|
261
|
+
expect(computeAgentPanelHeight(30, 10)).toBe(14);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("small height: respects 50% cap", () => {
|
|
265
|
+
// height=20: max(8, min(10, 20+4)) = max(8,10) = 10
|
|
266
|
+
expect(computeAgentPanelHeight(20, 20)).toBe(10);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Helper to build a minimal DashboardData for panel tests
|
|
271
|
+
function makeDashboardData(
|
|
272
|
+
overrides: Partial<{
|
|
273
|
+
tasks: Array<{ id: string; title: string; priority: number; status: string; type: string }>;
|
|
274
|
+
recentEvents: Array<{
|
|
275
|
+
id: number;
|
|
276
|
+
agentName: string;
|
|
277
|
+
eventType: string;
|
|
278
|
+
level: string;
|
|
279
|
+
createdAt: string;
|
|
280
|
+
runId: null;
|
|
281
|
+
sessionId: null;
|
|
282
|
+
toolName: null;
|
|
283
|
+
toolArgs: null;
|
|
284
|
+
toolDurationMs: null;
|
|
285
|
+
data: null;
|
|
286
|
+
}>;
|
|
287
|
+
}> = {},
|
|
288
|
+
) {
|
|
289
|
+
return {
|
|
290
|
+
currentRunId: null,
|
|
291
|
+
status: {
|
|
292
|
+
currentRunId: null,
|
|
293
|
+
agents: [],
|
|
294
|
+
worktrees: [],
|
|
295
|
+
tmuxSessions: [],
|
|
296
|
+
unreadMailCount: 0,
|
|
297
|
+
mergeQueueCount: 0,
|
|
298
|
+
recentMetricsCount: 0,
|
|
299
|
+
},
|
|
300
|
+
recentMail: [],
|
|
301
|
+
mergeQueue: [],
|
|
302
|
+
metrics: { totalSessions: 0, avgDuration: 0, byCapability: {} },
|
|
303
|
+
tasks: overrides.tasks ?? [],
|
|
304
|
+
recentEvents: (overrides.recentEvents as never[]) ?? [],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
describe("renderTasksPanel", () => {
|
|
309
|
+
test("renders task id in output", () => {
|
|
310
|
+
const data = makeDashboardData({
|
|
311
|
+
tasks: [{ id: "t1", title: "Test task", priority: 2, status: "open", type: "task" }],
|
|
312
|
+
});
|
|
313
|
+
const out = renderTasksPanel(data, 1, 80, 10, 1);
|
|
314
|
+
expect(out).toContain("t1");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("renders task title in output", () => {
|
|
318
|
+
const data = makeDashboardData({
|
|
319
|
+
tasks: [{ id: "t1", title: "Test task", priority: 2, status: "open", type: "task" }],
|
|
320
|
+
});
|
|
321
|
+
const out = renderTasksPanel(data, 1, 80, 10, 1);
|
|
322
|
+
expect(out).toContain("Test task");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("renders priority label in output", () => {
|
|
326
|
+
const data = makeDashboardData({
|
|
327
|
+
tasks: [{ id: "t1", title: "Test task", priority: 2, status: "open", type: "task" }],
|
|
328
|
+
});
|
|
329
|
+
const out = renderTasksPanel(data, 1, 80, 10, 1);
|
|
330
|
+
expect(out).toContain("P2");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("shows 'No tracker data' when tasks list is empty", () => {
|
|
334
|
+
const data = makeDashboardData({ tasks: [] });
|
|
335
|
+
const out = renderTasksPanel(data, 1, 80, 10, 1);
|
|
336
|
+
expect(out).toContain("No tracker data");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("renders Tasks header", () => {
|
|
340
|
+
const data = makeDashboardData({ tasks: [] });
|
|
341
|
+
const out = renderTasksPanel(data, 1, 80, 6, 1);
|
|
342
|
+
expect(out).toContain("Tasks");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("renders multiple tasks", () => {
|
|
346
|
+
const data = makeDashboardData({
|
|
347
|
+
tasks: [
|
|
348
|
+
{ id: "abc-001", title: "First task", priority: 1, status: "open", type: "task" },
|
|
349
|
+
{ id: "abc-002", title: "Second task", priority: 3, status: "in_progress", type: "bug" },
|
|
350
|
+
],
|
|
351
|
+
});
|
|
352
|
+
const out = renderTasksPanel(data, 1, 80, 10, 1);
|
|
353
|
+
expect(out).toContain("abc-001");
|
|
354
|
+
expect(out).toContain("abc-002");
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe("renderFeedPanel", () => {
|
|
359
|
+
test("shows 'No recent events' when recentEvents is empty", () => {
|
|
360
|
+
const data = makeDashboardData({ recentEvents: [] });
|
|
361
|
+
const out = renderFeedPanel(data, 1, 80, 8, 1);
|
|
362
|
+
expect(out).toContain("No recent events");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("renders Feed header", () => {
|
|
366
|
+
const data = makeDashboardData({ recentEvents: [] });
|
|
367
|
+
const out = renderFeedPanel(data, 1, 80, 8, 1);
|
|
368
|
+
expect(out).toContain("Feed");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("renders event agent name when events are present", () => {
|
|
372
|
+
const event = {
|
|
373
|
+
id: 1,
|
|
374
|
+
agentName: "test-agent",
|
|
375
|
+
eventType: "tool_end" as const,
|
|
376
|
+
level: "info" as const,
|
|
377
|
+
createdAt: new Date().toISOString(),
|
|
378
|
+
runId: null,
|
|
379
|
+
sessionId: null,
|
|
380
|
+
toolName: null,
|
|
381
|
+
toolArgs: null,
|
|
382
|
+
toolDurationMs: null,
|
|
383
|
+
data: null,
|
|
384
|
+
};
|
|
385
|
+
const data = makeDashboardData({ recentEvents: [event] });
|
|
386
|
+
// formatEventLine is a stub — returns "" — so output won't have agent name from it.
|
|
387
|
+
// But the panel itself should not throw and should render the border structure.
|
|
388
|
+
const out = renderFeedPanel(data, 1, 80, 8, 1);
|
|
389
|
+
// Panel renders without error and contains Feed header
|
|
390
|
+
expect(out).toContain("Feed");
|
|
391
|
+
// At least 1 row rendered (not the "No recent events" path)
|
|
392
|
+
expect(out).not.toContain("No recent events");
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe("renderAgentPanel", () => {
|
|
397
|
+
test("renders Agents header", () => {
|
|
398
|
+
const data = makeDashboardData({});
|
|
399
|
+
const out = renderAgentPanel(data, 100, 12, 3);
|
|
400
|
+
expect(out).toContain("Agents");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("renders with dimmed border characters", () => {
|
|
404
|
+
const data = makeDashboardData({});
|
|
405
|
+
const out = renderAgentPanel(data, 100, 12, 3);
|
|
406
|
+
// dimBox.vertical is a dimmed ANSI string — present in output
|
|
407
|
+
expect(out).toContain(dimBox.vertical);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
206
411
|
describe("openDashboardStores", () => {
|
|
207
412
|
let tempDir: string;
|
|
208
413
|
|
|
@@ -211,14 +416,12 @@ describe("openDashboardStores", () => {
|
|
|
211
416
|
});
|
|
212
417
|
|
|
213
418
|
afterEach(async () => {
|
|
214
|
-
await
|
|
419
|
+
await cleanupTempDir(tempDir);
|
|
215
420
|
});
|
|
216
421
|
|
|
217
422
|
test("sessionStore is non-null when .overstory/ has sessions.db", async () => {
|
|
218
|
-
// Create the .overstory directory and seed a sessions.db via createSessionStore
|
|
219
423
|
const overstoryDir = join(tempDir, ".overstory");
|
|
220
424
|
await mkdir(overstoryDir, { recursive: true });
|
|
221
|
-
// createSessionStore creates and initialises sessions.db
|
|
222
425
|
const seeder = createSessionStore(join(overstoryDir, "sessions.db"));
|
|
223
426
|
seeder.close();
|
|
224
427
|
|
|
@@ -271,6 +474,38 @@ describe("openDashboardStores", () => {
|
|
|
271
474
|
closeDashboardStores(stores);
|
|
272
475
|
}
|
|
273
476
|
});
|
|
477
|
+
|
|
478
|
+
test("eventStore is null when events.db does not exist", async () => {
|
|
479
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
480
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
481
|
+
const seeder = createSessionStore(join(overstoryDir, "sessions.db"));
|
|
482
|
+
seeder.close();
|
|
483
|
+
|
|
484
|
+
const stores = openDashboardStores(tempDir);
|
|
485
|
+
try {
|
|
486
|
+
expect(stores.eventStore).toBeNull();
|
|
487
|
+
} finally {
|
|
488
|
+
closeDashboardStores(stores);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("eventStore is non-null when events.db exists", async () => {
|
|
493
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
494
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
495
|
+
const seeder = createSessionStore(join(overstoryDir, "sessions.db"));
|
|
496
|
+
seeder.close();
|
|
497
|
+
|
|
498
|
+
// Create events.db via createEventStore
|
|
499
|
+
const eventsDb = createEventStore(join(overstoryDir, "events.db"));
|
|
500
|
+
eventsDb.close();
|
|
501
|
+
|
|
502
|
+
const stores = openDashboardStores(tempDir);
|
|
503
|
+
try {
|
|
504
|
+
expect(stores.eventStore).not.toBeNull();
|
|
505
|
+
} finally {
|
|
506
|
+
closeDashboardStores(stores);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
274
509
|
});
|
|
275
510
|
|
|
276
511
|
describe("closeDashboardStores", () => {
|
|
@@ -281,7 +516,7 @@ describe("closeDashboardStores", () => {
|
|
|
281
516
|
});
|
|
282
517
|
|
|
283
518
|
afterEach(async () => {
|
|
284
|
-
await
|
|
519
|
+
await cleanupTempDir(tempDir);
|
|
285
520
|
});
|
|
286
521
|
|
|
287
522
|
test("closing stores does not throw", async () => {
|
|
@@ -305,4 +540,28 @@ describe("closeDashboardStores", () => {
|
|
|
305
540
|
// Second close should not throw due to best-effort try/catch
|
|
306
541
|
expect(() => closeDashboardStores(stores)).not.toThrow();
|
|
307
542
|
});
|
|
543
|
+
|
|
544
|
+
test("closing stores with eventStore does not throw", async () => {
|
|
545
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
546
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
547
|
+
const seeder = createSessionStore(join(overstoryDir, "sessions.db"));
|
|
548
|
+
seeder.close();
|
|
549
|
+
const eventsDb = createEventStore(join(overstoryDir, "events.db"));
|
|
550
|
+
eventsDb.close();
|
|
551
|
+
|
|
552
|
+
const stores = openDashboardStores(tempDir);
|
|
553
|
+
expect(() => closeDashboardStores(stores)).not.toThrow();
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Type check: DashboardStores includes eventStore
|
|
558
|
+
test("DashboardStores type includes eventStore field", () => {
|
|
559
|
+
const stores: DashboardStores = {
|
|
560
|
+
sessionStore: null as never,
|
|
561
|
+
mailStore: null,
|
|
562
|
+
mergeQueue: null,
|
|
563
|
+
metricsStore: null,
|
|
564
|
+
eventStore: null,
|
|
565
|
+
};
|
|
566
|
+
expect(stores.eventStore).toBeNull();
|
|
308
567
|
});
|