@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/README.md
CHANGED
|
@@ -127,6 +127,7 @@ overstory sling <task-id> Spawn a worker agent
|
|
|
127
127
|
--parent <agent-name> Parent (for hierarchy tracking)
|
|
128
128
|
--depth <n> Current hierarchy depth
|
|
129
129
|
--skip-scout Skip scout phase (passed to lead overlay)
|
|
130
|
+
--skip-task-check Skip task existence validation
|
|
130
131
|
--json JSON output
|
|
131
132
|
|
|
132
133
|
overstory stop <agent-name> Terminate a running agent
|
|
@@ -269,16 +270,16 @@ Global Flags:
|
|
|
269
270
|
## Tech Stack
|
|
270
271
|
|
|
271
272
|
- **Runtime**: Bun (TypeScript directly, no build step)
|
|
272
|
-
- **Dependencies**:
|
|
273
|
+
- **Dependencies**: Minimal runtime — `chalk` (color output), core I/O via Bun built-in APIs
|
|
273
274
|
- **Database**: SQLite via `bun:sqlite` (WAL mode for concurrent access)
|
|
274
275
|
- **Linting**: Biome (formatter + linter)
|
|
275
|
-
- **Testing**: `bun test` (
|
|
276
|
+
- **Testing**: `bun test` (2128 tests across 76 files, colocated with source)
|
|
276
277
|
- **External CLIs**: `bd` (beads) or `sd` (seeds), `mulch`, `git`, `tmux` — invoked as subprocesses
|
|
277
278
|
|
|
278
279
|
## Development
|
|
279
280
|
|
|
280
281
|
```bash
|
|
281
|
-
# Run tests (
|
|
282
|
+
# Run tests (2128 tests across 76 files)
|
|
282
283
|
bun test
|
|
283
284
|
|
|
284
285
|
# Run a single test
|
|
@@ -307,14 +308,14 @@ Use the bump script to update both:
|
|
|
307
308
|
bun run version:bump <major|minor|patch>
|
|
308
309
|
```
|
|
309
310
|
|
|
310
|
-
Git tags are
|
|
311
|
+
Git tags, npm publishing, and GitHub releases are handled automatically by the `publish.yml` workflow when a version bump is pushed to `main`.
|
|
311
312
|
|
|
312
313
|
## Project Structure
|
|
313
314
|
|
|
314
315
|
```
|
|
315
316
|
overstory/
|
|
316
317
|
src/
|
|
317
|
-
index.ts CLI entry point (
|
|
318
|
+
index.ts CLI entry point (Commander.js program)
|
|
318
319
|
types.ts Shared types and interfaces
|
|
319
320
|
config.ts Config loader + validation
|
|
320
321
|
errors.ts Custom error types
|
|
@@ -322,7 +323,7 @@ overstory/
|
|
|
322
323
|
agents.ts Agent discovery and querying
|
|
323
324
|
coordinator.ts Persistent orchestrator lifecycle
|
|
324
325
|
supervisor.ts Team lead management
|
|
325
|
-
dashboard.ts Live TUI dashboard (ANSI
|
|
326
|
+
dashboard.ts Live TUI dashboard (ANSI via Chalk)
|
|
326
327
|
hooks.ts Orchestrator hooks management
|
|
327
328
|
sling.ts Agent spawning
|
|
328
329
|
group.ts Task group batch tracking
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@os-eco/overstory-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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",
|
|
@@ -21,10 +21,15 @@
|
|
|
21
21
|
"developer-tools"
|
|
22
22
|
],
|
|
23
23
|
"bin": {
|
|
24
|
-
"overstory": "./src/index.ts"
|
|
24
|
+
"overstory": "./src/index.ts",
|
|
25
|
+
"ov": "./src/index.ts"
|
|
25
26
|
},
|
|
26
27
|
"main": "src/index.ts",
|
|
27
|
-
"files": [
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"agents",
|
|
31
|
+
"templates"
|
|
32
|
+
],
|
|
28
33
|
"publishConfig": {
|
|
29
34
|
"access": "public"
|
|
30
35
|
},
|
|
@@ -38,7 +43,10 @@
|
|
|
38
43
|
"typecheck": "tsc --noEmit",
|
|
39
44
|
"version:bump": "bun scripts/version-bump.ts"
|
|
40
45
|
},
|
|
41
|
-
"dependencies": {
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"chalk": "^5.6.2",
|
|
48
|
+
"commander": "^14.0.3"
|
|
49
|
+
},
|
|
42
50
|
"devDependencies": {
|
|
43
51
|
"@types/bun": "latest",
|
|
44
52
|
"typescript": "^5.9.0",
|
|
@@ -894,35 +894,37 @@ describe("isOverstoryHookEntry", () => {
|
|
|
894
894
|
describe("getCapabilityGuards", () => {
|
|
895
895
|
// 10 native team tool blocks apply to ALL capabilities
|
|
896
896
|
const NATIVE_TEAM_TOOL_COUNT = 10;
|
|
897
|
+
// 3 interactive tool blocks (AskUserQuestion, EnterPlanMode, EnterWorktree) apply to ALL capabilities
|
|
898
|
+
const INTERACTIVE_TOOL_COUNT = 3;
|
|
897
899
|
|
|
898
|
-
test("returns
|
|
900
|
+
test("returns 17 guards for scout (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
|
|
899
901
|
const guards = getCapabilityGuards("scout");
|
|
900
|
-
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
|
|
902
|
+
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
|
|
901
903
|
});
|
|
902
904
|
|
|
903
|
-
test("returns
|
|
905
|
+
test("returns 17 guards for reviewer (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
|
|
904
906
|
const guards = getCapabilityGuards("reviewer");
|
|
905
|
-
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
|
|
907
|
+
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
|
|
906
908
|
});
|
|
907
909
|
|
|
908
|
-
test("returns
|
|
910
|
+
test("returns 17 guards for lead (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
|
|
909
911
|
const guards = getCapabilityGuards("lead");
|
|
910
|
-
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
|
|
912
|
+
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
|
|
911
913
|
});
|
|
912
914
|
|
|
913
|
-
test("returns
|
|
915
|
+
test("returns 14 guards for builder (10 team + 3 interactive + 1 bash path boundary)", () => {
|
|
914
916
|
const guards = getCapabilityGuards("builder");
|
|
915
|
-
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 1);
|
|
917
|
+
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 1);
|
|
916
918
|
});
|
|
917
919
|
|
|
918
|
-
test("returns
|
|
920
|
+
test("returns 14 guards for merger (10 team + 3 interactive + 1 bash path boundary)", () => {
|
|
919
921
|
const guards = getCapabilityGuards("merger");
|
|
920
|
-
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 1);
|
|
922
|
+
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 1);
|
|
921
923
|
});
|
|
922
924
|
|
|
923
|
-
test("returns
|
|
925
|
+
test("returns 13 guards for unknown capability (10 team + 3 interactive tool blocks)", () => {
|
|
924
926
|
const guards = getCapabilityGuards("unknown");
|
|
925
|
-
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT);
|
|
927
|
+
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT);
|
|
926
928
|
});
|
|
927
929
|
|
|
928
930
|
test("builder gets Bash path boundary guard", () => {
|
|
@@ -1039,14 +1041,90 @@ describe("getCapabilityGuards", () => {
|
|
|
1039
1041
|
expect(taskGuard?.hooks[0]?.command).toContain('[ -z "$OVERSTORY_AGENT_NAME" ] && exit 0;');
|
|
1040
1042
|
});
|
|
1041
1043
|
|
|
1042
|
-
test("coordinator gets
|
|
1044
|
+
test("coordinator gets 17 guards (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
|
|
1043
1045
|
const guards = getCapabilityGuards("coordinator");
|
|
1044
|
-
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
|
|
1046
|
+
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
|
|
1045
1047
|
});
|
|
1046
1048
|
|
|
1047
|
-
test("supervisor gets
|
|
1049
|
+
test("supervisor gets 17 guards (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
|
|
1048
1050
|
const guards = getCapabilityGuards("supervisor");
|
|
1049
|
-
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
|
|
1051
|
+
expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
test("all capabilities get AskUserQuestion blocked", () => {
|
|
1055
|
+
for (const cap of [
|
|
1056
|
+
"scout",
|
|
1057
|
+
"reviewer",
|
|
1058
|
+
"lead",
|
|
1059
|
+
"coordinator",
|
|
1060
|
+
"supervisor",
|
|
1061
|
+
"builder",
|
|
1062
|
+
"merger",
|
|
1063
|
+
"unknown",
|
|
1064
|
+
]) {
|
|
1065
|
+
const guards = getCapabilityGuards(cap);
|
|
1066
|
+
const guard = guards.find((g) => g.matcher === "AskUserQuestion");
|
|
1067
|
+
expect(guard).toBeDefined();
|
|
1068
|
+
expect(guard?.hooks[0]?.command).toContain("human interaction");
|
|
1069
|
+
expect(guard?.hooks[0]?.command).toContain("overstory mail");
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
test("all capabilities get EnterPlanMode blocked", () => {
|
|
1074
|
+
for (const cap of [
|
|
1075
|
+
"scout",
|
|
1076
|
+
"reviewer",
|
|
1077
|
+
"lead",
|
|
1078
|
+
"coordinator",
|
|
1079
|
+
"supervisor",
|
|
1080
|
+
"builder",
|
|
1081
|
+
"merger",
|
|
1082
|
+
"unknown",
|
|
1083
|
+
]) {
|
|
1084
|
+
const guards = getCapabilityGuards(cap);
|
|
1085
|
+
const guard = guards.find((g) => g.matcher === "EnterPlanMode");
|
|
1086
|
+
expect(guard).toBeDefined();
|
|
1087
|
+
expect(guard?.hooks[0]?.command).toContain("human interaction");
|
|
1088
|
+
expect(guard?.hooks[0]?.command).toContain("overstory mail");
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
test("all capabilities get EnterWorktree blocked", () => {
|
|
1093
|
+
for (const cap of [
|
|
1094
|
+
"scout",
|
|
1095
|
+
"reviewer",
|
|
1096
|
+
"lead",
|
|
1097
|
+
"coordinator",
|
|
1098
|
+
"supervisor",
|
|
1099
|
+
"builder",
|
|
1100
|
+
"merger",
|
|
1101
|
+
"unknown",
|
|
1102
|
+
]) {
|
|
1103
|
+
const guards = getCapabilityGuards(cap);
|
|
1104
|
+
const guard = guards.find((g) => g.matcher === "EnterWorktree");
|
|
1105
|
+
expect(guard).toBeDefined();
|
|
1106
|
+
expect(guard?.hooks[0]?.command).toContain("human interaction");
|
|
1107
|
+
expect(guard?.hooks[0]?.command).toContain("overstory mail");
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
test("interactive guards include env var guard prefix", () => {
|
|
1112
|
+
const guards = getCapabilityGuards("builder");
|
|
1113
|
+
for (const tool of ["AskUserQuestion", "EnterPlanMode", "EnterWorktree"]) {
|
|
1114
|
+
const guard = guards.find((g) => g.matcher === tool);
|
|
1115
|
+
expect(guard).toBeDefined();
|
|
1116
|
+
expect(guard?.hooks[0]?.command).toContain('[ -z "$OVERSTORY_AGENT_NAME" ] && exit 0;');
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
test("interactive guard block reason mentions tool name", () => {
|
|
1121
|
+
const guards = getCapabilityGuards("scout");
|
|
1122
|
+
const askGuard = guards.find((g) => g.matcher === "AskUserQuestion");
|
|
1123
|
+
expect(askGuard?.hooks[0]?.command).toContain("AskUserQuestion");
|
|
1124
|
+
const planGuard = guards.find((g) => g.matcher === "EnterPlanMode");
|
|
1125
|
+
expect(planGuard?.hooks[0]?.command).toContain("EnterPlanMode");
|
|
1126
|
+
const worktreeGuard = guards.find((g) => g.matcher === "EnterWorktree");
|
|
1127
|
+
expect(worktreeGuard?.hooks[0]?.command).toContain("EnterWorktree");
|
|
1050
1128
|
});
|
|
1051
1129
|
});
|
|
1052
1130
|
|
|
@@ -46,6 +46,13 @@ const NATIVE_TEAM_TOOLS = [
|
|
|
46
46
|
"TaskStop",
|
|
47
47
|
];
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Tools that require human interaction and block indefinitely in non-interactive
|
|
51
|
+
* tmux sessions. Agents run non-interactively and must never call these tools.
|
|
52
|
+
* Use overstory mail (--type question) to escalate to the orchestrator instead.
|
|
53
|
+
*/
|
|
54
|
+
const INTERACTIVE_TOOLS = ["AskUserQuestion", "EnterPlanMode", "EnterWorktree"];
|
|
55
|
+
|
|
49
56
|
/** Tools that non-implementation agents must not use. */
|
|
50
57
|
const WRITE_TOOLS = ["Write", "Edit", "NotebookEdit"];
|
|
51
58
|
|
|
@@ -445,6 +452,17 @@ export function getCapabilityGuards(capability: string): HookEntry[] {
|
|
|
445
452
|
);
|
|
446
453
|
guards.push(...teamToolGuards);
|
|
447
454
|
|
|
455
|
+
// Block interactive tools for ALL overstory agents.
|
|
456
|
+
// These tools require a human to respond and block indefinitely in tmux sessions.
|
|
457
|
+
// Agents must use overstory mail (--type question) to escalate instead.
|
|
458
|
+
const interactiveGuards = INTERACTIVE_TOOLS.map((tool) =>
|
|
459
|
+
blockGuard(
|
|
460
|
+
tool,
|
|
461
|
+
`${tool} requires human interaction -- agents run non-interactively. Use overstory mail (--type question) to escalate`,
|
|
462
|
+
),
|
|
463
|
+
);
|
|
464
|
+
guards.push(...interactiveGuards);
|
|
465
|
+
|
|
448
466
|
if (NON_IMPLEMENTATION_CAPABILITIES.has(capability)) {
|
|
449
467
|
const toolGuards = WRITE_TOOLS.map((tool) =>
|
|
450
468
|
blockGuard(tool, `${capability} agents cannot modify files — ${tool} is not allowed`),
|
|
@@ -635,6 +635,40 @@ describe("resolveModel", () => {
|
|
|
635
635
|
const result = resolveModel(config, baseManifest, "coordinator", "opus");
|
|
636
636
|
expect(result).toEqual({ model: "native-gw/claude-3-5-sonnet" });
|
|
637
637
|
});
|
|
638
|
+
|
|
639
|
+
test("handles deeply nested model ID (slashes in model name)", () => {
|
|
640
|
+
const config = makeConfig(
|
|
641
|
+
{ coordinator: "openrouter/openai/gpt-5.3" },
|
|
642
|
+
{
|
|
643
|
+
openrouter: {
|
|
644
|
+
type: "gateway",
|
|
645
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
646
|
+
authTokenEnv: "OPENROUTER_API_KEY",
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
);
|
|
650
|
+
const result = resolveModel(config, baseManifest, "coordinator", "opus");
|
|
651
|
+
// First "/" splits provider "openrouter" from model ID "openai/gpt-5.3"
|
|
652
|
+
expect(result.model).toBe("sonnet");
|
|
653
|
+
expect(result.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe("openai/gpt-5.3");
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("handles model ID with multiple slashes after provider", () => {
|
|
657
|
+
const config = makeConfig(
|
|
658
|
+
{ coordinator: "mygateway/org/model/version" },
|
|
659
|
+
{
|
|
660
|
+
mygateway: {
|
|
661
|
+
type: "gateway",
|
|
662
|
+
baseUrl: "https://mygateway.example.com",
|
|
663
|
+
authTokenEnv: "MYGATEWAY_KEY",
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
);
|
|
667
|
+
const result = resolveModel(config, baseManifest, "coordinator", "opus");
|
|
668
|
+
// Provider is "mygateway", model ID is everything after the first "/"
|
|
669
|
+
expect(result.model).toBe("sonnet");
|
|
670
|
+
expect(result.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe("org/model/version");
|
|
671
|
+
});
|
|
638
672
|
});
|
|
639
673
|
|
|
640
674
|
describe("resolveProviderEnv", () => {
|
|
@@ -704,6 +738,58 @@ describe("resolveProviderEnv", () => {
|
|
|
704
738
|
);
|
|
705
739
|
expect(result).not.toHaveProperty("ANTHROPIC_AUTH_TOKEN");
|
|
706
740
|
});
|
|
741
|
+
|
|
742
|
+
test("env always sets ANTHROPIC_API_KEY to empty string", () => {
|
|
743
|
+
const result = resolveProviderEnv(
|
|
744
|
+
"openrouter",
|
|
745
|
+
"openai/gpt-5.3",
|
|
746
|
+
{
|
|
747
|
+
openrouter: {
|
|
748
|
+
type: "gateway",
|
|
749
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
750
|
+
authTokenEnv: "OPENROUTER_API_KEY",
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
{ OPENROUTER_API_KEY: "my-token" },
|
|
754
|
+
);
|
|
755
|
+
expect(result).not.toBeNull();
|
|
756
|
+
expect(result?.ANTHROPIC_API_KEY).toBe("");
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test("handles authTokenEnv pointing to undefined env var", () => {
|
|
760
|
+
const result = resolveProviderEnv(
|
|
761
|
+
"openrouter",
|
|
762
|
+
"openai/gpt-5.3",
|
|
763
|
+
{
|
|
764
|
+
openrouter: {
|
|
765
|
+
type: "gateway",
|
|
766
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
767
|
+
authTokenEnv: "MISSING_VAR",
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
{},
|
|
771
|
+
);
|
|
772
|
+
expect(result).not.toBeNull();
|
|
773
|
+
expect(result).not.toHaveProperty("ANTHROPIC_AUTH_TOKEN");
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
test("handles authTokenEnv field being undefined", () => {
|
|
777
|
+
const result = resolveProviderEnv(
|
|
778
|
+
"mygw",
|
|
779
|
+
"some-model",
|
|
780
|
+
{
|
|
781
|
+
mygw: {
|
|
782
|
+
type: "gateway",
|
|
783
|
+
baseUrl: "https://mygw.example.com",
|
|
784
|
+
},
|
|
785
|
+
},
|
|
786
|
+
{},
|
|
787
|
+
);
|
|
788
|
+
expect(result).not.toBeNull();
|
|
789
|
+
expect(result?.ANTHROPIC_BASE_URL).toBe("https://mygw.example.com");
|
|
790
|
+
expect(result?.ANTHROPIC_API_KEY).toBe("");
|
|
791
|
+
expect(result).not.toHaveProperty("ANTHROPIC_AUTH_TOKEN");
|
|
792
|
+
});
|
|
707
793
|
});
|
|
708
794
|
|
|
709
795
|
describe("manifest validation accepts arbitrary model strings", () => {
|
|
@@ -300,18 +300,18 @@ logging:
|
|
|
300
300
|
|
|
301
301
|
it("should show help with --help flag", async () => {
|
|
302
302
|
await agentsCommand(["--help"]);
|
|
303
|
-
expect(stdoutBuffer).toContain("
|
|
303
|
+
expect(stdoutBuffer).toContain("agents");
|
|
304
304
|
expect(stdoutBuffer).toContain("discover");
|
|
305
305
|
});
|
|
306
306
|
|
|
307
307
|
it("should show help with no subcommand", async () => {
|
|
308
308
|
await agentsCommand([]);
|
|
309
|
-
expect(stdoutBuffer).toContain("
|
|
309
|
+
expect(stdoutBuffer).toContain("agents");
|
|
310
310
|
expect(stdoutBuffer).toContain("discover");
|
|
311
311
|
});
|
|
312
312
|
|
|
313
313
|
it("should error on unknown subcommand", async () => {
|
|
314
|
-
await expect(agentsCommand(["unknown"])).rejects.toThrow("
|
|
314
|
+
await expect(agentsCommand(["unknown"])).rejects.toThrow("unknown command");
|
|
315
315
|
});
|
|
316
316
|
|
|
317
317
|
afterEach(async () => {
|
package/src/commands/agents.ts
CHANGED
|
@@ -5,26 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
+
import { Command } from "commander";
|
|
8
9
|
import { loadConfig } from "../config.ts";
|
|
9
10
|
import { ValidationError } from "../errors.ts";
|
|
10
11
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
11
12
|
import { type AgentSession, SUPPORTED_CAPABILITIES } from "../types.ts";
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
* Parse a named flag value from args.
|
|
15
|
-
*/
|
|
16
|
-
function getFlag(args: string[], flag: string): string | undefined {
|
|
17
|
-
const idx = args.indexOf(flag);
|
|
18
|
-
if (idx === -1 || idx + 1 >= args.length) {
|
|
19
|
-
return undefined;
|
|
20
|
-
}
|
|
21
|
-
return args[idx + 1];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function hasFlag(args: string[], flag: string): boolean {
|
|
25
|
-
return args.includes(flag);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
14
|
/**
|
|
29
15
|
* Discovered agent information including file scope.
|
|
30
16
|
*/
|
|
@@ -194,94 +180,79 @@ function printAgents(agents: DiscoveredAgent[]): void {
|
|
|
194
180
|
}
|
|
195
181
|
}
|
|
196
182
|
|
|
197
|
-
const DISCOVER_HELP = `overstory agents discover — Find active agents by capability
|
|
198
|
-
|
|
199
|
-
Usage: overstory agents discover [--capability <type>] [--all] [--json]
|
|
200
|
-
|
|
201
|
-
Options:
|
|
202
|
-
--capability <type> Filter by capability (builder, scout, reviewer, lead, merger, coordinator, supervisor)
|
|
203
|
-
--all Include completed and zombie agents (default: active only)
|
|
204
|
-
--json Output as JSON
|
|
205
|
-
--help, -h Show this help`;
|
|
206
|
-
|
|
207
|
-
const AGENTS_HELP = `overstory agents — Discover and query agents
|
|
208
|
-
|
|
209
|
-
Usage: overstory agents <subcommand> [options]
|
|
210
|
-
|
|
211
|
-
Subcommands:
|
|
212
|
-
discover Find active agents by capability
|
|
213
|
-
|
|
214
|
-
Options:
|
|
215
|
-
--json Output as JSON
|
|
216
|
-
--help, -h Show this help
|
|
217
|
-
|
|
218
|
-
Run 'overstory agents <subcommand> --help' for subcommand-specific help.`;
|
|
219
|
-
|
|
220
183
|
/**
|
|
221
|
-
*
|
|
184
|
+
* Create the Commander command for `overstory agents`.
|
|
222
185
|
*/
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
186
|
+
export function createAgentsCommand(): Command {
|
|
187
|
+
const cmd = new Command("agents").description("Discover and query agents");
|
|
188
|
+
|
|
189
|
+
cmd
|
|
190
|
+
.command("discover")
|
|
191
|
+
.description("Find active agents by capability")
|
|
192
|
+
.option(
|
|
193
|
+
"--capability <type>",
|
|
194
|
+
"Filter by capability (builder, scout, reviewer, lead, merger, coordinator, supervisor)",
|
|
195
|
+
)
|
|
196
|
+
.option("--all", "Include completed and zombie agents (default: active only)")
|
|
197
|
+
.option("--json", "Output as JSON")
|
|
198
|
+
.action(async (opts: { capability?: string; all?: boolean; json?: boolean }) => {
|
|
199
|
+
const capability = opts.capability;
|
|
200
|
+
|
|
201
|
+
// Validate capability if provided
|
|
202
|
+
if (capability && !SUPPORTED_CAPABILITIES.includes(capability as never)) {
|
|
203
|
+
throw new ValidationError(
|
|
204
|
+
`Invalid capability: ${capability}. Must be one of: ${SUPPORTED_CAPABILITIES.join(", ")}`,
|
|
205
|
+
{
|
|
206
|
+
field: "capability",
|
|
207
|
+
value: capability,
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
}
|
|
228
211
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// Validate capability if provided
|
|
234
|
-
if (capability && !SUPPORTED_CAPABILITIES.includes(capability as never)) {
|
|
235
|
-
throw new ValidationError(
|
|
236
|
-
`Invalid capability: ${capability}. Must be one of: ${SUPPORTED_CAPABILITIES.join(", ")}`,
|
|
237
|
-
{
|
|
238
|
-
field: "capability",
|
|
239
|
-
value: capability,
|
|
240
|
-
},
|
|
241
|
-
);
|
|
242
|
-
}
|
|
212
|
+
const cwd = process.cwd();
|
|
213
|
+
const config = await loadConfig(cwd);
|
|
214
|
+
const root = config.project.root;
|
|
243
215
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
216
|
+
const agents = await discoverAgents(root, {
|
|
217
|
+
capability,
|
|
218
|
+
includeAll: opts.all ?? false,
|
|
219
|
+
});
|
|
247
220
|
|
|
248
|
-
|
|
221
|
+
if (opts.json) {
|
|
222
|
+
process.stdout.write(`${JSON.stringify(agents, null, "\t")}\n`);
|
|
223
|
+
} else {
|
|
224
|
+
printAgents(agents);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
249
227
|
|
|
250
|
-
|
|
251
|
-
process.stdout.write(`${JSON.stringify(agents, null, "\t")}\n`);
|
|
252
|
-
} else {
|
|
253
|
-
printAgents(agents);
|
|
254
|
-
}
|
|
228
|
+
return cmd;
|
|
255
229
|
}
|
|
256
230
|
|
|
257
231
|
/**
|
|
258
232
|
* Entry point for `overstory agents <subcommand>`.
|
|
259
233
|
*/
|
|
260
234
|
export async function agentsCommand(args: string[]): Promise<void> {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
235
|
+
const cmd = createAgentsCommand();
|
|
236
|
+
cmd.exitOverride();
|
|
265
237
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (!subcommand) {
|
|
270
|
-
process.stdout.write(`${AGENTS_HELP}\n`);
|
|
238
|
+
if (args.length === 0) {
|
|
239
|
+
process.stdout.write(cmd.helpInformation());
|
|
271
240
|
return;
|
|
272
241
|
}
|
|
273
242
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
243
|
+
try {
|
|
244
|
+
await cmd.parseAsync(args, { from: "user" });
|
|
245
|
+
} catch (err: unknown) {
|
|
246
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
247
|
+
const code = (err as { code: string }).code;
|
|
248
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (code === "commander.unknownCommand") {
|
|
252
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
253
|
+
throw new ValidationError(message, { field: "subcommand" });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
throw err;
|
|
286
257
|
}
|
|
287
258
|
}
|