@os-eco/overstory-cli 0.6.1 → 0.6.4
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 +7 -6
- package/package.json +12 -4
- package/src/agents/hooks-deployer.test.ts +94 -16
- package/src/agents/hooks-deployer.ts +18 -0
- package/src/agents/manifest.test.ts +86 -0
- package/src/commands/agents.test.ts +3 -3
- package/src/commands/agents.ts +59 -88
- package/src/commands/clean.test.ts +31 -46
- package/src/commands/clean.ts +28 -49
- package/src/commands/completions.ts +14 -0
- package/src/commands/coordinator.test.ts +131 -24
- package/src/commands/coordinator.ts +100 -63
- package/src/commands/costs.test.ts +2 -2
- package/src/commands/costs.ts +96 -75
- package/src/commands/dashboard.test.ts +2 -2
- package/src/commands/dashboard.ts +73 -93
- 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 +2 -2
- package/src/commands/inspect.ts +54 -57
- package/src/commands/log.test.ts +5 -10
- package/src/commands/log.ts +90 -84
- package/src/commands/logs.test.ts +1 -1
- package/src/commands/logs.ts +101 -104
- package/src/commands/mail.ts +157 -169
- package/src/commands/merge.test.ts +20 -58
- package/src/commands/merge.ts +13 -43
- package/src/commands/metrics.test.ts +2 -2
- package/src/commands/metrics.ts +33 -34
- package/src/commands/monitor.test.ts +3 -3
- package/src/commands/monitor.ts +56 -61
- package/src/commands/nudge.ts +41 -89
- package/src/commands/prime.test.ts +15 -47
- package/src/commands/prime.ts +7 -44
- package/src/commands/replay.test.ts +2 -2
- package/src/commands/replay.ts +79 -86
- package/src/commands/run.ts +97 -77
- package/src/commands/sling.test.ts +196 -0
- package/src/commands/sling.ts +24 -54
- package/src/commands/spec.test.ts +13 -39
- package/src/commands/spec.ts +30 -99
- package/src/commands/status.ts +46 -42
- package/src/commands/stop.test.ts +21 -39
- package/src/commands/stop.ts +18 -33
- package/src/commands/supervisor.test.ts +3 -5
- package/src/commands/supervisor.ts +136 -157
- package/src/commands/trace.test.ts +9 -9
- package/src/commands/trace.ts +54 -77
- package/src/commands/watch.test.ts +2 -2
- package/src/commands/watch.ts +38 -45
- package/src/commands/worktree.test.ts +8 -8
- package/src/commands/worktree.ts +63 -46
- package/src/config.test.ts +96 -0
- 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/e2e/init-sling-lifecycle.test.ts +6 -6
- 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/merge/queue.test.ts +66 -0
- package/src/merge/queue.ts +15 -0
- package/src/schema-consistency.test.ts +239 -0
- package/src/sessions/compat.ts +1 -1
- package/src/sessions/store.test.ts +37 -0
- package/src/sessions/store.ts +11 -0
- package/src/worktree/tmux.test.ts +98 -9
- package/src/worktree/tmux.ts +18 -0
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,
|
|
@@ -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
|
+
});
|
package/src/commands/sling.ts
CHANGED
|
@@ -98,15 +98,17 @@ export function inferDomainsFromFiles(
|
|
|
98
98
|
return [...inferred].sort();
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
export interface SlingOptions {
|
|
102
|
+
capability?: string;
|
|
103
|
+
name?: string;
|
|
104
|
+
spec?: string;
|
|
105
|
+
files?: string;
|
|
106
|
+
parent?: string;
|
|
107
|
+
depth?: string;
|
|
108
|
+
skipScout?: boolean;
|
|
109
|
+
skipTaskCheck?: boolean;
|
|
110
|
+
forceHierarchy?: boolean;
|
|
111
|
+
json?: boolean;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
/**
|
|
@@ -225,58 +227,26 @@ export function validateHierarchy(
|
|
|
225
227
|
/**
|
|
226
228
|
* Entry point for `overstory sling <task-id> [flags]`.
|
|
227
229
|
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* --name <name> Unique agent name
|
|
231
|
-
* --spec <path> Path to task spec file
|
|
232
|
-
* --files <f1,f2,...> Exclusive file scope
|
|
233
|
-
* --parent <agent-name> Parent agent (for hierarchy tracking)
|
|
234
|
-
* --depth <n> Current hierarchy depth (default 0)
|
|
235
|
-
* --force-hierarchy Bypass hierarchy validation (debugging only)
|
|
230
|
+
* @param taskId - The task ID to assign to the agent
|
|
231
|
+
* @param opts - Command options
|
|
236
232
|
*/
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
Usage: overstory sling <task-id> [flags]
|
|
240
|
-
|
|
241
|
-
Arguments:
|
|
242
|
-
<task-id> Beads task ID to assign
|
|
243
|
-
|
|
244
|
-
Options:
|
|
245
|
-
--capability <type> Agent type: builder | scout | reviewer | lead | merger (default: builder)
|
|
246
|
-
--name <name> Unique agent name (required)
|
|
247
|
-
--spec <path> Path to task spec file
|
|
248
|
-
--files <f1,f2,...> Exclusive file scope (comma-separated)
|
|
249
|
-
--parent <agent-name> Parent agent for hierarchy tracking
|
|
250
|
-
--depth <n> Current hierarchy depth (default: 0)
|
|
251
|
-
--skip-scout Skip scout phase for lead agents (jump to build)
|
|
252
|
-
--skip-task-check Skip task existence validation (for worktree-created issues)
|
|
253
|
-
--force-hierarchy Bypass hierarchy validation (debugging only)
|
|
254
|
-
--json Output result as JSON
|
|
255
|
-
--help, -h Show this help`;
|
|
256
|
-
|
|
257
|
-
export async function slingCommand(args: string[]): Promise<void> {
|
|
258
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
259
|
-
process.stdout.write(`${SLING_HELP}\n`);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const taskId = args.find((a) => !a.startsWith("--"));
|
|
233
|
+
export async function slingCommand(taskId: string, opts: SlingOptions): Promise<void> {
|
|
264
234
|
if (!taskId) {
|
|
265
235
|
throw new ValidationError("Task ID is required: overstory sling <task-id>", {
|
|
266
236
|
field: "taskId",
|
|
267
237
|
});
|
|
268
238
|
}
|
|
269
239
|
|
|
270
|
-
const capability =
|
|
271
|
-
const name =
|
|
272
|
-
const specPath =
|
|
273
|
-
const filesRaw =
|
|
274
|
-
const parentAgent =
|
|
275
|
-
const depthStr =
|
|
240
|
+
const capability = opts.capability ?? "builder";
|
|
241
|
+
const name = opts.name;
|
|
242
|
+
const specPath = opts.spec ?? null;
|
|
243
|
+
const filesRaw = opts.files;
|
|
244
|
+
const parentAgent = opts.parent ?? null;
|
|
245
|
+
const depthStr = opts.depth;
|
|
276
246
|
const depth = depthStr !== undefined ? Number.parseInt(depthStr, 10) : 0;
|
|
277
|
-
const forceHierarchy =
|
|
278
|
-
const skipScout =
|
|
279
|
-
const skipTaskCheck =
|
|
247
|
+
const forceHierarchy = opts.forceHierarchy ?? false;
|
|
248
|
+
const skipScout = opts.skipScout ?? false;
|
|
249
|
+
const skipTaskCheck = opts.skipTaskCheck ?? false;
|
|
280
250
|
|
|
281
251
|
if (!name || name.trim().length === 0) {
|
|
282
252
|
throw new ValidationError("--name is required for sling", { field: "name" });
|
|
@@ -645,7 +615,7 @@ export async function slingCommand(args: string[]): Promise<void> {
|
|
|
645
615
|
pid,
|
|
646
616
|
};
|
|
647
617
|
|
|
648
|
-
if (
|
|
618
|
+
if (opts.json ?? false) {
|
|
649
619
|
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
650
620
|
} else {
|
|
651
621
|
process.stdout.write(`🚀 Agent "${name}" launched!\n`);
|
|
@@ -9,7 +9,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
9
9
|
import { mkdir } from "node:fs/promises";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { cleanupTempDir, createTempGitRepo } from "../test-helpers.ts";
|
|
12
|
-
import {
|
|
12
|
+
import { specWriteCommand, writeSpec } from "./spec.ts";
|
|
13
13
|
|
|
14
14
|
let tempDir: string;
|
|
15
15
|
let overstoryDir: string;
|
|
@@ -55,47 +55,21 @@ afterEach(async () => {
|
|
|
55
55
|
await cleanupTempDir(tempDir);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
// === help ===
|
|
59
|
-
|
|
60
|
-
describe("help", () => {
|
|
61
|
-
test("--help shows usage", async () => {
|
|
62
|
-
await specCommand(["--help"]);
|
|
63
|
-
expect(stdoutOutput).toContain("overstory spec");
|
|
64
|
-
expect(stdoutOutput).toContain("write");
|
|
65
|
-
expect(stdoutOutput).toContain("--body");
|
|
66
|
-
expect(stdoutOutput).toContain("--agent");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("-h shows usage", async () => {
|
|
70
|
-
await specCommand(["-h"]);
|
|
71
|
-
expect(stdoutOutput).toContain("overstory spec");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("no args shows help", async () => {
|
|
75
|
-
await specCommand([]);
|
|
76
|
-
expect(stdoutOutput).toContain("overstory spec");
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
58
|
// === validation ===
|
|
81
59
|
|
|
82
60
|
describe("validation", () => {
|
|
83
|
-
test("unknown subcommand throws ValidationError", async () => {
|
|
84
|
-
await expect(specCommand(["unknown"])).rejects.toThrow("Unknown spec subcommand");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
61
|
test("write without bead-id throws ValidationError", async () => {
|
|
88
|
-
await expect(
|
|
62
|
+
await expect(specWriteCommand("", {})).rejects.toThrow("Bead ID is required");
|
|
89
63
|
});
|
|
90
64
|
|
|
91
65
|
test("write without body throws ValidationError", async () => {
|
|
92
|
-
await expect(
|
|
66
|
+
await expect(specWriteCommand("task-abc", { agent: "scout-1" })).rejects.toThrow(
|
|
93
67
|
"Spec body is required",
|
|
94
68
|
);
|
|
95
69
|
});
|
|
96
70
|
|
|
97
71
|
test("write with empty body throws ValidationError", async () => {
|
|
98
|
-
await expect(
|
|
72
|
+
await expect(specWriteCommand("task-abc", { body: " " })).rejects.toThrow(
|
|
99
73
|
"Spec body is required",
|
|
100
74
|
);
|
|
101
75
|
});
|
|
@@ -165,11 +139,11 @@ describe("writeSpec", () => {
|
|
|
165
139
|
});
|
|
166
140
|
});
|
|
167
141
|
|
|
168
|
-
// ===
|
|
142
|
+
// === specWriteCommand (CLI integration) ===
|
|
169
143
|
|
|
170
|
-
describe("
|
|
144
|
+
describe("specWriteCommand (integration)", () => {
|
|
171
145
|
test("writes spec and prints path", async () => {
|
|
172
|
-
await
|
|
146
|
+
await specWriteCommand("task-cmd", { body: "# CLI Spec" });
|
|
173
147
|
|
|
174
148
|
// Path may differ due to macOS /var -> /private/var symlink resolution
|
|
175
149
|
expect(stdoutOutput.trim()).toContain(".overstory/specs/task-cmd.md");
|
|
@@ -180,7 +154,7 @@ describe("specCommand write", () => {
|
|
|
180
154
|
});
|
|
181
155
|
|
|
182
156
|
test("writes spec with agent attribution", async () => {
|
|
183
|
-
await
|
|
157
|
+
await specWriteCommand("task-attr", { body: "# Attributed", agent: "scout-2" });
|
|
184
158
|
|
|
185
159
|
expect(stdoutOutput.trim()).toContain(".overstory/specs/task-attr.md");
|
|
186
160
|
|
|
@@ -190,14 +164,14 @@ describe("specCommand write", () => {
|
|
|
190
164
|
expect(content).toContain("# Attributed");
|
|
191
165
|
});
|
|
192
166
|
|
|
193
|
-
test("
|
|
194
|
-
await
|
|
167
|
+
test("writes spec without agent when agent is omitted", async () => {
|
|
168
|
+
await specWriteCommand("task-noagent", { body: "# No Agent" });
|
|
195
169
|
|
|
196
|
-
expect(stdoutOutput.trim()).toContain(".overstory/specs/task-
|
|
170
|
+
expect(stdoutOutput.trim()).toContain(".overstory/specs/task-noagent.md");
|
|
197
171
|
|
|
198
172
|
const specPath = stdoutOutput.trim();
|
|
199
173
|
const content = await Bun.file(specPath).text();
|
|
200
|
-
expect(content).toContain("
|
|
201
|
-
expect(content).
|
|
174
|
+
expect(content).not.toContain("written-by");
|
|
175
|
+
expect(content).toBe("# No Agent\n");
|
|
202
176
|
});
|
|
203
177
|
});
|