@os-eco/overstory-cli 0.8.4 → 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
@@ -20,6 +20,7 @@ Requires [Bun](https://bun.sh) v1.0+, git, and tmux. At least one supported agen
20
20
  - [Codex](https://github.com/openai/codex) (`codex` CLI)
21
21
  - [Gemini CLI](https://github.com/google-gemini/gemini-cli) (`gemini` CLI)
22
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.4",
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
  }
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
- import { mkdtemp } from "node:fs/promises";
12
+ import { mkdir, mkdtemp } from "node:fs/promises";
13
13
  import { tmpdir } from "node:os";
14
14
  import { join } from "node:path";
15
15
  import { ValidationError } from "../errors.ts";
@@ -672,6 +672,7 @@ describe("inspectCommand", () => {
672
672
  toolStats: [],
673
673
  tokenUsage: null,
674
674
  tmuxOutput: null,
675
+ headlessTurnInfo: null,
675
676
  };
676
677
 
677
678
  printInspectData(data);
@@ -710,6 +711,7 @@ describe("inspectCommand", () => {
710
711
  toolStats: [],
711
712
  tokenUsage: null,
712
713
  tmuxOutput: "[Headless agent — showing recent tool events]",
714
+ headlessTurnInfo: null,
713
715
  };
714
716
 
715
717
  printInspectData(data);
@@ -720,6 +722,401 @@ describe("inspectCommand", () => {
720
722
  });
721
723
  });
722
724
 
725
+ // === stdout.log fallback (headless agents) ===
726
+
727
+ describe("stdout.log fallback", () => {
728
+ /** Create a headless session in SessionStore and return the overstoryDir. */
729
+ async function setupHeadlessSession(
730
+ agentName: string,
731
+ worktreePathVal = "/tmp/wt",
732
+ ): Promise<string> {
733
+ const overstoryDir = join(tempDir, ".overstory");
734
+ const sessionsDbPath = join(overstoryDir, "sessions.db");
735
+ const store = createSessionStore(sessionsDbPath);
736
+ store.upsert({
737
+ id: `sess-${agentName}`,
738
+ agentName,
739
+ capability: "builder",
740
+ worktreePath: worktreePathVal,
741
+ branchName: `overstory/headless/${agentName}`,
742
+ taskId: "overstory-h10",
743
+ tmuxSession: "", // headless
744
+ state: "working",
745
+ pid: process.pid,
746
+ parentAgent: null,
747
+ depth: 0,
748
+ runId: null,
749
+ startedAt: new Date().toISOString(),
750
+ lastActivity: new Date().toISOString(),
751
+ escalationLevel: 0,
752
+ stalledSince: null,
753
+ transcriptPath: null,
754
+ });
755
+ store.close();
756
+ return overstoryDir;
757
+ }
758
+
759
+ /** Write NDJSON events to stdout.log in the agent's log dir. */
760
+ async function writeStdoutLog(
761
+ overstoryDir: string,
762
+ agentName: string,
763
+ events: Record<string, unknown>[],
764
+ ): Promise<void> {
765
+ const logDir = join(overstoryDir, "logs", agentName, "2026-03-05T14-30-00-000Z");
766
+ await mkdir(logDir, { recursive: true });
767
+ const ndjson = `${events.map((e) => JSON.stringify(e)).join("\n")}\n`;
768
+ await Bun.write(join(logDir, "stdout.log"), ndjson);
769
+ }
770
+
771
+ test("populates recentToolCalls from stdout.log when events.db is empty", async () => {
772
+ const overstoryDir = await setupHeadlessSession("stdout-tools");
773
+ await writeStdoutLog(overstoryDir, "stdout-tools", [
774
+ {
775
+ type: "tool_start",
776
+ timestamp: "2026-03-05T14:30:01.000Z",
777
+ toolName: "Read",
778
+ argsSummary: "src/index.ts",
779
+ },
780
+ {
781
+ type: "tool_end",
782
+ timestamp: "2026-03-05T14:30:01.050Z",
783
+ toolName: "Read",
784
+ success: true,
785
+ durationMs: 50,
786
+ },
787
+ {
788
+ type: "tool_start",
789
+ timestamp: "2026-03-05T14:30:02.000Z",
790
+ toolName: "Edit",
791
+ argsSummary: "src/commands/inspect.ts",
792
+ },
793
+ {
794
+ type: "tool_end",
795
+ timestamp: "2026-03-05T14:30:02.200Z",
796
+ toolName: "Edit",
797
+ success: true,
798
+ durationMs: 200,
799
+ },
800
+ ]);
801
+
802
+ const data = await gatherInspectData(tempDir, "stdout-tools", { noTmux: true });
803
+
804
+ expect(data.recentToolCalls.length).toBe(2);
805
+ expect(data.recentToolCalls[0]?.toolName).toBe("Read");
806
+ expect(data.recentToolCalls[0]?.durationMs).toBe(50);
807
+ expect(data.recentToolCalls[0]?.args).toBe("src/index.ts");
808
+ expect(data.recentToolCalls[1]?.toolName).toBe("Edit");
809
+ expect(data.recentToolCalls[1]?.durationMs).toBe(200);
810
+ });
811
+
812
+ test("populates tokenUsage from turn_end events when metrics.db is absent", async () => {
813
+ const overstoryDir = await setupHeadlessSession("stdout-tokens");
814
+ await writeStdoutLog(overstoryDir, "stdout-tokens", [
815
+ {
816
+ type: "turn_start",
817
+ timestamp: "2026-03-05T14:30:00.000Z",
818
+ turn: 1,
819
+ },
820
+ {
821
+ type: "turn_end",
822
+ timestamp: "2026-03-05T14:30:05.000Z",
823
+ inputTokens: 1000,
824
+ outputTokens: 500,
825
+ cacheReadTokens: 200,
826
+ model: "claude-sonnet-4-6",
827
+ contextUtilization: 0.3,
828
+ },
829
+ {
830
+ type: "turn_start",
831
+ timestamp: "2026-03-05T14:30:06.000Z",
832
+ turn: 2,
833
+ },
834
+ {
835
+ type: "turn_end",
836
+ timestamp: "2026-03-05T14:30:10.000Z",
837
+ inputTokens: 800,
838
+ outputTokens: 300,
839
+ cacheReadTokens: 150,
840
+ model: "claude-sonnet-4-6",
841
+ contextUtilization: 0.45,
842
+ },
843
+ ]);
844
+
845
+ const data = await gatherInspectData(tempDir, "stdout-tokens", { noTmux: true });
846
+
847
+ // Token usage should be cumulative across turn_end events
848
+ expect(data.tokenUsage).not.toBeNull();
849
+ expect(data.tokenUsage?.inputTokens).toBe(1800);
850
+ expect(data.tokenUsage?.outputTokens).toBe(800);
851
+ expect(data.tokenUsage?.cacheReadTokens).toBe(350);
852
+ expect(data.tokenUsage?.modelUsed).toBe("claude-sonnet-4-6");
853
+ expect(data.tokenUsage?.cacheCreationTokens).toBe(0);
854
+ expect(data.tokenUsage?.estimatedCostUsd).toBeNull();
855
+ });
856
+
857
+ test("populates headlessTurnInfo with turn number, context utilization, and isMidTool", async () => {
858
+ const overstoryDir = await setupHeadlessSession("stdout-turn-info");
859
+ await writeStdoutLog(overstoryDir, "stdout-turn-info", [
860
+ {
861
+ type: "turn_start",
862
+ timestamp: "2026-03-05T14:30:00.000Z",
863
+ turn: 3,
864
+ },
865
+ {
866
+ type: "tool_start",
867
+ timestamp: "2026-03-05T14:30:01.000Z",
868
+ toolName: "Bash",
869
+ argsSummary: "bun test",
870
+ },
871
+ // No tool_end — still mid-tool
872
+ ]);
873
+
874
+ const data = await gatherInspectData(tempDir, "stdout-turn-info", { noTmux: true });
875
+
876
+ expect(data.headlessTurnInfo).not.toBeNull();
877
+ expect(data.headlessTurnInfo?.currentTurn).toBe(3);
878
+ expect(data.headlessTurnInfo?.isMidTool).toBe(true);
879
+ });
880
+
881
+ test("isMidTool is false when last event is not tool_start", async () => {
882
+ const overstoryDir = await setupHeadlessSession("stdout-between-turns");
883
+ await writeStdoutLog(overstoryDir, "stdout-between-turns", [
884
+ {
885
+ type: "turn_start",
886
+ timestamp: "2026-03-05T14:30:00.000Z",
887
+ turn: 2,
888
+ },
889
+ {
890
+ type: "turn_end",
891
+ timestamp: "2026-03-05T14:30:05.000Z",
892
+ inputTokens: 500,
893
+ outputTokens: 200,
894
+ cacheReadTokens: 0,
895
+ model: "claude-sonnet-4-6",
896
+ contextUtilization: 0.2,
897
+ },
898
+ ]);
899
+
900
+ const data = await gatherInspectData(tempDir, "stdout-between-turns", { noTmux: true });
901
+
902
+ expect(data.headlessTurnInfo?.isMidTool).toBe(false);
903
+ expect(data.headlessTurnInfo?.contextUtilization).toBeCloseTo(0.2);
904
+ });
905
+
906
+ test("does not overwrite tokenUsage from metrics.db with stdout.log data", async () => {
907
+ const overstoryDir = await setupHeadlessSession("stdout-no-override");
908
+ const metricsDbPath = join(overstoryDir, "metrics.db");
909
+
910
+ // Metrics DB has authoritative data
911
+ const metricsStore = createMetricsStore(metricsDbPath);
912
+ metricsStore.recordSession(
913
+ makeMetrics({
914
+ agentName: "stdout-no-override",
915
+ inputTokens: 9999,
916
+ outputTokens: 8888,
917
+ modelUsed: "claude-opus-4-6",
918
+ }),
919
+ );
920
+ metricsStore.close();
921
+
922
+ // stdout.log also has token data
923
+ await writeStdoutLog(overstoryDir, "stdout-no-override", [
924
+ {
925
+ type: "turn_end",
926
+ timestamp: "2026-03-05T14:30:05.000Z",
927
+ inputTokens: 100,
928
+ outputTokens: 50,
929
+ cacheReadTokens: 0,
930
+ model: "claude-sonnet-4-6",
931
+ contextUtilization: 0.1,
932
+ },
933
+ ]);
934
+
935
+ const data = await gatherInspectData(tempDir, "stdout-no-override", { noTmux: true });
936
+
937
+ // metrics.db data wins
938
+ expect(data.tokenUsage?.inputTokens).toBe(9999);
939
+ expect(data.tokenUsage?.modelUsed).toBe("claude-opus-4-6");
940
+ });
941
+
942
+ test("gracefully handles missing stdout.log", async () => {
943
+ const overstoryDir = await setupHeadlessSession("stdout-missing");
944
+ // Create log dir but no stdout.log inside it
945
+ await mkdir(join(overstoryDir, "logs", "stdout-missing", "2026-03-05T14-30-00-000Z"), {
946
+ recursive: true,
947
+ });
948
+
949
+ const data = await gatherInspectData(tempDir, "stdout-missing", { noTmux: true });
950
+
951
+ expect(data.recentToolCalls).toEqual([]);
952
+ expect(data.tokenUsage).toBeNull();
953
+ expect(data.headlessTurnInfo).toBeNull();
954
+ });
955
+
956
+ test("gracefully handles no log dir at all", async () => {
957
+ await setupHeadlessSession("stdout-no-log-dir");
958
+ // Don't create any log dir
959
+
960
+ const data = await gatherInspectData(tempDir, "stdout-no-log-dir", { noTmux: true });
961
+
962
+ expect(data.recentToolCalls).toEqual([]);
963
+ expect(data.headlessTurnInfo).toBeNull();
964
+ });
965
+
966
+ test("respects limit when populating recentToolCalls from stdout.log", async () => {
967
+ const overstoryDir = await setupHeadlessSession("stdout-limit");
968
+ const events: Record<string, unknown>[] = [];
969
+ for (let i = 0; i < 10; i++) {
970
+ events.push({
971
+ type: "tool_start",
972
+ timestamp: `2026-03-05T14:30:0${i}.000Z`,
973
+ toolName: "Read",
974
+ argsSummary: `src/file${i}.ts`,
975
+ });
976
+ events.push({
977
+ type: "tool_end",
978
+ timestamp: `2026-03-05T14:30:0${i}.050Z`,
979
+ toolName: "Read",
980
+ success: true,
981
+ durationMs: 50,
982
+ });
983
+ }
984
+ await writeStdoutLog(overstoryDir, "stdout-limit", events);
985
+
986
+ const data = await gatherInspectData(tempDir, "stdout-limit", {
987
+ noTmux: true,
988
+ limit: 3,
989
+ });
990
+
991
+ expect(data.recentToolCalls.length).toBe(3);
992
+ });
993
+
994
+ test("printInspectData shows Turn Progress section when headlessTurnInfo is set", () => {
995
+ const data = {
996
+ session: {
997
+ id: "sess-tp",
998
+ agentName: "headless-turn-progress",
999
+ capability: "builder",
1000
+ worktreePath: "/tmp/wt",
1001
+ branchName: "overstory/headless/tp",
1002
+ taskId: "overstory-tp",
1003
+ tmuxSession: "",
1004
+ state: "working" as const,
1005
+ pid: 12345,
1006
+ parentAgent: null,
1007
+ depth: 0,
1008
+ runId: null,
1009
+ startedAt: new Date().toISOString(),
1010
+ lastActivity: new Date().toISOString(),
1011
+ escalationLevel: 0,
1012
+ stalledSince: null,
1013
+ transcriptPath: null,
1014
+ },
1015
+ timeSinceLastActivity: 1000,
1016
+ recentToolCalls: [],
1017
+ currentFile: null,
1018
+ toolStats: [],
1019
+ tokenUsage: null,
1020
+ tmuxOutput: null,
1021
+ headlessTurnInfo: {
1022
+ currentTurn: 5,
1023
+ contextUtilization: 0.625,
1024
+ isMidTool: false,
1025
+ },
1026
+ };
1027
+
1028
+ printInspectData(data);
1029
+
1030
+ const out = output();
1031
+ expect(out).toContain("Turn Progress");
1032
+ expect(out).toContain("5");
1033
+ expect(out).toContain("62.5%");
1034
+ expect(out).toContain("between turns");
1035
+ });
1036
+
1037
+ test("printInspectData shows executing tool status when isMidTool is true", () => {
1038
+ const data = {
1039
+ session: {
1040
+ id: "sess-mid",
1041
+ agentName: "headless-mid-tool",
1042
+ capability: "builder",
1043
+ worktreePath: "/tmp/wt",
1044
+ branchName: "overstory/headless/mid",
1045
+ taskId: "overstory-mid",
1046
+ tmuxSession: "",
1047
+ state: "working" as const,
1048
+ pid: 12345,
1049
+ parentAgent: null,
1050
+ depth: 0,
1051
+ runId: null,
1052
+ startedAt: new Date().toISOString(),
1053
+ lastActivity: new Date().toISOString(),
1054
+ escalationLevel: 0,
1055
+ stalledSince: null,
1056
+ transcriptPath: null,
1057
+ },
1058
+ timeSinceLastActivity: 500,
1059
+ recentToolCalls: [],
1060
+ currentFile: null,
1061
+ toolStats: [],
1062
+ tokenUsage: null,
1063
+ tmuxOutput: null,
1064
+ headlessTurnInfo: {
1065
+ currentTurn: 2,
1066
+ contextUtilization: null,
1067
+ isMidTool: true,
1068
+ },
1069
+ };
1070
+
1071
+ printInspectData(data);
1072
+
1073
+ const out = output();
1074
+ expect(out).toContain("Turn Progress");
1075
+ expect(out).toContain("executing tool");
1076
+ });
1077
+
1078
+ test("uses latest log dir when multiple exist", async () => {
1079
+ const overstoryDir = await setupHeadlessSession("stdout-multi-dir");
1080
+ const agentLogsDir = join(overstoryDir, "logs", "stdout-multi-dir");
1081
+
1082
+ // Create two log dirs — the later one has the important data
1083
+ const oldDir = join(agentLogsDir, "2026-03-05T10-00-00-000Z");
1084
+ const newDir = join(agentLogsDir, "2026-03-05T14-30-00-000Z");
1085
+ await mkdir(oldDir, { recursive: true });
1086
+ await mkdir(newDir, { recursive: true });
1087
+
1088
+ // Old dir: no useful data
1089
+ await Bun.write(join(oldDir, "stdout.log"), "");
1090
+
1091
+ // New dir: has turn data
1092
+ const events = [
1093
+ {
1094
+ type: "turn_start",
1095
+ timestamp: "2026-03-05T14:30:00.000Z",
1096
+ turn: 7,
1097
+ },
1098
+ {
1099
+ type: "turn_end",
1100
+ timestamp: "2026-03-05T14:30:05.000Z",
1101
+ inputTokens: 2000,
1102
+ outputTokens: 700,
1103
+ cacheReadTokens: 300,
1104
+ model: "claude-sonnet-4-6",
1105
+ contextUtilization: 0.55,
1106
+ },
1107
+ ];
1108
+ await Bun.write(
1109
+ join(newDir, "stdout.log"),
1110
+ `${events.map((e) => JSON.stringify(e)).join("\n")}\n`,
1111
+ );
1112
+
1113
+ const data = await gatherInspectData(tempDir, "stdout-multi-dir", { noTmux: true });
1114
+
1115
+ expect(data.headlessTurnInfo?.currentTurn).toBe(7);
1116
+ expect(data.tokenUsage?.inputTokens).toBe(2000);
1117
+ });
1118
+ });
1119
+
723
1120
  // === Human-readable output ===
724
1121
 
725
1122
  describe("human-readable output", () => {