@os-eco/overstory-cli 0.7.5 → 0.7.7
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 +12 -8
- package/package.json +1 -1
- package/src/commands/agents.ts +21 -3
- package/src/commands/completions.ts +7 -1
- package/src/commands/costs.test.ts +45 -2
- package/src/commands/costs.ts +42 -13
- package/src/commands/dashboard.test.ts +101 -10
- package/src/commands/dashboard.ts +95 -61
- package/src/commands/doctor.ts +3 -1
- package/src/commands/init.test.ts +366 -27
- package/src/commands/init.ts +194 -2
- package/src/doctor/providers.test.ts +373 -0
- package/src/doctor/providers.ts +250 -0
- package/src/doctor/types.ts +2 -1
- package/src/e2e/init-sling-lifecycle.test.ts +12 -7
- package/src/index.ts +11 -2
- package/src/metrics/pricing.ts +57 -2
- package/src/metrics/store.test.ts +38 -0
- package/src/metrics/store.ts +10 -0
- package/src/metrics/transcript.test.ts +84 -2
- package/src/metrics/transcript.ts +1 -1
- package/src/runtimes/claude.test.ts +1 -1
- package/src/runtimes/codex.test.ts +741 -0
- package/src/runtimes/codex.ts +228 -0
- package/src/runtimes/copilot.test.ts +507 -0
- package/src/runtimes/copilot.ts +226 -0
- package/src/runtimes/pi.test.ts +1 -1
- package/src/runtimes/registry.test.ts +26 -6
- package/src/runtimes/registry.ts +4 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Multi-agent orchestration for AI coding agents.
|
|
|
6
6
|
[](https://github.com/jayminwest/overstory/actions/workflows/ci.yml)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
|
-
Overstory turns a single coding session into a multi-agent team by spawning worker agents in git worktrees via tmux, coordinating them through a custom SQLite mail system, and merging their work back with tiered conflict resolution. A pluggable `AgentRuntime` interface lets you swap between runtimes — Claude Code, [Pi](https://github.com/
|
|
9
|
+
Overstory turns a single coding session into a multi-agent team by spawning worker agents in git worktrees via tmux, coordinating them through a custom SQLite mail system, and merging their work back with tiered conflict resolution. A pluggable `AgentRuntime` interface lets you swap between runtimes — Claude Code, [Pi](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent), or your own adapter.
|
|
10
10
|
|
|
11
11
|
> **Warning: Agent swarms are not a universal solution.** Do not deploy Overstory without understanding the risks of multi-agent orchestration — compounding error rates, cost amplification, debugging complexity, and merge conflicts are the normal case, not edge cases. Read [STEELMAN.md](STEELMAN.md) for a full risk analysis and the [Agentic Engineering Book](https://github.com/jayminwest/agentic-engineering-book) ([web version](https://jayminwest.com/agentic-engineering-book)) before using this tool in production.
|
|
12
12
|
|
|
@@ -15,7 +15,9 @@ Overstory turns a single coding session into a multi-agent team by spawning work
|
|
|
15
15
|
Requires [Bun](https://bun.sh) v1.0+, git, and tmux. At least one supported agent runtime must be installed:
|
|
16
16
|
|
|
17
17
|
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude` CLI)
|
|
18
|
-
- [Pi](https://github.com/
|
|
18
|
+
- [Pi](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) (`pi` CLI)
|
|
19
|
+
- [GitHub Copilot](https://github.com/features/copilot) (`copilot` CLI)
|
|
20
|
+
- [Codex](https://github.com/openai/codex) (`codex` CLI)
|
|
19
21
|
|
|
20
22
|
```bash
|
|
21
23
|
bun install -g @os-eco/overstory-cli
|
|
@@ -77,7 +79,7 @@ Every command supports `--json` where noted. Global flags: `-q`/`--quiet`, `--ti
|
|
|
77
79
|
|
|
78
80
|
| Command | Description |
|
|
79
81
|
|---------|-------------|
|
|
80
|
-
| `ov init` | Initialize `.overstory/`
|
|
82
|
+
| `ov init` | Initialize `.overstory/` and bootstrap os-eco tools (`--yes`, `--name`, `--tools`, `--skip-mulch`, `--skip-seeds`, `--skip-canopy`, `--skip-onboard`, `--json`) |
|
|
81
83
|
| `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`) |
|
|
82
84
|
| `ov stop <agent-name>` | Terminate a running agent (`--clean-worktree`, `--json`) |
|
|
83
85
|
| `ov prime` | Load context for orchestrator/agent (`--agent`, `--compact`) |
|
|
@@ -132,7 +134,7 @@ Every command supports `--json` where noted. Global flags: `-q`/`--quiet`, `--ti
|
|
|
132
134
|
| `ov replay` | Interleaved chronological replay (`--run`, `--agent`, `--since`, `--until`, `--limit`, `--json`) |
|
|
133
135
|
| `ov feed` | Unified real-time event stream (`--follow`, `--interval`, `--agent`, `--run`, `--json`) |
|
|
134
136
|
| `ov logs` | Query NDJSON logs across agents (`--agent`, `--level`, `--since`, `--until`, `--follow`, `--json`) |
|
|
135
|
-
| `ov costs` | Token/cost analysis and breakdown (`--live`, `--self`, `--agent`, `--run`, `--by-capability`, `--last`, `--json`) |
|
|
137
|
+
| `ov costs` | Token/cost analysis and breakdown (`--live`, `--self`, `--agent`, `--run`, `--bead`, `--by-capability`, `--last`, `--json`) |
|
|
136
138
|
| `ov metrics` | Show session metrics (`--last`, `--json`) |
|
|
137
139
|
| `ov run list` | List orchestration runs (`--last`, `--json`) |
|
|
138
140
|
| `ov run show <id>` | Show run details |
|
|
@@ -153,7 +155,7 @@ Every command supports `--json` where noted. Global flags: `-q`/`--quiet`, `--ti
|
|
|
153
155
|
| `ov monitor status` | Show monitor state |
|
|
154
156
|
| `ov log <event>` | Log a hook event (`--agent`) |
|
|
155
157
|
| `ov clean` | Clean up worktrees, sessions, artifacts (`--completed`, `--all`, `--run`) |
|
|
156
|
-
| `ov doctor` | Run health checks on overstory setup (`--category`, `--fix`, `--json`) |
|
|
158
|
+
| `ov doctor` | Run health checks on overstory setup — 11 categories (`--category`, `--fix`, `--json`) |
|
|
157
159
|
| `ov ecosystem` | Show os-eco tool versions and health (`--json`) |
|
|
158
160
|
| `ov upgrade` | Upgrade overstory to latest npm version (`--check`, `--all`, `--json`) |
|
|
159
161
|
| `ov agents discover` | Discover agents by capability/state/parent (`--capability`, `--state`, `--parent`, `--json`) |
|
|
@@ -171,6 +173,8 @@ Overstory is runtime-agnostic. The `AgentRuntime` interface (`src/runtimes/types
|
|
|
171
173
|
|---------|-----|-----------------|--------|
|
|
172
174
|
| Claude Code | `claude` | `settings.local.json` hooks | Stable |
|
|
173
175
|
| Pi | `pi` | `.pi/extensions/` guard extension | Active development |
|
|
176
|
+
| Copilot | `copilot` | (none — `--allow-all-tools`) | Active development |
|
|
177
|
+
| Codex | `codex` | OS-level sandbox (Seatbelt/Landlock) | Active development |
|
|
174
178
|
|
|
175
179
|
## How It Works
|
|
176
180
|
|
|
@@ -240,7 +244,7 @@ overstory/
|
|
|
240
244
|
run.ts Orchestration run lifecycle
|
|
241
245
|
trace.ts Agent/task timeline viewing
|
|
242
246
|
clean.ts Worktree/session cleanup
|
|
243
|
-
doctor.ts Health check runner (
|
|
247
|
+
doctor.ts Health check runner (11 check modules)
|
|
244
248
|
inspect.ts Deep per-agent inspection
|
|
245
249
|
spec.ts Task spec management
|
|
246
250
|
errors.ts Aggregated error view
|
|
@@ -265,9 +269,9 @@ overstory/
|
|
|
265
269
|
watchdog/ Tiered health monitoring (daemon, triage, health)
|
|
266
270
|
logging/ Multi-format logger + sanitizer + reporter + color control + shared theme/format
|
|
267
271
|
metrics/ SQLite metrics + pricing + transcript parsing
|
|
268
|
-
doctor/ Health check modules (
|
|
272
|
+
doctor/ Health check modules (11 checks)
|
|
269
273
|
insights/ Session insight analyzer for auto-expertise
|
|
270
|
-
runtimes/ AgentRuntime abstraction (registry + adapters: Claude, Pi)
|
|
274
|
+
runtimes/ AgentRuntime abstraction (registry + adapters: Claude, Pi, Copilot, Codex)
|
|
271
275
|
tracker/ Pluggable task tracker (beads + seeds backends)
|
|
272
276
|
mulch/ mulch client (programmatic API + CLI wrapper)
|
|
273
277
|
e2e/ End-to-end lifecycle tests
|
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.7",
|
|
4
4
|
"description": "Multi-agent orchestration for AI coding agents — spawn workers in git worktrees via tmux, coordinate through SQLite mail, merge with tiered conflict resolution. Pluggable runtime adapters for Claude Code, Pi, and more.",
|
|
5
5
|
"author": "Jaymin West",
|
|
6
6
|
"license": "MIT",
|
package/src/commands/agents.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { loadConfig } from "../config.ts";
|
|
|
10
10
|
import { ValidationError } from "../errors.ts";
|
|
11
11
|
import { jsonOutput } from "../json.ts";
|
|
12
12
|
import { accent, color } from "../logging/color.ts";
|
|
13
|
+
import { getRuntime } from "../runtimes/registry.ts";
|
|
13
14
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
14
15
|
import { type AgentSession, SUPPORTED_CAPABILITIES } from "../types.ts";
|
|
15
16
|
|
|
@@ -41,12 +42,19 @@ const KNOWN_INSTRUCTION_PATHS = [
|
|
|
41
42
|
* or can't be read.
|
|
42
43
|
*
|
|
43
44
|
* @param worktreePath - Absolute path to the agent's worktree
|
|
45
|
+
* @param runtimeInstructionPath - Optional runtime-specific instruction path to try first
|
|
44
46
|
* @returns Array of file paths (relative to worktree root)
|
|
45
47
|
*/
|
|
46
|
-
export async function extractFileScope(
|
|
48
|
+
export async function extractFileScope(
|
|
49
|
+
worktreePath: string,
|
|
50
|
+
runtimeInstructionPath?: string,
|
|
51
|
+
): Promise<string[]> {
|
|
47
52
|
try {
|
|
48
53
|
let content: string | null = null;
|
|
49
|
-
|
|
54
|
+
const pathsToTry = runtimeInstructionPath
|
|
55
|
+
? [runtimeInstructionPath, ...KNOWN_INSTRUCTION_PATHS]
|
|
56
|
+
: KNOWN_INSTRUCTION_PATHS;
|
|
57
|
+
for (const relPath of pathsToTry) {
|
|
50
58
|
const overlayPath = join(worktreePath, relPath);
|
|
51
59
|
const overlayFile = Bun.file(overlayPath);
|
|
52
60
|
if (await overlayFile.exists()) {
|
|
@@ -112,6 +120,16 @@ export async function discoverAgents(
|
|
|
112
120
|
const overstoryDir = join(root, ".overstory");
|
|
113
121
|
const { store } = openSessionStore(overstoryDir);
|
|
114
122
|
|
|
123
|
+
// Resolve runtime instruction path from config; fall back gracefully if config is absent.
|
|
124
|
+
let runtimeInstructionPath: string | undefined;
|
|
125
|
+
try {
|
|
126
|
+
const config = await loadConfig(root);
|
|
127
|
+
const runtime = getRuntime(undefined, config);
|
|
128
|
+
runtimeInstructionPath = runtime.instructionPath;
|
|
129
|
+
} catch {
|
|
130
|
+
// Config may not exist in all contexts; KNOWN_INSTRUCTION_PATHS will be used as fallback.
|
|
131
|
+
}
|
|
132
|
+
|
|
115
133
|
try {
|
|
116
134
|
const sessions: AgentSession[] = opts?.includeAll ? store.getAll() : store.getActive();
|
|
117
135
|
|
|
@@ -124,7 +142,7 @@ export async function discoverAgents(
|
|
|
124
142
|
// Extract file scopes for each agent
|
|
125
143
|
const agents: DiscoveredAgent[] = await Promise.all(
|
|
126
144
|
filteredSessions.map(async (session) => {
|
|
127
|
-
const fileScope = await extractFileScope(session.worktreePath);
|
|
145
|
+
const fileScope = await extractFileScope(session.worktreePath, runtimeInstructionPath);
|
|
128
146
|
return {
|
|
129
147
|
agentName: session.agentName,
|
|
130
148
|
capability: session.capability,
|
|
@@ -55,12 +55,18 @@ export const COMMANDS: readonly CommandDef[] = [
|
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
name: "init",
|
|
58
|
-
desc: "Initialize .overstory/
|
|
58
|
+
desc: "Initialize .overstory/ and bootstrap os-eco ecosystem tools",
|
|
59
59
|
flags: [
|
|
60
60
|
{ name: "--force", desc: "Overwrite existing configuration" },
|
|
61
61
|
{ name: "--yes", desc: "Accept all defaults without prompting" },
|
|
62
62
|
{ name: "-y", desc: "Alias for --yes" },
|
|
63
63
|
{ name: "--name", desc: "Project name", takesValue: true },
|
|
64
|
+
{ name: "--tools", desc: "Comma-separated list of tools to bootstrap", takesValue: true },
|
|
65
|
+
{ name: "--skip-mulch", desc: "Skip mulch bootstrap" },
|
|
66
|
+
{ name: "--skip-seeds", desc: "Skip seeds bootstrap" },
|
|
67
|
+
{ name: "--skip-canopy", desc: "Skip canopy bootstrap" },
|
|
68
|
+
{ name: "--skip-onboard", desc: "Skip CLAUDE.md onboarding step" },
|
|
69
|
+
{ name: "--json", desc: "Output result as JSON" },
|
|
64
70
|
{ name: "--help", desc: "Show help" },
|
|
65
71
|
],
|
|
66
72
|
},
|
|
@@ -1023,6 +1023,48 @@ describe("costsCommand", () => {
|
|
|
1023
1023
|
});
|
|
1024
1024
|
});
|
|
1025
1025
|
|
|
1026
|
+
// === --bead filter ===
|
|
1027
|
+
|
|
1028
|
+
describe("--bead filter", () => {
|
|
1029
|
+
test("--bead filters by task ID (JSON)", async () => {
|
|
1030
|
+
const dbPath = join(tempDir, ".overstory", "metrics.db");
|
|
1031
|
+
const store = createMetricsStore(dbPath);
|
|
1032
|
+
store.recordSession(makeMetrics({ agentName: "builder-1", taskId: "task-A" }));
|
|
1033
|
+
store.recordSession(makeMetrics({ agentName: "builder-2", taskId: "task-A" }));
|
|
1034
|
+
store.recordSession(
|
|
1035
|
+
makeMetrics({ agentName: "scout-1", taskId: "task-B", capability: "scout" }),
|
|
1036
|
+
);
|
|
1037
|
+
store.close();
|
|
1038
|
+
|
|
1039
|
+
await costsCommand(["--json", "--bead", "task-A"]);
|
|
1040
|
+
const out = output();
|
|
1041
|
+
|
|
1042
|
+
const parsed = JSON.parse(out.trim()) as { sessions: Record<string, unknown>[] };
|
|
1043
|
+
expect(parsed.sessions).toHaveLength(2);
|
|
1044
|
+
expect(parsed.sessions.every((s) => s.taskId === "task-A")).toBe(true);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
test("--bead returns empty for unknown task", async () => {
|
|
1048
|
+
const dbPath = join(tempDir, ".overstory", "metrics.db");
|
|
1049
|
+
const store = createMetricsStore(dbPath);
|
|
1050
|
+
store.recordSession(makeMetrics({ agentName: "builder-1", taskId: "task-A" }));
|
|
1051
|
+
store.close();
|
|
1052
|
+
|
|
1053
|
+
await costsCommand(["--json", "--bead", "nonexistent"]);
|
|
1054
|
+
const out = output();
|
|
1055
|
+
|
|
1056
|
+
const parsed = JSON.parse(out.trim()) as { sessions: unknown[] };
|
|
1057
|
+
expect(parsed.sessions).toEqual([]);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
test("--bead appears in help text", async () => {
|
|
1061
|
+
await costsCommand(["--help"]);
|
|
1062
|
+
const out = output();
|
|
1063
|
+
|
|
1064
|
+
expect(out).toContain("--bead");
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1026
1068
|
// === --self flag ===
|
|
1027
1069
|
|
|
1028
1070
|
describe("--self flag", () => {
|
|
@@ -1111,7 +1153,7 @@ describe("costsCommand", () => {
|
|
|
1111
1153
|
await costsCommand(["--self"]);
|
|
1112
1154
|
const out = output();
|
|
1113
1155
|
|
|
1114
|
-
expect(out).toContain("No
|
|
1156
|
+
expect(out).toContain("No transcript found");
|
|
1115
1157
|
});
|
|
1116
1158
|
|
|
1117
1159
|
test("--self --json outputs error JSON when no transcript found", async () => {
|
|
@@ -1122,7 +1164,8 @@ describe("costsCommand", () => {
|
|
|
1122
1164
|
const out = output();
|
|
1123
1165
|
|
|
1124
1166
|
const parsed = JSON.parse(out.trim()) as Record<string, unknown>;
|
|
1125
|
-
expect(parsed.error).toBe("
|
|
1167
|
+
expect(typeof parsed.error).toBe("string");
|
|
1168
|
+
expect(parsed.error as string).toContain("No transcript found");
|
|
1126
1169
|
});
|
|
1127
1170
|
|
|
1128
1171
|
test("--self in help text", async () => {
|
package/src/commands/costs.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { color } from "../logging/color.ts";
|
|
|
16
16
|
import { renderHeader, separator } from "../logging/theme.ts";
|
|
17
17
|
import { createMetricsStore } from "../metrics/store.ts";
|
|
18
18
|
import { estimateCost, parseTranscriptUsage } from "../metrics/transcript.ts";
|
|
19
|
+
import { getRuntime } from "../runtimes/registry.ts";
|
|
19
20
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
20
21
|
import type { SessionMetrics } from "../types.ts";
|
|
21
22
|
|
|
@@ -43,24 +44,45 @@ function padLeft(str: string, width: number): string {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* Scans ~/.claude/projects/{project-key}/ for JSONL files and returns
|
|
49
|
-
* the most recently modified one, corresponding to the current orchestrator session.
|
|
47
|
+
* Resolve the transcript directory for a given runtime and project root.
|
|
50
48
|
*
|
|
49
|
+
* @param runtimeId - The runtime identifier (e.g. "claude")
|
|
51
50
|
* @param projectRoot - Absolute path to the project root
|
|
52
|
-
* @returns Absolute path to the
|
|
51
|
+
* @returns Absolute path to the transcript directory, or null if not supported
|
|
53
52
|
*/
|
|
54
|
-
|
|
53
|
+
function getTranscriptDir(runtimeId: string, projectRoot: string): string | null {
|
|
55
54
|
const homeDir = process.env.HOME ?? "";
|
|
56
55
|
if (homeDir.length === 0) return null;
|
|
56
|
+
switch (runtimeId) {
|
|
57
|
+
case "claude": {
|
|
58
|
+
const projectKey = projectRoot.replace(/\//g, "-");
|
|
59
|
+
return join(homeDir, ".claude", "projects", projectKey);
|
|
60
|
+
}
|
|
61
|
+
default:
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
57
65
|
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Discover the orchestrator's transcript JSONL file for the given runtime.
|
|
68
|
+
*
|
|
69
|
+
* Scans the runtime-specific transcript directory for JSONL files and returns
|
|
70
|
+
* the most recently modified one, corresponding to the current orchestrator session.
|
|
71
|
+
*
|
|
72
|
+
* @param runtimeId - The runtime identifier (e.g. "claude")
|
|
73
|
+
* @param projectRoot - Absolute path to the project root
|
|
74
|
+
* @returns Absolute path to the most recent transcript, or null if none found
|
|
75
|
+
*/
|
|
76
|
+
async function discoverOrchestratorTranscript(
|
|
77
|
+
runtimeId: string,
|
|
78
|
+
projectRoot: string,
|
|
79
|
+
): Promise<string | null> {
|
|
80
|
+
const transcriptDir = getTranscriptDir(runtimeId, projectRoot);
|
|
81
|
+
if (transcriptDir === null) return null;
|
|
60
82
|
|
|
61
83
|
let entries: string[];
|
|
62
84
|
try {
|
|
63
|
-
entries = await readdir(
|
|
85
|
+
entries = await readdir(transcriptDir);
|
|
64
86
|
} catch {
|
|
65
87
|
return null;
|
|
66
88
|
}
|
|
@@ -72,7 +94,7 @@ async function discoverOrchestratorTranscript(projectRoot: string): Promise<stri
|
|
|
72
94
|
let bestMtime = 0;
|
|
73
95
|
|
|
74
96
|
for (const file of jsonlFiles) {
|
|
75
|
-
const filePath = join(
|
|
97
|
+
const filePath = join(transcriptDir, file);
|
|
76
98
|
try {
|
|
77
99
|
const fileStat = await stat(filePath);
|
|
78
100
|
if (fileStat.mtimeMs > bestMtime) {
|
|
@@ -236,6 +258,7 @@ interface CostsOpts {
|
|
|
236
258
|
byCapability?: boolean;
|
|
237
259
|
agent?: string;
|
|
238
260
|
run?: string;
|
|
261
|
+
bead?: string;
|
|
239
262
|
last?: string;
|
|
240
263
|
json?: boolean;
|
|
241
264
|
}
|
|
@@ -247,6 +270,7 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
|
|
|
247
270
|
const byCapability = opts.byCapability ?? false;
|
|
248
271
|
const agentName = opts.agent;
|
|
249
272
|
const runId = opts.run;
|
|
273
|
+
const beadId = opts.bead;
|
|
250
274
|
const lastStr = opts.last;
|
|
251
275
|
|
|
252
276
|
if (lastStr !== undefined) {
|
|
@@ -267,13 +291,15 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
|
|
|
267
291
|
|
|
268
292
|
// Handle --self flag (early return for self-scan)
|
|
269
293
|
if (self) {
|
|
270
|
-
const
|
|
294
|
+
const runtime = getRuntime(undefined, config);
|
|
295
|
+
const transcriptPath = await discoverOrchestratorTranscript(runtime.id, config.project.root);
|
|
271
296
|
if (!transcriptPath) {
|
|
272
297
|
if (json) {
|
|
273
|
-
jsonError("costs",
|
|
298
|
+
jsonError("costs", `No transcript found for runtime '${runtime.id}'`);
|
|
274
299
|
} else {
|
|
275
300
|
process.stdout.write(
|
|
276
|
-
|
|
301
|
+
`No transcript found for runtime '${runtime.id}'.\n` +
|
|
302
|
+
"Transcript discovery may not be supported for this runtime.\n",
|
|
277
303
|
);
|
|
278
304
|
}
|
|
279
305
|
return;
|
|
@@ -521,6 +547,8 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
|
|
|
521
547
|
sessions = metricsStore.getSessionsByAgent(agentName);
|
|
522
548
|
} else if (runId !== undefined) {
|
|
523
549
|
sessions = metricsStore.getSessionsByRun(runId);
|
|
550
|
+
} else if (beadId !== undefined) {
|
|
551
|
+
sessions = metricsStore.getSessionsByTask(beadId);
|
|
524
552
|
} else {
|
|
525
553
|
sessions = metricsStore.getRecentSessions(last);
|
|
526
554
|
}
|
|
@@ -559,6 +587,7 @@ export function createCostsCommand(): Command {
|
|
|
559
587
|
.option("--self", "Show cost for the current orchestrator session")
|
|
560
588
|
.option("--agent <name>", "Filter by agent name")
|
|
561
589
|
.option("--run <id>", "Filter by run ID")
|
|
590
|
+
.option("--bead <id>", "Show cost breakdown for a specific task/bead")
|
|
562
591
|
.option("--by-capability", "Group results by capability with subtotals")
|
|
563
592
|
.option("--last <n>", "Number of recent sessions (default: 20)")
|
|
564
593
|
.option("--json", "Output as JSON")
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
computeAgentPanelHeight,
|
|
22
22
|
dashboardCommand,
|
|
23
23
|
dimBox,
|
|
24
|
+
EventBuffer,
|
|
24
25
|
filterAgentsByRun,
|
|
25
26
|
horizontalLine,
|
|
26
27
|
openDashboardStores,
|
|
@@ -242,28 +243,28 @@ describe("dimBox", () => {
|
|
|
242
243
|
|
|
243
244
|
describe("computeAgentPanelHeight", () => {
|
|
244
245
|
test("0 agents: clamps to minimum 8", () => {
|
|
245
|
-
// max(8, min(floor(30*0.
|
|
246
|
+
// max(8, min(floor(30*0.35)=10, 0+4)) = max(8, min(10,4)) = max(8,4) = 8
|
|
246
247
|
expect(computeAgentPanelHeight(30, 0)).toBe(8);
|
|
247
248
|
});
|
|
248
249
|
|
|
249
250
|
test("4 agents: still clamps to minimum 8", () => {
|
|
250
|
-
// max(8, min(
|
|
251
|
+
// max(8, min(10, 4+4)) = max(8, 8) = 8
|
|
251
252
|
expect(computeAgentPanelHeight(30, 4)).toBe(8);
|
|
252
253
|
});
|
|
253
254
|
|
|
254
|
-
test("20 agents with height 30: clamps to floor(height*0.
|
|
255
|
-
// max(8, min(
|
|
256
|
-
expect(computeAgentPanelHeight(30, 20)).toBe(
|
|
255
|
+
test("20 agents with height 30: clamps to floor(height*0.35)", () => {
|
|
256
|
+
// max(8, min(floor(30*0.35)=10, 24)) = max(8,10) = 10
|
|
257
|
+
expect(computeAgentPanelHeight(30, 20)).toBe(10);
|
|
257
258
|
});
|
|
258
259
|
|
|
259
260
|
test("10 agents with height 30: grows with agent count", () => {
|
|
260
|
-
// max(8, min(
|
|
261
|
-
expect(computeAgentPanelHeight(30, 10)).toBe(
|
|
261
|
+
// max(8, min(10, 14)) = max(8,10) = 10
|
|
262
|
+
expect(computeAgentPanelHeight(30, 10)).toBe(10);
|
|
262
263
|
});
|
|
263
264
|
|
|
264
|
-
test("small height: respects
|
|
265
|
-
// height=20: max(8, min(
|
|
266
|
-
expect(computeAgentPanelHeight(20, 20)).toBe(
|
|
265
|
+
test("small height: respects 35% cap", () => {
|
|
266
|
+
// height=20: max(8, min(floor(20*0.35)=7, 24)) = max(8,7) = 8
|
|
267
|
+
expect(computeAgentPanelHeight(20, 20)).toBe(8);
|
|
267
268
|
});
|
|
268
269
|
});
|
|
269
270
|
|
|
@@ -302,6 +303,7 @@ function makeDashboardData(
|
|
|
302
303
|
metrics: { totalSessions: 0, avgDuration: 0, byCapability: {} },
|
|
303
304
|
tasks: overrides.tasks ?? [],
|
|
304
305
|
recentEvents: (overrides.recentEvents as never[]) ?? [],
|
|
306
|
+
feedColorMap: new Map(),
|
|
305
307
|
};
|
|
306
308
|
}
|
|
307
309
|
|
|
@@ -366,6 +368,7 @@ describe("renderFeedPanel", () => {
|
|
|
366
368
|
const data = makeDashboardData({ recentEvents: [] });
|
|
367
369
|
const out = renderFeedPanel(data, 1, 80, 8, 1);
|
|
368
370
|
expect(out).toContain("Feed");
|
|
371
|
+
expect(out).toContain("(live)");
|
|
369
372
|
});
|
|
370
373
|
|
|
371
374
|
test("renders event agent name when events are present", () => {
|
|
@@ -554,6 +557,94 @@ describe("closeDashboardStores", () => {
|
|
|
554
557
|
});
|
|
555
558
|
});
|
|
556
559
|
|
|
560
|
+
describe("EventBuffer", () => {
|
|
561
|
+
let tempDir: string;
|
|
562
|
+
|
|
563
|
+
beforeEach(async () => {
|
|
564
|
+
tempDir = await mkdtemp(join(tmpdir(), "event-buffer-test-"));
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
afterEach(async () => {
|
|
568
|
+
await cleanupTempDir(tempDir);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
function makeEvent(agentName: string) {
|
|
572
|
+
return {
|
|
573
|
+
agentName,
|
|
574
|
+
eventType: "tool_end" as const,
|
|
575
|
+
level: "info" as const,
|
|
576
|
+
runId: null,
|
|
577
|
+
sessionId: null,
|
|
578
|
+
toolName: null,
|
|
579
|
+
toolArgs: null,
|
|
580
|
+
toolDurationMs: null,
|
|
581
|
+
data: null,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
test("starts empty", () => {
|
|
586
|
+
const buf = new EventBuffer();
|
|
587
|
+
expect(buf.size).toBe(0);
|
|
588
|
+
expect(buf.getEvents()).toEqual([]);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("poll adds events from event store", async () => {
|
|
592
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
593
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
594
|
+
const store = createEventStore(join(overstoryDir, "events.db"));
|
|
595
|
+
store.insert(makeEvent("agent-a"));
|
|
596
|
+
|
|
597
|
+
const buf = new EventBuffer();
|
|
598
|
+
buf.poll(store);
|
|
599
|
+
expect(buf.size).toBe(1);
|
|
600
|
+
store.close();
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test("deduplicates by lastSeenId (double poll returns same count)", async () => {
|
|
604
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
605
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
606
|
+
const store = createEventStore(join(overstoryDir, "events.db"));
|
|
607
|
+
store.insert(makeEvent("agent-a"));
|
|
608
|
+
|
|
609
|
+
const buf = new EventBuffer();
|
|
610
|
+
buf.poll(store);
|
|
611
|
+
buf.poll(store); // second poll should not duplicate
|
|
612
|
+
expect(buf.size).toBe(1);
|
|
613
|
+
store.close();
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("trims to maxSize keeping most recent events", async () => {
|
|
617
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
618
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
619
|
+
const store = createEventStore(join(overstoryDir, "events.db"));
|
|
620
|
+
for (let i = 0; i < 5; i++) {
|
|
621
|
+
store.insert(makeEvent(`agent-${i}`));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const buf = new EventBuffer(3);
|
|
625
|
+
buf.poll(store);
|
|
626
|
+
expect(buf.size).toBe(3);
|
|
627
|
+
store.close();
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test("builds color map across polls", async () => {
|
|
631
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
632
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
633
|
+
const store = createEventStore(join(overstoryDir, "events.db"));
|
|
634
|
+
store.insert(makeEvent("agent-x"));
|
|
635
|
+
|
|
636
|
+
const buf = new EventBuffer();
|
|
637
|
+
buf.poll(store);
|
|
638
|
+
expect(buf.getColorMap().has("agent-x")).toBe(true);
|
|
639
|
+
|
|
640
|
+
store.insert(makeEvent("agent-y"));
|
|
641
|
+
buf.poll(store);
|
|
642
|
+
expect(buf.getColorMap().has("agent-x")).toBe(true);
|
|
643
|
+
expect(buf.getColorMap().has("agent-y")).toBe(true);
|
|
644
|
+
store.close();
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
557
648
|
// Type check: DashboardStores includes eventStore
|
|
558
649
|
test("DashboardStores type includes eventStore field", () => {
|
|
559
650
|
const stores: DashboardStores = {
|