@os-eco/overstory-cli 0.8.3 → 0.8.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 CHANGED
@@ -19,7 +19,8 @@ Requires [Bun](https://bun.sh) v1.0+, git, and tmux. At least one supported agen
19
19
  - [GitHub Copilot](https://github.com/features/copilot) (`copilot` CLI)
20
20
  - [Codex](https://github.com/openai/codex) (`codex` CLI)
21
21
  - [Gemini CLI](https://github.com/google-gemini/gemini-cli) (`gemini` CLI)
22
- - [Sapling](https://github.com/nichochar/sapling) (`sp` CLI)
22
+ - [Sapling](https://github.com/jayminwest/sapling) (`sp` CLI)
23
+ - [OpenCode](https://opencode.ai) (`opencode` CLI)
23
24
 
24
25
  ```bash
25
26
  bun install -g @os-eco/overstory-cli
@@ -282,7 +283,7 @@ overstory/
282
283
  metrics/ SQLite metrics + pricing + transcript parsing
283
284
  doctor/ Health check modules (11 checks)
284
285
  insights/ Session insight analyzer for auto-expertise
285
- runtimes/ AgentRuntime abstraction (registry + adapters: Claude, Pi, Copilot, Codex, Gemini, Sapling)
286
+ runtimes/ AgentRuntime abstraction (registry + adapters: Claude, Pi, Copilot, Codex, Gemini, Sapling, OpenCode)
286
287
  tracker/ Pluggable task tracker (beads + seeds backends)
287
288
  mulch/ mulch client (programmatic API + CLI wrapper)
288
289
  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.8.3",
3
+ "version": "0.8.5",
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",
@@ -551,13 +551,17 @@ describe("resolveModel", () => {
551
551
 
552
552
  test("returns manifest model when no config override", () => {
553
553
  const config = makeConfig();
554
- expect(resolveModel(config, baseManifest, "coordinator", "haiku")).toEqual({ model: "opus" });
554
+ expect(resolveModel(config, baseManifest, "coordinator", "haiku")).toEqual({
555
+ model: "opus",
556
+ isExplicitOverride: false,
557
+ });
555
558
  });
556
559
 
557
560
  test("config override takes precedence over manifest", () => {
558
561
  const config = makeConfig({ coordinator: "sonnet" });
559
562
  expect(resolveModel(config, baseManifest, "coordinator", "haiku")).toEqual({
560
563
  model: "sonnet",
564
+ isExplicitOverride: true,
561
565
  });
562
566
  });
563
567
 
@@ -565,12 +569,16 @@ describe("resolveModel", () => {
565
569
  const config = makeConfig();
566
570
  expect(resolveModel(config, baseManifest, "unknown-role", "haiku")).toEqual({
567
571
  model: "haiku",
572
+ isExplicitOverride: false,
568
573
  });
569
574
  });
570
575
 
571
576
  test("config override works for roles not in manifest", () => {
572
577
  const config = makeConfig({ supervisor: "opus" });
573
- expect(resolveModel(config, baseManifest, "supervisor", "sonnet")).toEqual({ model: "opus" });
578
+ expect(resolveModel(config, baseManifest, "supervisor", "sonnet")).toEqual({
579
+ model: "opus",
580
+ isExplicitOverride: true,
581
+ });
574
582
  });
575
583
 
576
584
  test("returns gateway env for provider-prefixed model", () => {
@@ -592,6 +600,7 @@ describe("resolveModel", () => {
592
600
  ANTHROPIC_API_KEY: "",
593
601
  ANTHROPIC_DEFAULT_SONNET_MODEL: "openai/gpt-5.3",
594
602
  },
603
+ isExplicitOverride: true,
595
604
  });
596
605
  });
597
606
 
@@ -618,6 +627,7 @@ describe("resolveModel", () => {
618
627
  ANTHROPIC_DEFAULT_SONNET_MODEL: "openai/gpt-5.3",
619
628
  ANTHROPIC_AUTH_TOKEN: "test-token-123",
620
629
  },
630
+ isExplicitOverride: true,
621
631
  });
622
632
  } finally {
623
633
  if (savedEnv === undefined) {
@@ -631,7 +641,7 @@ describe("resolveModel", () => {
631
641
  test("unknown provider falls through to model as-is", () => {
632
642
  const config = makeConfig({ coordinator: "unknown-provider/some-model" });
633
643
  const result = resolveModel(config, baseManifest, "coordinator", "opus");
634
- expect(result).toEqual({ model: "unknown-provider/some-model" });
644
+ expect(result).toEqual({ model: "unknown-provider/some-model", isExplicitOverride: true });
635
645
  });
636
646
 
637
647
  test("native provider returns model string without env", () => {
@@ -640,7 +650,7 @@ describe("resolveModel", () => {
640
650
  { "native-gw": { type: "native" } },
641
651
  );
642
652
  const result = resolveModel(config, baseManifest, "coordinator", "opus");
643
- expect(result).toEqual({ model: "native-gw/claude-3-5-sonnet" });
653
+ expect(result).toEqual({ model: "native-gw/claude-3-5-sonnet", isExplicitOverride: true });
644
654
  });
645
655
 
646
656
  test("handles deeply nested model ID (slashes in model name)", () => {
@@ -676,6 +686,18 @@ describe("resolveModel", () => {
676
686
  expect(result.model).toBe("sonnet");
677
687
  expect(result.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe("org/model/version");
678
688
  });
689
+
690
+ test("resolveModel sets isExplicitOverride true when config.models has override", () => {
691
+ const config = makeConfig({ builder: "opus" });
692
+ const result = resolveModel(config, baseManifest, "builder", "haiku");
693
+ expect(result.isExplicitOverride).toBe(true);
694
+ });
695
+
696
+ test("resolveModel sets isExplicitOverride false when using manifest default", () => {
697
+ const config = makeConfig();
698
+ const result = resolveModel(config, baseManifest, "coordinator", "haiku");
699
+ expect(result.isExplicitOverride).toBe(false);
700
+ });
679
701
  });
680
702
 
681
703
  describe("expandAliasFromEnv", () => {
@@ -783,7 +805,10 @@ describe("resolveModel env var expansion", () => {
783
805
  process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = "us.anthropic.claude-3-5-haiku-20241022-v1:0";
784
806
  try {
785
807
  const result = resolveModel(makeConfig(), baseManifest, "scout", "sonnet");
786
- expect(result).toEqual({ model: "us.anthropic.claude-3-5-haiku-20241022-v1:0" });
808
+ expect(result).toEqual({
809
+ model: "us.anthropic.claude-3-5-haiku-20241022-v1:0",
810
+ isExplicitOverride: false,
811
+ });
787
812
  } finally {
788
813
  if (saved === undefined) {
789
814
  delete process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
@@ -798,7 +823,7 @@ describe("resolveModel env var expansion", () => {
798
823
  delete process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
799
824
  try {
800
825
  const result = resolveModel(makeConfig(), baseManifest, "scout", "sonnet");
801
- expect(result).toEqual({ model: "haiku" });
826
+ expect(result).toEqual({ model: "haiku", isExplicitOverride: false });
802
827
  } finally {
803
828
  if (saved !== undefined) {
804
829
  process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = saved;
@@ -813,7 +838,7 @@ describe("resolveModel env var expansion", () => {
813
838
  // Config overrides to a direct model string (not an alias)
814
839
  const config = makeConfig({ builder: "claude-3-5-sonnet-20241022" });
815
840
  const result = resolveModel(config, baseManifest, "builder", "haiku");
816
- expect(result).toEqual({ model: "claude-3-5-sonnet-20241022" });
841
+ expect(result).toEqual({ model: "claude-3-5-sonnet-20241022", isExplicitOverride: true });
817
842
  } finally {
818
843
  if (saved === undefined) {
819
844
  delete process.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
@@ -829,7 +854,7 @@ describe("resolveModel env var expansion", () => {
829
854
  try {
830
855
  const config = makeConfig({ scout: "opus" });
831
856
  const result = resolveModel(config, baseManifest, "scout", "haiku");
832
- expect(result).toEqual({ model: "bedrock-opus-id" });
857
+ expect(result).toEqual({ model: "bedrock-opus-id", isExplicitOverride: true });
833
858
  } finally {
834
859
  if (saved === undefined) {
835
860
  delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL;
@@ -353,10 +353,11 @@ export function resolveModel(
353
353
  ): ResolvedModel {
354
354
  const configModel = config.models[role];
355
355
  const rawModel = configModel ?? manifest.agents[role]?.model ?? fallback;
356
+ const isExplicitOverride = configModel !== undefined;
356
357
 
357
358
  // Simple alias — expand via env var if set (e.g. ANTHROPIC_DEFAULT_SONNET_MODEL)
358
359
  if (MODEL_ALIASES.has(rawModel)) {
359
- return { model: expandAliasFromEnv(rawModel) };
360
+ return { model: expandAliasFromEnv(rawModel), isExplicitOverride };
360
361
  }
361
362
 
362
363
  // Provider-prefixed: split on first "/" to get provider name and model ID
@@ -366,10 +367,10 @@ export function resolveModel(
366
367
  const modelId = rawModel.substring(slashIdx + 1);
367
368
  const providerEnv = resolveProviderEnv(providerName, modelId, config.providers);
368
369
  if (providerEnv) {
369
- return { model: DEFAULT_GATEWAY_ALIAS, env: providerEnv };
370
+ return { model: DEFAULT_GATEWAY_ALIAS, env: providerEnv, isExplicitOverride };
370
371
  }
371
372
  }
372
373
 
373
374
  // Unknown format — return as-is (may be a direct model string)
374
- return { model: rawModel };
375
+ return { model: rawModel, isExplicitOverride };
375
376
  }
@@ -10,7 +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
+ import { getAllRuntimes, getRuntime } from "../runtimes/registry.ts";
14
14
  import { openSessionStore } from "../sessions/compat.ts";
15
15
  import { type AgentSession, SUPPORTED_CAPABILITIES } from "../types.ts";
16
16
 
@@ -30,12 +30,10 @@ export interface DiscoveredAgent {
30
30
  lastActivity: string;
31
31
  }
32
32
 
33
- /** Known instruction file paths, tried in order until one exists. */
34
- const KNOWN_INSTRUCTION_PATHS = [
35
- join(".claude", "CLAUDE.md"), // Claude Code, Pi
36
- "AGENTS.md", // Codex (future)
37
- "GEMINI.md", // Gemini CLI
38
- ];
33
+ /** Build the list of known instruction file paths from all registered runtimes. */
34
+ function getKnownInstructionPaths(): string[] {
35
+ return [...new Set(getAllRuntimes().map((r) => r.instructionPath))];
36
+ }
39
37
 
40
38
  /**
41
39
  * Extract file scope from an agent's overlay instruction file.
@@ -52,9 +50,10 @@ export async function extractFileScope(
52
50
  ): Promise<string[]> {
53
51
  try {
54
52
  let content: string | null = null;
53
+ const knownPaths = getKnownInstructionPaths();
55
54
  const pathsToTry = runtimeInstructionPath
56
- ? [runtimeInstructionPath, ...KNOWN_INSTRUCTION_PATHS]
57
- : KNOWN_INSTRUCTION_PATHS;
55
+ ? [runtimeInstructionPath, ...knownPaths]
56
+ : knownPaths;
58
57
  for (const relPath of pathsToTry) {
59
58
  const overlayPath = join(worktreePath, relPath);
60
59
  const overlayFile = Bun.file(overlayPath);
@@ -363,7 +363,7 @@ async function startCoordinator(
363
363
  );
364
364
  const manifest = await manifestLoader.load();
365
365
  const resolvedModel = resolveModel(config, manifest, "coordinator", "opus");
366
- const runtime = getRuntime(undefined, config);
366
+ const runtime = getRuntime(undefined, config, "coordinator");
367
367
 
368
368
  // Deploy hooks to the project root so the coordinator gets event logging,
369
369
  // mail check --inject, and activity tracking via the standard hook pipeline.
@@ -14,9 +14,11 @@ import { ValidationError } from "../errors.ts";
14
14
  import { jsonError, jsonOutput } from "../json.ts";
15
15
  import { color } from "../logging/color.ts";
16
16
  import { renderHeader, separator } from "../logging/theme.ts";
17
+ import { estimateCost } from "../metrics/pricing.ts";
17
18
  import { createMetricsStore } from "../metrics/store.ts";
18
- import { estimateCost, parseTranscriptUsage } from "../metrics/transcript.ts";
19
+ import { parseTranscriptUsage } from "../metrics/transcript.ts";
19
20
  import { getRuntime } from "../runtimes/registry.ts";
21
+ import type { AgentRuntime } from "../runtimes/types.ts";
20
22
  import { openSessionStore } from "../sessions/compat.ts";
21
23
  import type { SessionMetrics } from "../types.ts";
22
24
 
@@ -43,41 +45,21 @@ function padLeft(str: string, width: number): string {
43
45
  return str.length >= width ? str : " ".repeat(width - str.length) + str;
44
46
  }
45
47
 
46
- /**
47
- * Resolve the transcript directory for a given runtime and project root.
48
- *
49
- * @param runtimeId - The runtime identifier (e.g. "claude")
50
- * @param projectRoot - Absolute path to the project root
51
- * @returns Absolute path to the transcript directory, or null if not supported
52
- */
53
- function getTranscriptDir(runtimeId: string, projectRoot: string): string | null {
54
- const homeDir = process.env.HOME ?? "";
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
- }
65
-
66
48
  /**
67
49
  * Discover the orchestrator's transcript JSONL file for the given runtime.
68
50
  *
69
51
  * Scans the runtime-specific transcript directory for JSONL files and returns
70
52
  * the most recently modified one, corresponding to the current orchestrator session.
71
53
  *
72
- * @param runtimeId - The runtime identifier (e.g. "claude")
54
+ * @param runtime - The agent runtime adapter
73
55
  * @param projectRoot - Absolute path to the project root
74
56
  * @returns Absolute path to the most recent transcript, or null if none found
75
57
  */
76
58
  async function discoverOrchestratorTranscript(
77
- runtimeId: string,
59
+ runtime: AgentRuntime,
78
60
  projectRoot: string,
79
61
  ): Promise<string | null> {
80
- const transcriptDir = getTranscriptDir(runtimeId, projectRoot);
62
+ const transcriptDir = runtime.getTranscriptDir(projectRoot);
81
63
  if (transcriptDir === null) return null;
82
64
 
83
65
  let entries: string[];
@@ -292,7 +274,7 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
292
274
  // Handle --self flag (early return for self-scan)
293
275
  if (self) {
294
276
  const runtime = getRuntime(undefined, config);
295
- const transcriptPath = await discoverOrchestratorTranscript(runtime.id, config.project.root);
277
+ const transcriptPath = await discoverOrchestratorTranscript(runtime, config.project.root);
296
278
  if (!transcriptPath) {
297
279
  if (json) {
298
280
  jsonError("costs", `No transcript found for runtime '${runtime.id}'`);