@os-eco/overstory-cli 0.6.1 → 0.6.5
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 +8 -7
- package/package.json +12 -4
- package/src/agents/checkpoint.test.ts +2 -2
- package/src/agents/hooks-deployer.test.ts +131 -16
- package/src/agents/hooks-deployer.ts +33 -1
- 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/manifest.test.ts +86 -0
- package/src/agents/overlay.test.ts +9 -9
- package/src/agents/overlay.ts +4 -4
- package/src/commands/agents.test.ts +8 -8
- package/src/commands/agents.ts +62 -91
- package/src/commands/clean.test.ts +36 -51
- package/src/commands/clean.ts +28 -49
- package/src/commands/completions.ts +14 -0
- package/src/commands/coordinator.test.ts +133 -26
- package/src/commands/coordinator.ts +101 -64
- package/src/commands/costs.test.ts +47 -47
- package/src/commands/costs.ts +96 -75
- package/src/commands/dashboard.test.ts +2 -2
- package/src/commands/dashboard.ts +75 -95
- package/src/commands/doctor.test.ts +2 -2
- package/src/commands/doctor.ts +92 -79
- package/src/commands/errors.test.ts +2 -2
- package/src/commands/errors.ts +56 -50
- package/src/commands/feed.test.ts +2 -2
- package/src/commands/feed.ts +86 -83
- package/src/commands/group.ts +167 -177
- package/src/commands/hooks.test.ts +2 -2
- package/src/commands/hooks.ts +52 -42
- package/src/commands/init.test.ts +19 -19
- package/src/commands/init.ts +7 -16
- package/src/commands/inspect.test.ts +18 -18
- package/src/commands/inspect.ts +55 -58
- package/src/commands/log.test.ts +26 -31
- package/src/commands/log.ts +97 -91
- package/src/commands/logs.test.ts +1 -1
- package/src/commands/logs.ts +101 -104
- package/src/commands/mail.test.ts +5 -5
- package/src/commands/mail.ts +157 -169
- package/src/commands/merge.test.ts +28 -66
- package/src/commands/merge.ts +21 -51
- package/src/commands/metrics.test.ts +8 -8
- package/src/commands/metrics.ts +34 -35
- package/src/commands/monitor.test.ts +3 -3
- package/src/commands/monitor.ts +57 -62
- package/src/commands/nudge.test.ts +1 -1
- package/src/commands/nudge.ts +41 -89
- package/src/commands/prime.test.ts +19 -51
- package/src/commands/prime.ts +13 -50
- package/src/commands/replay.test.ts +2 -2
- package/src/commands/replay.ts +79 -86
- package/src/commands/run.test.ts +1 -1
- package/src/commands/run.ts +97 -77
- package/src/commands/sling.test.ts +201 -5
- package/src/commands/sling.ts +37 -64
- package/src/commands/spec.test.ts +14 -40
- package/src/commands/spec.ts +32 -101
- package/src/commands/status.test.ts +97 -1
- package/src/commands/status.ts +63 -58
- package/src/commands/stop.test.ts +22 -40
- package/src/commands/stop.ts +18 -33
- package/src/commands/supervisor.test.ts +12 -14
- package/src/commands/supervisor.ts +144 -165
- package/src/commands/trace.test.ts +15 -15
- package/src/commands/trace.ts +59 -82
- package/src/commands/watch.test.ts +2 -2
- package/src/commands/watch.ts +38 -45
- package/src/commands/worktree.test.ts +213 -37
- package/src/commands/worktree.ts +110 -55
- package/src/config.test.ts +96 -0
- package/src/doctor/consistency.test.ts +14 -14
- package/src/doctor/databases.test.ts +22 -2
- package/src/doctor/databases.ts +16 -0
- package/src/doctor/dependencies.test.ts +55 -1
- package/src/doctor/dependencies.ts +113 -18
- package/src/doctor/merge-queue.test.ts +4 -4
- package/src/e2e/init-sling-lifecycle.test.ts +8 -8
- package/src/errors.ts +1 -1
- package/src/index.ts +223 -213
- package/src/logging/color.test.ts +74 -91
- package/src/logging/color.ts +52 -46
- package/src/logging/reporter.test.ts +10 -10
- package/src/logging/reporter.ts +6 -5
- package/src/mail/broadcast.test.ts +1 -1
- package/src/mail/client.test.ts +6 -6
- package/src/mail/store.test.ts +3 -3
- package/src/merge/queue.test.ts +73 -7
- package/src/merge/queue.ts +17 -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/schema-consistency.test.ts +239 -0
- package/src/sessions/compat.test.ts +3 -3
- package/src/sessions/compat.ts +2 -2
- package/src/sessions/store.test.ts +41 -4
- package/src/sessions/store.ts +13 -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 +98 -9
- package/src/worktree/tmux.ts +18 -0
package/src/commands/replay.ts
CHANGED
|
@@ -7,14 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
+
import { Command } from "commander";
|
|
10
11
|
import { loadConfig } from "../config.ts";
|
|
11
12
|
import { ValidationError } from "../errors.ts";
|
|
12
13
|
import { createEventStore } from "../events/store.ts";
|
|
14
|
+
import type { ColorFn } from "../logging/color.ts";
|
|
13
15
|
import { color } from "../logging/color.ts";
|
|
14
16
|
import type { EventType, StoredEvent } from "../types.ts";
|
|
15
17
|
|
|
16
18
|
/** Labels and colors for each event type. */
|
|
17
|
-
const EVENT_LABELS: Record<EventType, { label: string; color:
|
|
19
|
+
const EVENT_LABELS: Record<EventType, { label: string; color: ColorFn }> = {
|
|
18
20
|
tool_start: { label: "TOOL START", color: color.blue },
|
|
19
21
|
tool_end: { label: "TOOL END ", color: color.blue },
|
|
20
22
|
session_start: { label: "SESSION +", color: color.green },
|
|
@@ -26,41 +28,14 @@ const EVENT_LABELS: Record<EventType, { label: string; color: string }> = {
|
|
|
26
28
|
custom: { label: "CUSTOM ", color: color.gray },
|
|
27
29
|
};
|
|
28
30
|
|
|
29
|
-
/**
|
|
30
|
-
const AGENT_COLORS
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (idx === -1 || idx + 1 >= args.length) {
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
return args[idx + 1];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Parse all occurrences of a named flag from args.
|
|
45
|
-
* Returns an array of values (e.g., --agent a --agent b => ["a", "b"]).
|
|
46
|
-
*/
|
|
47
|
-
function getAllFlags(args: string[], flag: string): string[] {
|
|
48
|
-
const values: string[] = [];
|
|
49
|
-
for (let i = 0; i < args.length; i++) {
|
|
50
|
-
if (args[i] === flag && i + 1 < args.length) {
|
|
51
|
-
const value = args[i + 1];
|
|
52
|
-
if (value !== undefined) {
|
|
53
|
-
values.push(value);
|
|
54
|
-
}
|
|
55
|
-
i++; // skip the value
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return values;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function hasFlag(args: string[], flag: string): boolean {
|
|
62
|
-
return args.includes(flag);
|
|
63
|
-
}
|
|
31
|
+
/** Color functions assigned to agents in order of first appearance. */
|
|
32
|
+
const AGENT_COLORS: readonly ColorFn[] = [
|
|
33
|
+
color.blue,
|
|
34
|
+
color.green,
|
|
35
|
+
color.yellow,
|
|
36
|
+
color.cyan,
|
|
37
|
+
color.magenta,
|
|
38
|
+
];
|
|
64
39
|
|
|
65
40
|
/**
|
|
66
41
|
* Format a relative time string from a timestamp.
|
|
@@ -150,16 +125,16 @@ function buildEventDetail(event: StoredEvent): string {
|
|
|
150
125
|
}
|
|
151
126
|
|
|
152
127
|
/**
|
|
153
|
-
* Assign a stable color to each agent based on order of first appearance.
|
|
128
|
+
* Assign a stable color function to each agent based on order of first appearance.
|
|
154
129
|
*/
|
|
155
|
-
function buildAgentColorMap(events: StoredEvent[]): Map<string,
|
|
156
|
-
const colorMap = new Map<string,
|
|
130
|
+
function buildAgentColorMap(events: StoredEvent[]): Map<string, ColorFn> {
|
|
131
|
+
const colorMap = new Map<string, ColorFn>();
|
|
157
132
|
for (const event of events) {
|
|
158
133
|
if (!colorMap.has(event.agentName)) {
|
|
159
134
|
const colorIndex = colorMap.size % AGENT_COLORS.length;
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
colorMap.set(event.agentName,
|
|
135
|
+
const agentColorFn = AGENT_COLORS[colorIndex];
|
|
136
|
+
if (agentColorFn !== undefined) {
|
|
137
|
+
colorMap.set(event.agentName, agentColorFn);
|
|
163
138
|
}
|
|
164
139
|
}
|
|
165
140
|
}
|
|
@@ -172,15 +147,15 @@ function buildAgentColorMap(events: StoredEvent[]): Map<string, string> {
|
|
|
172
147
|
function printReplay(events: StoredEvent[], useAbsoluteTime: boolean): void {
|
|
173
148
|
const w = process.stdout.write.bind(process.stdout);
|
|
174
149
|
|
|
175
|
-
w(`${color.bold
|
|
150
|
+
w(`${color.bold("Replay")}\n`);
|
|
176
151
|
w(`${"=".repeat(70)}\n`);
|
|
177
152
|
|
|
178
153
|
if (events.length === 0) {
|
|
179
|
-
w(`${color.dim
|
|
154
|
+
w(`${color.dim("No events found.")}\n`);
|
|
180
155
|
return;
|
|
181
156
|
}
|
|
182
157
|
|
|
183
|
-
w(`${color.dim
|
|
158
|
+
w(`${color.dim(`${events.length} event${events.length === 1 ? "" : "s"}`)}\n\n`);
|
|
184
159
|
|
|
185
160
|
const colorMap = buildAgentColorMap(events);
|
|
186
161
|
let lastDate = "";
|
|
@@ -192,7 +167,7 @@ function printReplay(events: StoredEvent[], useAbsoluteTime: boolean): void {
|
|
|
192
167
|
if (lastDate !== "") {
|
|
193
168
|
w("\n");
|
|
194
169
|
}
|
|
195
|
-
w(`${color.dim
|
|
170
|
+
w(`${color.dim(`--- ${date} ---`)}\n`);
|
|
196
171
|
lastDate = date;
|
|
197
172
|
}
|
|
198
173
|
|
|
@@ -205,57 +180,40 @@ function printReplay(events: StoredEvent[], useAbsoluteTime: boolean): void {
|
|
|
205
180
|
color: color.gray,
|
|
206
181
|
};
|
|
207
182
|
|
|
208
|
-
const
|
|
209
|
-
event.level === "error" ? color.red : event.level === "warn" ? color.yellow :
|
|
210
|
-
const
|
|
183
|
+
const levelColorFn =
|
|
184
|
+
event.level === "error" ? color.red : event.level === "warn" ? color.yellow : null;
|
|
185
|
+
const applyLevel = (text: string) => (levelColorFn ? levelColorFn(text) : text);
|
|
211
186
|
|
|
212
187
|
const detail = buildEventDetail(event);
|
|
213
|
-
const detailSuffix = detail ? ` ${color.dim
|
|
188
|
+
const detailSuffix = detail ? ` ${color.dim(detail)}` : "";
|
|
214
189
|
|
|
215
|
-
const
|
|
216
|
-
const agentLabel = ` ${
|
|
190
|
+
const agentColorFn = colorMap.get(event.agentName) ?? color.gray;
|
|
191
|
+
const agentLabel = ` ${agentColorFn(`[${event.agentName}]`)}`;
|
|
217
192
|
|
|
218
193
|
w(
|
|
219
|
-
`${color.dim
|
|
220
|
-
`${
|
|
194
|
+
`${color.dim(timeStr.padStart(10))} ` +
|
|
195
|
+
`${applyLevel(eventInfo.color(color.bold(eventInfo.label)))}` +
|
|
221
196
|
`${agentLabel}${detailSuffix}\n`,
|
|
222
197
|
);
|
|
223
198
|
}
|
|
224
199
|
}
|
|
225
200
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
--until <timestamp> End time filter (ISO 8601)
|
|
235
|
-
--limit <n> Max events to show (default: 200)
|
|
236
|
-
--json Output as JSON array of StoredEvent objects
|
|
237
|
-
--help, -h Show this help
|
|
238
|
-
|
|
239
|
-
If --run is specified, shows all events from that run.
|
|
240
|
-
If --agent is specified, shows events from those agents merged chronologically.
|
|
241
|
-
If neither is specified, tries to read the current run from .overstory/current-run.txt.
|
|
242
|
-
Falls back to a 24-hour timeline of all events.`;
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Entry point for `overstory replay [--run <id>] [--agent <name>...] [--json]`.
|
|
246
|
-
*/
|
|
247
|
-
export async function replayCommand(args: string[]): Promise<void> {
|
|
248
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
249
|
-
process.stdout.write(`${REPLAY_HELP}\n`);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
201
|
+
interface ReplayOpts {
|
|
202
|
+
run?: string;
|
|
203
|
+
agent: string[]; // repeatable
|
|
204
|
+
since?: string;
|
|
205
|
+
until?: string;
|
|
206
|
+
limit?: string;
|
|
207
|
+
json?: boolean;
|
|
208
|
+
}
|
|
252
209
|
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
const
|
|
210
|
+
async function executeReplay(opts: ReplayOpts): Promise<void> {
|
|
211
|
+
const json = opts.json ?? false;
|
|
212
|
+
const runId = opts.run;
|
|
213
|
+
const agentNames = opts.agent;
|
|
214
|
+
const sinceStr = opts.since;
|
|
215
|
+
const untilStr = opts.until;
|
|
216
|
+
const limitStr = opts.limit;
|
|
259
217
|
const limit = limitStr ? Number.parseInt(limitStr, 10) : 200;
|
|
260
218
|
|
|
261
219
|
if (Number.isNaN(limit) || limit < 1) {
|
|
@@ -358,3 +316,38 @@ export async function replayCommand(args: string[]): Promise<void> {
|
|
|
358
316
|
eventStore.close();
|
|
359
317
|
}
|
|
360
318
|
}
|
|
319
|
+
|
|
320
|
+
export function createReplayCommand(): Command {
|
|
321
|
+
return new Command("replay")
|
|
322
|
+
.description("Interleaved chronological replay across agents")
|
|
323
|
+
.option("--run <id>", "Filter events by run ID")
|
|
324
|
+
.option(
|
|
325
|
+
"--agent <name>",
|
|
326
|
+
"Filter by agent name (can appear multiple times)",
|
|
327
|
+
(val: string, prev: string[]) => [...prev, val],
|
|
328
|
+
[] as string[],
|
|
329
|
+
)
|
|
330
|
+
.option("--since <timestamp>", "Start time filter (ISO 8601)")
|
|
331
|
+
.option("--until <timestamp>", "End time filter (ISO 8601)")
|
|
332
|
+
.option("--limit <n>", "Max events to show (default: 200)")
|
|
333
|
+
.option("--json", "Output as JSON array of StoredEvent objects")
|
|
334
|
+
.action(async (opts: ReplayOpts) => {
|
|
335
|
+
await executeReplay(opts);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export async function replayCommand(args: string[]): Promise<void> {
|
|
340
|
+
const cmd = createReplayCommand();
|
|
341
|
+
cmd.exitOverride();
|
|
342
|
+
try {
|
|
343
|
+
await cmd.parseAsync(args, { from: "user" });
|
|
344
|
+
} catch (err: unknown) {
|
|
345
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
346
|
+
const code = (err as { code: string }).code;
|
|
347
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
throw err;
|
|
352
|
+
}
|
|
353
|
+
}
|
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
|
@@ -12,39 +12,12 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
+
import { Command, CommanderError } from "commander";
|
|
15
16
|
import { loadConfig } from "../config.ts";
|
|
17
|
+
import { ValidationError } from "../errors.ts";
|
|
16
18
|
import { createRunStore, createSessionStore } from "../sessions/store.ts";
|
|
17
19
|
import type { AgentSession, Run } from "../types.ts";
|
|
18
20
|
|
|
19
|
-
const RUN_HELP = `overstory run -- Manage runs (coordinator session groupings)
|
|
20
|
-
|
|
21
|
-
Usage: overstory run [subcommand] [options]
|
|
22
|
-
|
|
23
|
-
Subcommands:
|
|
24
|
-
(default) Show current run status
|
|
25
|
-
list [--last <n>] List recent runs (default last 10)
|
|
26
|
-
complete Mark current run as completed
|
|
27
|
-
show <id> Show run details (agents, duration)
|
|
28
|
-
|
|
29
|
-
Options:
|
|
30
|
-
--json Output as JSON
|
|
31
|
-
--help, -h Show this help`;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Parse a named flag value from args.
|
|
35
|
-
*/
|
|
36
|
-
function getFlag(args: string[], flag: string): string | undefined {
|
|
37
|
-
const idx = args.indexOf(flag);
|
|
38
|
-
if (idx === -1 || idx + 1 >= args.length) {
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
return args[idx + 1];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function hasFlag(args: string[], flag: string): boolean {
|
|
45
|
-
return args.includes(flag);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
21
|
/**
|
|
49
22
|
* Format milliseconds as human-readable duration.
|
|
50
23
|
*/
|
|
@@ -90,6 +63,18 @@ function runDuration(run: Run): string {
|
|
|
90
63
|
return formatDuration(end - start);
|
|
91
64
|
}
|
|
92
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Format an agent's duration from startedAt to now (or completion).
|
|
68
|
+
*/
|
|
69
|
+
function formatAgentDuration(agent: AgentSession): string {
|
|
70
|
+
const start = new Date(agent.startedAt).getTime();
|
|
71
|
+
const end =
|
|
72
|
+
agent.state === "completed" || agent.state === "zombie"
|
|
73
|
+
? new Date(agent.lastActivity).getTime()
|
|
74
|
+
: Date.now();
|
|
75
|
+
return formatDuration(end - start);
|
|
76
|
+
}
|
|
77
|
+
|
|
93
78
|
/**
|
|
94
79
|
* Show current run status (default subcommand).
|
|
95
80
|
*/
|
|
@@ -291,61 +276,96 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
|
|
|
291
276
|
}
|
|
292
277
|
}
|
|
293
278
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
*/
|
|
297
|
-
function formatAgentDuration(agent: AgentSession): string {
|
|
298
|
-
const start = new Date(agent.startedAt).getTime();
|
|
299
|
-
const end =
|
|
300
|
-
agent.state === "completed" || agent.state === "zombie"
|
|
301
|
-
? new Date(agent.lastActivity).getTime()
|
|
302
|
-
: Date.now();
|
|
303
|
-
return formatDuration(end - start);
|
|
279
|
+
interface RunDefaultOpts {
|
|
280
|
+
json?: boolean;
|
|
304
281
|
}
|
|
305
282
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
311
|
-
process.stdout.write(`${RUN_HELP}\n`);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
283
|
+
interface RunListOpts {
|
|
284
|
+
last?: string;
|
|
285
|
+
json?: boolean;
|
|
286
|
+
}
|
|
314
287
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const overstoryDir = join(config.project.root, ".overstory");
|
|
288
|
+
interface RunShowOpts {
|
|
289
|
+
json?: boolean;
|
|
290
|
+
}
|
|
319
291
|
|
|
320
|
-
|
|
292
|
+
interface RunCompleteOpts {
|
|
293
|
+
json?: boolean;
|
|
294
|
+
}
|
|
321
295
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
296
|
+
export function createRunCommand(): Command {
|
|
297
|
+
const cmd = new Command("run").description("Manage runs (coordinator session groupings)");
|
|
298
|
+
|
|
299
|
+
// Default action (bare `overstory run`)
|
|
300
|
+
cmd.option("--json", "Output as JSON").action(async (opts: RunDefaultOpts) => {
|
|
301
|
+
const cwd = process.cwd();
|
|
302
|
+
const config = await loadConfig(cwd);
|
|
303
|
+
const overstoryDir = join(config.project.root, ".overstory");
|
|
304
|
+
await showCurrentRun(overstoryDir, opts.json ?? false);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// `overstory run list`
|
|
308
|
+
cmd
|
|
309
|
+
.command("list")
|
|
310
|
+
.description("List recent runs")
|
|
311
|
+
.option("--last <n>", "Number of recent runs to show (default: 10)")
|
|
312
|
+
.option("--json", "Output as JSON")
|
|
313
|
+
.action(async (opts: RunListOpts) => {
|
|
314
|
+
const lastStr = opts.last;
|
|
325
315
|
const limit = lastStr ? Number.parseInt(lastStr, 10) : 10;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
break;
|
|
332
|
-
case "show": {
|
|
333
|
-
const runId = args[1];
|
|
334
|
-
if (!runId || runId.startsWith("--")) {
|
|
335
|
-
if (json) {
|
|
336
|
-
process.stdout.write('{"error":"Missing run ID. Usage: overstory run show <id>"}\n');
|
|
337
|
-
} else {
|
|
338
|
-
process.stderr.write("Missing run ID. Usage: overstory run show <id>\n");
|
|
339
|
-
}
|
|
340
|
-
process.exitCode = 1;
|
|
341
|
-
return;
|
|
316
|
+
if (Number.isNaN(limit) || limit < 1) {
|
|
317
|
+
throw new ValidationError("--last must be a positive integer", {
|
|
318
|
+
field: "last",
|
|
319
|
+
value: lastStr,
|
|
320
|
+
});
|
|
342
321
|
}
|
|
343
|
-
|
|
344
|
-
|
|
322
|
+
const cwd = process.cwd();
|
|
323
|
+
const config = await loadConfig(cwd);
|
|
324
|
+
const overstoryDir = join(config.project.root, ".overstory");
|
|
325
|
+
await listRuns(overstoryDir, limit, opts.json ?? false);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// `overstory run show <id>`
|
|
329
|
+
cmd
|
|
330
|
+
.command("show")
|
|
331
|
+
.description("Show run details (agents, duration)")
|
|
332
|
+
.argument("<id>", "Run ID")
|
|
333
|
+
.option("--json", "Output as JSON")
|
|
334
|
+
.action(async (id: string, opts: RunShowOpts) => {
|
|
335
|
+
const cwd = process.cwd();
|
|
336
|
+
const config = await loadConfig(cwd);
|
|
337
|
+
const overstoryDir = join(config.project.root, ".overstory");
|
|
338
|
+
await showRun(overstoryDir, id, opts.json ?? false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// `overstory run complete`
|
|
342
|
+
cmd
|
|
343
|
+
.command("complete")
|
|
344
|
+
.description("Mark current run as completed")
|
|
345
|
+
.option("--json", "Output as JSON")
|
|
346
|
+
.action(async (opts: RunCompleteOpts) => {
|
|
347
|
+
const cwd = process.cwd();
|
|
348
|
+
const config = await loadConfig(cwd);
|
|
349
|
+
const overstoryDir = join(config.project.root, ".overstory");
|
|
350
|
+
await completeCurrentRun(overstoryDir, opts.json ?? false);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
return cmd;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export async function runCommand(args: string[]): Promise<void> {
|
|
357
|
+
const program = new Command("overstory").exitOverride().configureOutput({
|
|
358
|
+
writeOut: (str) => process.stdout.write(str),
|
|
359
|
+
writeErr: (str) => process.stderr.write(str),
|
|
360
|
+
});
|
|
361
|
+
program.addCommand(createRunCommand());
|
|
362
|
+
try {
|
|
363
|
+
await program.parseAsync(["node", "overstory", "run", ...args]);
|
|
364
|
+
} catch (err: unknown) {
|
|
365
|
+
if (err instanceof CommanderError) {
|
|
366
|
+
if (err.code === "commander.helpDisplayed" || err.code === "commander.version") return;
|
|
367
|
+
throw new ValidationError(err.message, { field: "args" });
|
|
345
368
|
}
|
|
346
|
-
|
|
347
|
-
// Default: show current run status
|
|
348
|
-
await showCurrentRun(overstoryDir, json);
|
|
349
|
-
break;
|
|
369
|
+
throw err;
|
|
350
370
|
}
|
|
351
371
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolveModel, resolveProviderEnv } from "../agents/manifest.ts";
|
|
2
3
|
import { HierarchyError } from "../errors.ts";
|
|
4
|
+
import type { AgentManifest, OverstoryConfig } from "../types.ts";
|
|
3
5
|
import {
|
|
4
6
|
type BeaconOptions,
|
|
5
7
|
buildBeacon,
|
|
@@ -552,13 +554,13 @@ describe("isRunningAsRoot", () => {
|
|
|
552
554
|
/**
|
|
553
555
|
* Tests for checkBeadLock.
|
|
554
556
|
*
|
|
555
|
-
* checkBeadLock prevents concurrent agents from working the same
|
|
557
|
+
* checkBeadLock prevents concurrent agents from working the same task ID.
|
|
556
558
|
* It checks the active session list and returns the agent name that holds
|
|
557
559
|
* the lock (i.e., is already working on the bead), or null if the bead is free.
|
|
558
560
|
*/
|
|
559
561
|
|
|
560
|
-
function makeBeadSession(agentName: string,
|
|
561
|
-
return { agentName,
|
|
562
|
+
function makeBeadSession(agentName: string, taskId: string): { agentName: string; taskId: string } {
|
|
563
|
+
return { agentName, taskId };
|
|
562
564
|
}
|
|
563
565
|
|
|
564
566
|
describe("checkBeadLock", () => {
|
|
@@ -566,7 +568,7 @@ describe("checkBeadLock", () => {
|
|
|
566
568
|
expect(checkBeadLock([], "overstory-abc")).toBeNull();
|
|
567
569
|
});
|
|
568
570
|
|
|
569
|
-
test("returns null when no session matches the
|
|
571
|
+
test("returns null when no session matches the task ID", () => {
|
|
570
572
|
const sessions = [
|
|
571
573
|
makeBeadSession("builder-1", "overstory-xyz"),
|
|
572
574
|
makeBeadSession("builder-2", "overstory-def"),
|
|
@@ -585,7 +587,7 @@ describe("checkBeadLock", () => {
|
|
|
585
587
|
});
|
|
586
588
|
|
|
587
589
|
test("returns the first matching agent when multiple sessions match", () => {
|
|
588
|
-
// Multiple sessions can have the same
|
|
590
|
+
// Multiple sessions can have the same taskId (e.g., retried agent)
|
|
589
591
|
// checkBeadLock returns the first match
|
|
590
592
|
const sessions = [
|
|
591
593
|
makeBeadSession("builder-1", "overstory-abc"),
|
|
@@ -655,3 +657,197 @@ describe("checkRunSessionLimit", () => {
|
|
|
655
657
|
expect(checkRunSessionLimit(-1, 100)).toBe(false);
|
|
656
658
|
});
|
|
657
659
|
});
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Tests for sling provider env injection building blocks.
|
|
663
|
+
*
|
|
664
|
+
* In slingCommand, resolveModel() is called to get the { model, env } for the
|
|
665
|
+
* spawned agent. The env dict is then spread into createSession's env parameter
|
|
666
|
+
* alongside OVERSTORY_AGENT_NAME and OVERSTORY_WORKTREE_PATH:
|
|
667
|
+
*
|
|
668
|
+
* const { model, env } = resolveModel(config, manifest, capability, agentDef.model);
|
|
669
|
+
* const pid = await createSession(tmuxSessionName, worktreePath, claudeCmd, {
|
|
670
|
+
* ...env,
|
|
671
|
+
* OVERSTORY_AGENT_NAME: name,
|
|
672
|
+
* OVERSTORY_WORKTREE_PATH: worktreePath,
|
|
673
|
+
* });
|
|
674
|
+
*
|
|
675
|
+
* These tests verify the building blocks: that resolveModel and resolveProviderEnv
|
|
676
|
+
* produce the correct env dicts for the provider scenarios sling will encounter.
|
|
677
|
+
*/
|
|
678
|
+
|
|
679
|
+
function makeConfig(
|
|
680
|
+
models: OverstoryConfig["models"] = {},
|
|
681
|
+
providers: OverstoryConfig["providers"] = { anthropic: { type: "native" } },
|
|
682
|
+
): OverstoryConfig {
|
|
683
|
+
return {
|
|
684
|
+
project: { name: "test", root: "/tmp/test", canonicalBranch: "main" },
|
|
685
|
+
agents: {
|
|
686
|
+
manifestPath: ".overstory/agent-manifest.json",
|
|
687
|
+
baseDir: ".overstory/agent-defs",
|
|
688
|
+
maxConcurrent: 5,
|
|
689
|
+
staggerDelayMs: 0,
|
|
690
|
+
maxDepth: 2,
|
|
691
|
+
maxSessionsPerRun: 0,
|
|
692
|
+
},
|
|
693
|
+
worktrees: { baseDir: ".overstory/worktrees" },
|
|
694
|
+
taskTracker: { backend: "auto", enabled: false },
|
|
695
|
+
mulch: { enabled: false, domains: [], primeFormat: "markdown" },
|
|
696
|
+
merge: { aiResolveEnabled: false, reimagineEnabled: false },
|
|
697
|
+
providers,
|
|
698
|
+
watchdog: {
|
|
699
|
+
tier0Enabled: false,
|
|
700
|
+
tier0IntervalMs: 30_000,
|
|
701
|
+
tier1Enabled: false,
|
|
702
|
+
tier2Enabled: false,
|
|
703
|
+
staleThresholdMs: 300_000,
|
|
704
|
+
zombieThresholdMs: 600_000,
|
|
705
|
+
nudgeIntervalMs: 60_000,
|
|
706
|
+
},
|
|
707
|
+
models,
|
|
708
|
+
logging: { verbose: false, redactSecrets: true },
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function makeManifest(): AgentManifest {
|
|
713
|
+
return {
|
|
714
|
+
version: "1.0",
|
|
715
|
+
agents: {
|
|
716
|
+
builder: {
|
|
717
|
+
file: "builder.md",
|
|
718
|
+
model: "opus",
|
|
719
|
+
tools: ["Read", "Write", "Edit", "Bash"],
|
|
720
|
+
capabilities: ["implement"],
|
|
721
|
+
canSpawn: false,
|
|
722
|
+
constraints: [],
|
|
723
|
+
},
|
|
724
|
+
coordinator: {
|
|
725
|
+
file: "coordinator.md",
|
|
726
|
+
model: "sonnet",
|
|
727
|
+
tools: ["Read", "Bash"],
|
|
728
|
+
capabilities: ["coordinate"],
|
|
729
|
+
canSpawn: true,
|
|
730
|
+
constraints: [],
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
capabilityIndex: { implement: ["builder"], coordinate: ["coordinator"] },
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
describe("sling provider env injection building blocks", () => {
|
|
738
|
+
test("resolveModel produces env for gateway provider in config override scenario", () => {
|
|
739
|
+
const config = makeConfig(
|
|
740
|
+
{ builder: "openrouter/anthropic/claude-3-5-sonnet" },
|
|
741
|
+
{ openrouter: { type: "gateway", baseUrl: "https://openrouter.ai/api/v1" } },
|
|
742
|
+
);
|
|
743
|
+
const manifest = makeManifest();
|
|
744
|
+
|
|
745
|
+
const result = resolveModel(config, manifest, "builder", "sonnet");
|
|
746
|
+
|
|
747
|
+
expect(result.model).toBe("sonnet");
|
|
748
|
+
expect(result.env).toBeDefined();
|
|
749
|
+
expect(result.env?.ANTHROPIC_BASE_URL).toBe("https://openrouter.ai/api/v1");
|
|
750
|
+
expect(result.env?.ANTHROPIC_API_KEY).toBe("");
|
|
751
|
+
expect(result.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe("anthropic/claude-3-5-sonnet");
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test("env dict from resolveModel can be spread with OVERSTORY_AGENT_NAME and OVERSTORY_WORKTREE_PATH", () => {
|
|
755
|
+
const config = makeConfig(
|
|
756
|
+
{ builder: "openrouter/anthropic/claude-3-5-sonnet" },
|
|
757
|
+
{ openrouter: { type: "gateway", baseUrl: "https://openrouter.ai/api/v1" } },
|
|
758
|
+
);
|
|
759
|
+
const manifest = makeManifest();
|
|
760
|
+
|
|
761
|
+
const { env } = resolveModel(config, manifest, "builder", "sonnet");
|
|
762
|
+
// Simulates the spread in slingCommand: { ...env, OVERSTORY_AGENT_NAME: name, OVERSTORY_WORKTREE_PATH: wt }
|
|
763
|
+
const combined: Record<string, string> = {
|
|
764
|
+
...(env ?? {}),
|
|
765
|
+
OVERSTORY_AGENT_NAME: "test-builder",
|
|
766
|
+
OVERSTORY_WORKTREE_PATH: "/tmp/wt",
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
expect(combined.ANTHROPIC_BASE_URL).toBe("https://openrouter.ai/api/v1");
|
|
770
|
+
expect(combined.ANTHROPIC_API_KEY).toBe("");
|
|
771
|
+
expect(combined.OVERSTORY_AGENT_NAME).toBe("test-builder");
|
|
772
|
+
expect(combined.OVERSTORY_WORKTREE_PATH).toBe("/tmp/wt");
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test("resolveModel returns no env for native anthropic provider", () => {
|
|
776
|
+
const config = makeConfig({ builder: "sonnet" }, { anthropic: { type: "native" } });
|
|
777
|
+
const manifest = makeManifest();
|
|
778
|
+
|
|
779
|
+
const result = resolveModel(config, manifest, "builder", "sonnet");
|
|
780
|
+
|
|
781
|
+
expect(result.model).toBe("sonnet");
|
|
782
|
+
expect(result.env).toBeUndefined();
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
test("resolveModel returns no env when model is a simple alias from manifest default", () => {
|
|
786
|
+
// No models override: manifest builder model "opus" is a simple alias
|
|
787
|
+
const config = makeConfig({}, {});
|
|
788
|
+
const manifest = makeManifest();
|
|
789
|
+
|
|
790
|
+
const result = resolveModel(config, manifest, "builder", "sonnet");
|
|
791
|
+
|
|
792
|
+
expect(result.model).toBe("opus");
|
|
793
|
+
expect(result.env).toBeUndefined();
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
test("resolveProviderEnv includes ANTHROPIC_AUTH_TOKEN when authTokenEnv var is set", () => {
|
|
797
|
+
const providers = {
|
|
798
|
+
openrouter: {
|
|
799
|
+
type: "gateway" as const,
|
|
800
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
801
|
+
authTokenEnv: "MY_API_KEY",
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
const env = { MY_API_KEY: "sk-test-123" };
|
|
805
|
+
|
|
806
|
+
const result = resolveProviderEnv("openrouter", "anthropic/claude-3-5-sonnet", providers, env);
|
|
807
|
+
|
|
808
|
+
expect(result).not.toBeNull();
|
|
809
|
+
expect(result?.ANTHROPIC_AUTH_TOKEN).toBe("sk-test-123");
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
test("resolveProviderEnv omits ANTHROPIC_AUTH_TOKEN when authTokenEnv var is absent", () => {
|
|
813
|
+
const providers = {
|
|
814
|
+
openrouter: {
|
|
815
|
+
type: "gateway" as const,
|
|
816
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
817
|
+
authTokenEnv: "MY_API_KEY",
|
|
818
|
+
},
|
|
819
|
+
};
|
|
820
|
+
const env: Record<string, string | undefined> = {};
|
|
821
|
+
|
|
822
|
+
const result = resolveProviderEnv("openrouter", "anthropic/claude-3-5-sonnet", providers, env);
|
|
823
|
+
|
|
824
|
+
expect(result).not.toBeNull();
|
|
825
|
+
expect(result?.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
test("resolveModel produces different env dicts for coordinator and builder with different gateway providers", () => {
|
|
829
|
+
const config = makeConfig(
|
|
830
|
+
{
|
|
831
|
+
coordinator: "openrouter/anthropic/claude-3-5-sonnet",
|
|
832
|
+
builder: "litellm/anthropic/claude-3-5-haiku",
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
openrouter: { type: "gateway", baseUrl: "https://openrouter.ai/api/v1" },
|
|
836
|
+
litellm: { type: "gateway", baseUrl: "https://litellm.example.com/v1" },
|
|
837
|
+
},
|
|
838
|
+
);
|
|
839
|
+
const manifest = makeManifest();
|
|
840
|
+
|
|
841
|
+
const coordinatorResult = resolveModel(config, manifest, "coordinator", "sonnet");
|
|
842
|
+
const builderResult = resolveModel(config, manifest, "builder", "sonnet");
|
|
843
|
+
|
|
844
|
+
expect(coordinatorResult.model).toBe("sonnet");
|
|
845
|
+
expect(builderResult.model).toBe("sonnet");
|
|
846
|
+
expect(coordinatorResult.env?.ANTHROPIC_BASE_URL).toBe("https://openrouter.ai/api/v1");
|
|
847
|
+
expect(builderResult.env?.ANTHROPIC_BASE_URL).toBe("https://litellm.example.com/v1");
|
|
848
|
+
expect(coordinatorResult.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe(
|
|
849
|
+
"anthropic/claude-3-5-sonnet",
|
|
850
|
+
);
|
|
851
|
+
expect(builderResult.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe("anthropic/claude-3-5-haiku");
|
|
852
|
+
});
|
|
853
|
+
});
|