@os-eco/overstory-cli 0.6.11 → 0.7.2

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.
Files changed (87) hide show
  1. package/README.md +12 -13
  2. package/agents/builder.md +1 -1
  3. package/agents/coordinator.md +12 -11
  4. package/agents/lead.md +25 -24
  5. package/agents/monitor.md +4 -4
  6. package/agents/reviewer.md +1 -1
  7. package/agents/scout.md +5 -5
  8. package/agents/supervisor.md +36 -32
  9. package/package.json +5 -3
  10. package/src/agents/guard-rules.ts +97 -0
  11. package/src/agents/hooks-deployer.ts +7 -90
  12. package/src/agents/overlay.test.ts +30 -7
  13. package/src/agents/overlay.ts +10 -9
  14. package/src/commands/agents.test.ts +5 -0
  15. package/src/commands/clean.test.ts +3 -0
  16. package/src/commands/completions.ts +1 -1
  17. package/src/commands/coordinator.test.ts +1 -0
  18. package/src/commands/coordinator.ts +34 -18
  19. package/src/commands/costs.test.ts +6 -1
  20. package/src/commands/costs.ts +13 -20
  21. package/src/commands/dashboard.ts +38 -138
  22. package/src/commands/doctor.test.ts +1 -1
  23. package/src/commands/doctor.ts +2 -2
  24. package/src/commands/ecosystem.ts +2 -1
  25. package/src/commands/errors.test.ts +4 -5
  26. package/src/commands/errors.ts +4 -62
  27. package/src/commands/feed.test.ts +2 -2
  28. package/src/commands/feed.ts +12 -106
  29. package/src/commands/init.test.ts +1 -2
  30. package/src/commands/init.ts +1 -8
  31. package/src/commands/inspect.test.ts +14 -0
  32. package/src/commands/inspect.ts +10 -44
  33. package/src/commands/log.test.ts +14 -0
  34. package/src/commands/log.ts +39 -0
  35. package/src/commands/logs.ts +7 -63
  36. package/src/commands/mail.test.ts +5 -0
  37. package/src/commands/metrics.test.ts +2 -2
  38. package/src/commands/metrics.ts +3 -17
  39. package/src/commands/monitor.ts +30 -16
  40. package/src/commands/nudge.test.ts +1 -0
  41. package/src/commands/prime.test.ts +2 -0
  42. package/src/commands/prime.ts +6 -2
  43. package/src/commands/replay.test.ts +2 -2
  44. package/src/commands/replay.ts +12 -135
  45. package/src/commands/run.test.ts +1 -0
  46. package/src/commands/run.ts +7 -23
  47. package/src/commands/sling.test.ts +68 -1
  48. package/src/commands/sling.ts +62 -24
  49. package/src/commands/status.test.ts +1 -0
  50. package/src/commands/status.ts +4 -17
  51. package/src/commands/stop.test.ts +1 -0
  52. package/src/commands/supervisor.ts +35 -18
  53. package/src/commands/trace.test.ts +6 -6
  54. package/src/commands/trace.ts +11 -109
  55. package/src/commands/worktree.test.ts +9 -0
  56. package/src/config.ts +39 -0
  57. package/src/doctor/consistency.test.ts +14 -0
  58. package/src/e2e/init-sling-lifecycle.test.ts +3 -5
  59. package/src/index.ts +2 -1
  60. package/src/logging/format.ts +214 -0
  61. package/src/logging/theme.ts +132 -0
  62. package/src/mail/broadcast.test.ts +1 -0
  63. package/src/merge/resolver.ts +23 -4
  64. package/src/metrics/store.test.ts +46 -0
  65. package/src/metrics/store.ts +11 -0
  66. package/src/mulch/client.test.ts +20 -0
  67. package/src/mulch/client.ts +312 -45
  68. package/src/runtimes/claude.test.ts +616 -0
  69. package/src/runtimes/claude.ts +218 -0
  70. package/src/runtimes/pi-guards.test.ts +433 -0
  71. package/src/runtimes/pi-guards.ts +349 -0
  72. package/src/runtimes/pi.test.ts +620 -0
  73. package/src/runtimes/pi.ts +244 -0
  74. package/src/runtimes/registry.test.ts +86 -0
  75. package/src/runtimes/registry.ts +46 -0
  76. package/src/runtimes/types.ts +188 -0
  77. package/src/schema-consistency.test.ts +1 -0
  78. package/src/sessions/compat.ts +1 -0
  79. package/src/sessions/store.test.ts +31 -0
  80. package/src/sessions/store.ts +37 -4
  81. package/src/types.ts +21 -0
  82. package/src/watchdog/daemon.test.ts +7 -4
  83. package/src/watchdog/daemon.ts +1 -1
  84. package/src/watchdog/health.test.ts +1 -0
  85. package/src/watchdog/triage.ts +14 -4
  86. package/src/worktree/tmux.test.ts +28 -13
  87. package/src/worktree/tmux.ts +14 -28
@@ -14,97 +14,14 @@ import { createEventStore } from "../events/store.ts";
14
14
  import { jsonOutput } from "../json.ts";
15
15
  import type { ColorFn } from "../logging/color.ts";
16
16
  import { color } from "../logging/color.ts";
17
- import type { EventType, StoredEvent } from "../types.ts";
18
-
19
- /** Compact 5-char labels for feed output. */
20
- const EVENT_LABELS: Record<EventType, { label: string; color: ColorFn }> = {
21
- tool_start: { label: "TOOL+", color: color.blue },
22
- tool_end: { label: "TOOL-", color: color.blue },
23
- session_start: { label: "SESS+", color: color.green },
24
- session_end: { label: "SESS-", color: color.yellow },
25
- mail_sent: { label: "MAIL>", color: color.cyan },
26
- mail_received: { label: "MAIL<", color: color.cyan },
27
- spawn: { label: "SPAWN", color: color.magenta },
28
- error: { label: "ERROR", color: color.red },
29
- custom: { label: "CUSTM", color: color.gray },
30
- };
31
-
32
- /** Color functions assigned to agents in order of first appearance. */
33
- const AGENT_COLORS: readonly ColorFn[] = [
34
- color.blue,
35
- color.green,
36
- color.yellow,
37
- color.cyan,
38
- color.magenta,
39
- ];
40
-
41
- /**
42
- * Format an absolute time from an ISO timestamp.
43
- * Returns "HH:MM:SS" portion.
44
- */
45
- function formatAbsoluteTime(timestamp: string): string {
46
- const match = /T(\d{2}:\d{2}:\d{2})/.exec(timestamp);
47
- if (match?.[1]) {
48
- return match[1];
49
- }
50
- return timestamp;
51
- }
52
-
53
- /**
54
- * Build a detail string for a feed event based on its type and fields.
55
- */
56
- function buildEventDetail(event: StoredEvent): string {
57
- const parts: string[] = [];
58
-
59
- if (event.toolName) {
60
- parts.push(`tool=${event.toolName}`);
61
- }
62
-
63
- if (event.toolDurationMs !== null) {
64
- parts.push(`${event.toolDurationMs}ms`);
65
- }
66
-
67
- if (event.data) {
68
- try {
69
- const parsed: unknown = JSON.parse(event.data);
70
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
71
- const data = parsed as Record<string, unknown>;
72
- for (const [key, value] of Object.entries(data)) {
73
- if (value !== null && value !== undefined) {
74
- const strValue = typeof value === "string" ? value : JSON.stringify(value);
75
- // Truncate long values
76
- const truncated = strValue.length > 60 ? `${strValue.slice(0, 57)}...` : strValue;
77
- parts.push(`${key}=${truncated}`);
78
- }
79
- }
80
- }
81
- } catch {
82
- // data is not valid JSON; show it raw if short enough
83
- if (event.data.length <= 60) {
84
- parts.push(event.data);
85
- }
86
- }
87
- }
88
-
89
- return parts.join(" ");
90
- }
91
-
92
- /**
93
- * Assign a stable color function to each agent based on order of first appearance.
94
- */
95
- function buildAgentColorMap(events: StoredEvent[]): Map<string, ColorFn> {
96
- const colorMap = new Map<string, ColorFn>();
97
- for (const event of events) {
98
- if (!colorMap.has(event.agentName)) {
99
- const colorIndex = colorMap.size % AGENT_COLORS.length;
100
- const agentColorFn = AGENT_COLORS[colorIndex];
101
- if (agentColorFn !== undefined) {
102
- colorMap.set(event.agentName, agentColorFn);
103
- }
104
- }
105
- }
106
- return colorMap;
107
- }
17
+ import {
18
+ buildAgentColorMap,
19
+ buildEventDetail,
20
+ extendAgentColorMap,
21
+ formatAbsoluteTime,
22
+ } from "../logging/format.ts";
23
+ import { eventLabel } from "../logging/theme.ts";
24
+ import type { StoredEvent } from "../types.ts";
108
25
 
109
26
  /**
110
27
  * Print a single event in compact feed format:
@@ -115,16 +32,13 @@ function printEvent(event: StoredEvent, colorMap: Map<string, ColorFn>): void {
115
32
 
116
33
  const timeStr = formatAbsoluteTime(event.createdAt);
117
34
 
118
- const eventInfo = EVENT_LABELS[event.eventType] ?? {
119
- label: event.eventType.padEnd(5),
120
- color: color.gray,
121
- };
35
+ const label = eventLabel(event.eventType);
122
36
 
123
37
  const levelColorFn =
124
38
  event.level === "error" ? color.red : event.level === "warn" ? color.yellow : null;
125
39
  const applyLevel = (text: string) => (levelColorFn ? levelColorFn(text) : text);
126
40
 
127
- const detail = buildEventDetail(event);
41
+ const detail = buildEventDetail(event, 60);
128
42
  const detailSuffix = detail ? ` ${color.dim(detail)}` : "";
129
43
 
130
44
  const agentColorFn = colorMap.get(event.agentName) ?? color.gray;
@@ -132,7 +46,7 @@ function printEvent(event: StoredEvent, colorMap: Map<string, ColorFn>): void {
132
46
 
133
47
  w(
134
48
  `${color.dim(timeStr)} ` +
135
- `${applyLevel(eventInfo.color(color.bold(eventInfo.label)))}` +
49
+ `${applyLevel(label.color(color.bold(label.compact)))}` +
136
50
  `${agentLabel}${detailSuffix}\n`,
137
51
  );
138
52
  }
@@ -291,15 +205,7 @@ async function executeFeed(opts: FeedOpts): Promise<void> {
291
205
  if (newEvents.length > 0) {
292
206
  if (!json) {
293
207
  // Update color map for any new agents
294
- for (const event of newEvents) {
295
- if (!globalColorMap.has(event.agentName)) {
296
- const colorIndex = globalColorMap.size % AGENT_COLORS.length;
297
- const agentColorFn = AGENT_COLORS[colorIndex];
298
- if (agentColorFn !== undefined) {
299
- globalColorMap.set(event.agentName, agentColorFn);
300
- }
301
- }
302
- }
208
+ extendAgentColorMap(globalColorMap, newEvents);
303
209
 
304
210
  // Print new events
305
211
  for (const event of newEvents) {
@@ -17,7 +17,6 @@ const AGENT_DEF_FILES = [
17
17
  "reviewer.md",
18
18
  "lead.md",
19
19
  "merger.md",
20
- "supervisor.md",
21
20
  "coordinator.md",
22
21
  "monitor.md",
23
22
  ];
@@ -46,7 +45,7 @@ describe("initCommand: agent-defs deployment", () => {
46
45
  await cleanupTempDir(tempDir);
47
46
  });
48
47
 
49
- test("creates .overstory/agent-defs/ with all 8 agent definition files", async () => {
48
+ test("creates .overstory/agent-defs/ with all 7 agent definition files (supervisor deprecated)", async () => {
50
49
  await initCommand({});
51
50
 
52
51
  const agentDefsDir = join(tempDir, ".overstory", "agent-defs");
@@ -241,14 +241,6 @@ function buildAgentManifest(): AgentManifest {
241
241
  canSpawn: true,
242
242
  constraints: ["read-only", "no-worktree"],
243
243
  },
244
- supervisor: {
245
- file: "supervisor.md",
246
- model: "opus",
247
- tools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "Task"],
248
- capabilities: ["coordinate", "supervise"],
249
- canSpawn: true,
250
- constraints: [],
251
- },
252
244
  monitor: {
253
245
  file: "monitor.md",
254
246
  model: "sonnet",
@@ -595,6 +587,7 @@ export async function initCommand(opts: InitOptions): Promise<void> {
595
587
  const agentDefFiles = await readdir(overstoryAgentsDir);
596
588
  for (const fileName of agentDefFiles) {
597
589
  if (!fileName.endsWith(".md")) continue;
590
+ if (fileName === "supervisor.md") continue; // Deprecated: not deployed to new projects
598
591
  const source = Bun.file(join(overstoryAgentsDir, fileName));
599
592
  const content = await source.text();
600
593
  await Bun.write(join(agentDefsTarget, fileName), content);
@@ -153,6 +153,7 @@ describe("inspectCommand", () => {
153
153
  lastActivity: new Date().toISOString(),
154
154
  escalationLevel: 0,
155
155
  stalledSince: null,
156
+ transcriptPath: null,
156
157
  });
157
158
  store.close();
158
159
 
@@ -185,6 +186,7 @@ describe("inspectCommand", () => {
185
186
  lastActivity: new Date().toISOString(),
186
187
  escalationLevel: 0,
187
188
  stalledSince: null,
189
+ transcriptPath: null,
188
190
  });
189
191
  store.close();
190
192
 
@@ -221,6 +223,7 @@ describe("inspectCommand", () => {
221
223
  lastActivity: new Date(Date.now() - 5_000).toISOString(),
222
224
  escalationLevel: 0,
223
225
  stalledSince: null,
226
+ transcriptPath: null,
224
227
  });
225
228
  store.close();
226
229
 
@@ -257,6 +260,7 @@ describe("inspectCommand", () => {
257
260
  lastActivity: new Date().toISOString(),
258
261
  escalationLevel: 0,
259
262
  stalledSince: null,
263
+ transcriptPath: null,
260
264
  });
261
265
  store.close();
262
266
 
@@ -296,6 +300,7 @@ describe("inspectCommand", () => {
296
300
  lastActivity: new Date().toISOString(),
297
301
  escalationLevel: 0,
298
302
  stalledSince: null,
303
+ transcriptPath: null,
299
304
  });
300
305
  store.close();
301
306
 
@@ -331,6 +336,7 @@ describe("inspectCommand", () => {
331
336
  lastActivity: new Date().toISOString(),
332
337
  escalationLevel: 0,
333
338
  stalledSince: null,
339
+ transcriptPath: null,
334
340
  });
335
341
  store.close();
336
342
 
@@ -366,6 +372,7 @@ describe("inspectCommand", () => {
366
372
  lastActivity: new Date().toISOString(),
367
373
  escalationLevel: 0,
368
374
  stalledSince: null,
375
+ transcriptPath: null,
369
376
  });
370
377
  store.close();
371
378
 
@@ -410,6 +417,7 @@ describe("inspectCommand", () => {
410
417
  lastActivity: new Date().toISOString(),
411
418
  escalationLevel: 0,
412
419
  stalledSince: null,
420
+ transcriptPath: null,
413
421
  });
414
422
  store.close();
415
423
 
@@ -453,6 +461,7 @@ describe("inspectCommand", () => {
453
461
  lastActivity: new Date().toISOString(),
454
462
  escalationLevel: 0,
455
463
  stalledSince: null,
464
+ transcriptPath: null,
456
465
  });
457
466
  store.close();
458
467
 
@@ -502,6 +511,7 @@ describe("inspectCommand", () => {
502
511
  lastActivity: new Date().toISOString(),
503
512
  escalationLevel: 0,
504
513
  stalledSince: null,
514
+ transcriptPath: null,
505
515
  });
506
516
  store.close();
507
517
 
@@ -539,6 +549,7 @@ describe("inspectCommand", () => {
539
549
  lastActivity: new Date().toISOString(),
540
550
  escalationLevel: 0,
541
551
  stalledSince: null,
552
+ transcriptPath: null,
542
553
  });
543
554
  store.close();
544
555
 
@@ -577,6 +588,7 @@ describe("inspectCommand", () => {
577
588
  lastActivity: new Date().toISOString(),
578
589
  escalationLevel: 0,
579
590
  stalledSince: null,
591
+ transcriptPath: null,
580
592
  });
581
593
  store.close();
582
594
 
@@ -614,6 +626,7 @@ describe("inspectCommand", () => {
614
626
  lastActivity: new Date().toISOString(),
615
627
  escalationLevel: 0,
616
628
  stalledSince: null,
629
+ transcriptPath: null,
617
630
  });
618
631
  store.close();
619
632
 
@@ -652,6 +665,7 @@ describe("inspectCommand", () => {
652
665
  lastActivity: new Date().toISOString(),
653
666
  escalationLevel: 0,
654
667
  stalledSince: null,
668
+ transcriptPath: null,
655
669
  });
656
670
  store.close();
657
671
 
@@ -11,45 +11,13 @@ import { loadConfig } from "../config.ts";
11
11
  import { ValidationError } from "../errors.ts";
12
12
  import { createEventStore } from "../events/store.ts";
13
13
  import { jsonOutput } from "../json.ts";
14
- import { accent, color } from "../logging/color.ts";
14
+ import { accent } from "../logging/color.ts";
15
+ import { formatDuration } from "../logging/format.ts";
16
+ import { renderHeader, separator, stateIconColored } from "../logging/theme.ts";
15
17
  import { createMetricsStore } from "../metrics/store.ts";
16
18
  import { openSessionStore } from "../sessions/compat.ts";
17
19
  import type { AgentSession, StoredEvent, ToolStats } from "../types.ts";
18
20
 
19
- /**
20
- * Format a duration in ms to a human-readable string.
21
- */
22
- function formatDuration(ms: number): string {
23
- const seconds = Math.floor(ms / 1000);
24
- if (seconds < 60) return `${seconds}s`;
25
- const minutes = Math.floor(seconds / 60);
26
- const remainSec = seconds % 60;
27
- if (minutes < 60) return `${minutes}m ${remainSec}s`;
28
- const hours = Math.floor(minutes / 60);
29
- const remainMin = minutes % 60;
30
- return `${hours}h ${remainMin}m`;
31
- }
32
-
33
- /**
34
- * Get colored state icon based on agent state.
35
- */
36
- function getStateIcon(state: AgentSession["state"]): string {
37
- switch (state) {
38
- case "booting":
39
- return color.green("-");
40
- case "working":
41
- return color.cyan(">");
42
- case "stalled":
43
- return color.yellow("!");
44
- case "completed":
45
- return color.dim("x");
46
- case "zombie":
47
- return color.dim("x");
48
- default:
49
- return "?";
50
- }
51
- }
52
-
53
21
  /**
54
22
  * Extract current file from most recent Edit/Write/Read tool_start event.
55
23
  */
@@ -253,12 +221,10 @@ export function printInspectData(data: InspectData): void {
253
221
  const w = process.stdout.write.bind(process.stdout);
254
222
  const { session } = data;
255
223
 
256
- w(`\nAgent Inspection: ${accent(session.agentName)}\n`);
257
- w(`${"═".repeat(80)}\n\n`);
224
+ w(`\n${renderHeader(`Agent Inspection: ${accent(session.agentName)}`)}\n\n`);
258
225
 
259
226
  // Agent state and metadata
260
- const stateIcon = getStateIcon(session.state);
261
- w(`${stateIcon} State: ${session.state}\n`);
227
+ w(`${stateIconColored(session.state)} State: ${session.state}\n`);
262
228
  w(`Last activity: ${formatDuration(data.timeSinceLastActivity)} ago\n`);
263
229
  w(`Task: ${accent(session.taskId)}\n`);
264
230
  w(`Capability: ${session.capability}\n`);
@@ -278,7 +244,7 @@ export function printInspectData(data: InspectData): void {
278
244
  // Token usage
279
245
  if (data.tokenUsage) {
280
246
  w("Token Usage\n");
281
- w(`${"─".repeat(80)}\n`);
247
+ w(`${separator()}\n`);
282
248
  w(` Input: ${data.tokenUsage.inputTokens.toLocaleString()}\n`);
283
249
  w(` Output: ${data.tokenUsage.outputTokens.toLocaleString()}\n`);
284
250
  w(` Cache read: ${data.tokenUsage.cacheReadTokens.toLocaleString()}\n`);
@@ -295,7 +261,7 @@ export function printInspectData(data: InspectData): void {
295
261
  // Tool usage statistics (top 10)
296
262
  if (data.toolStats.length > 0) {
297
263
  w("Tool Usage (Top 10)\n");
298
- w(`${"─".repeat(80)}\n`);
264
+ w(`${separator()}\n`);
299
265
  const top10 = data.toolStats.slice(0, 10);
300
266
  for (const stat of top10) {
301
267
  const avgMs = stat.avgDurationMs.toFixed(0);
@@ -308,7 +274,7 @@ export function printInspectData(data: InspectData): void {
308
274
  // Recent tool calls
309
275
  if (data.recentToolCalls.length > 0) {
310
276
  w(`Recent Tool Calls (last ${data.recentToolCalls.length})\n`);
311
- w(`${"─".repeat(80)}\n`);
277
+ w(`${separator()}\n`);
312
278
  for (const call of data.recentToolCalls) {
313
279
  const time = new Date(call.timestamp).toLocaleTimeString();
314
280
  const duration = call.durationMs !== null ? `${call.durationMs}ms` : "pending";
@@ -324,9 +290,9 @@ export function printInspectData(data: InspectData): void {
324
290
  // tmux output
325
291
  if (data.tmuxOutput) {
326
292
  w("Live Tmux Output\n");
327
- w(`${"─".repeat(80)}\n`);
293
+ w(`${separator()}\n`);
328
294
  w(`${data.tmuxOutput}\n`);
329
- w(`${"─".repeat(80)}\n`);
295
+ w(`${separator()}\n`);
330
296
  }
331
297
  }
332
298
 
@@ -229,6 +229,7 @@ describe("logCommand", () => {
229
229
  lastActivity: new Date().toISOString(),
230
230
  escalationLevel: 0,
231
231
  stalledSince: null,
232
+ transcriptPath: null,
232
233
  };
233
234
  const store = createSessionStore(dbPath);
234
235
  store.upsert(session);
@@ -284,6 +285,7 @@ describe("logCommand", () => {
284
285
  lastActivity: new Date().toISOString(),
285
286
  escalationLevel: 0,
286
287
  stalledSince: null,
288
+ transcriptPath: null,
287
289
  };
288
290
  const sessStore = createSessionStore(sessionsDbPath);
289
291
  sessStore.upsert(session);
@@ -324,6 +326,7 @@ describe("logCommand", () => {
324
326
  lastActivity: new Date(Date.now() - 60_000).toISOString(),
325
327
  escalationLevel: 0,
326
328
  stalledSince: null,
329
+ transcriptPath: null,
327
330
  };
328
331
  const store = createSessionStore(dbPath);
329
332
  store.upsert(session);
@@ -363,6 +366,7 @@ describe("logCommand", () => {
363
366
  lastActivity: new Date(Date.now() - 60_000).toISOString(),
364
367
  escalationLevel: 0,
365
368
  stalledSince: null,
369
+ transcriptPath: null,
366
370
  };
367
371
  const store = createSessionStore(dbPath);
368
372
  store.upsert(session);
@@ -400,6 +404,7 @@ describe("logCommand", () => {
400
404
  lastActivity: new Date().toISOString(),
401
405
  escalationLevel: 0,
402
406
  stalledSince: null,
407
+ transcriptPath: null,
403
408
  });
404
409
  sessionStoreLocal.close();
405
410
 
@@ -457,6 +462,7 @@ describe("logCommand", () => {
457
462
  lastActivity: new Date().toISOString(),
458
463
  escalationLevel: 0,
459
464
  stalledSince: null,
465
+ transcriptPath: null,
460
466
  });
461
467
  sessionStoreLocal.close();
462
468
 
@@ -487,6 +493,7 @@ describe("logCommand", () => {
487
493
  lastActivity: new Date().toISOString(),
488
494
  escalationLevel: 0,
489
495
  stalledSince: null,
496
+ transcriptPath: null,
490
497
  });
491
498
  sessionStoreLocal.close();
492
499
 
@@ -541,6 +548,7 @@ describe("logCommand", () => {
541
548
  lastActivity: new Date().toISOString(),
542
549
  escalationLevel: 0,
543
550
  stalledSince: null,
551
+ transcriptPath: null,
544
552
  });
545
553
  sessionStoreLocal.close();
546
554
 
@@ -594,6 +602,7 @@ describe("logCommand", () => {
594
602
  lastActivity: new Date().toISOString(),
595
603
  escalationLevel: 0,
596
604
  stalledSince: null,
605
+ transcriptPath: null,
597
606
  };
598
607
  const store = createSessionStore(dbPath);
599
608
  store.upsert(session);
@@ -634,6 +643,7 @@ describe("logCommand", () => {
634
643
  lastActivity: new Date().toISOString(),
635
644
  escalationLevel: 0,
636
645
  stalledSince: null,
646
+ transcriptPath: null,
637
647
  };
638
648
  const store = createSessionStore(dbPath);
639
649
  store.upsert(session);
@@ -676,6 +686,7 @@ describe("logCommand", () => {
676
686
  lastActivity: oldTimestamp,
677
687
  escalationLevel: 0,
678
688
  stalledSince: null,
689
+ transcriptPath: null,
679
690
  };
680
691
  const store = createSessionStore(dbPath);
681
692
  store.upsert(session);
@@ -715,6 +726,7 @@ describe("logCommand", () => {
715
726
  lastActivity: new Date().toISOString(),
716
727
  escalationLevel: 0,
717
728
  stalledSince: null,
729
+ transcriptPath: null,
718
730
  };
719
731
  const store = createSessionStore(dbPath);
720
732
  store.upsert(session);
@@ -800,6 +812,7 @@ describe("logCommand", () => {
800
812
  lastActivity: new Date().toISOString(),
801
813
  escalationLevel: 0,
802
814
  stalledSince: null,
815
+ transcriptPath: null,
803
816
  };
804
817
  const store = createSessionStore(dbPath);
805
818
  store.upsert(session);
@@ -839,6 +852,7 @@ describe("logCommand", () => {
839
852
  lastActivity: new Date().toISOString(),
840
853
  escalationLevel: 0,
841
854
  stalledSince: null,
855
+ transcriptPath: null,
842
856
  };
843
857
  const store = createSessionStore(dbPath);
844
858
  store.upsert(session);
@@ -176,6 +176,23 @@ async function resolveTranscriptPath(
176
176
  logsBase: string,
177
177
  agentName: string,
178
178
  ): Promise<string | null> {
179
+ // Check SessionStore for a runtime-provided transcript path
180
+ try {
181
+ const { store } = openSessionStore(join(projectRoot, ".overstory"));
182
+ try {
183
+ const session = store.getByName(agentName);
184
+ if (session?.transcriptPath) {
185
+ if (await Bun.file(session.transcriptPath).exists()) {
186
+ return session.transcriptPath;
187
+ }
188
+ }
189
+ } finally {
190
+ store.close();
191
+ }
192
+ } catch {
193
+ // Non-fatal: fall through to legacy resolution
194
+ }
195
+
179
196
  // Check cached path first
180
197
  const cachePath = join(logsBase, agentName, ".transcript-path");
181
198
  const cacheFile = Bun.file(cachePath);
@@ -194,6 +211,17 @@ async function resolveTranscriptPath(
194
211
  const directPath = join(claudeProjectsDir, projectKey, `${sessionId}.jsonl`);
195
212
  if (await Bun.file(directPath).exists()) {
196
213
  await Bun.write(cachePath, directPath);
214
+ // Save discovered path to SessionStore for future lookups
215
+ try {
216
+ const { store: writeStore } = openSessionStore(join(projectRoot, ".overstory"));
217
+ try {
218
+ writeStore.updateTranscriptPath(agentName, directPath);
219
+ } finally {
220
+ writeStore.close();
221
+ }
222
+ } catch {
223
+ // Non-fatal: cache write failure should not break transcript resolution
224
+ }
197
225
  return directPath;
198
226
  }
199
227
 
@@ -205,6 +233,17 @@ async function resolveTranscriptPath(
205
233
  const candidate = join(claudeProjectsDir, project, `${sessionId}.jsonl`);
206
234
  if (await Bun.file(candidate).exists()) {
207
235
  await Bun.write(cachePath, candidate);
236
+ // Save discovered path to SessionStore for future lookups
237
+ try {
238
+ const { store: writeStore } = openSessionStore(join(projectRoot, ".overstory"));
239
+ try {
240
+ writeStore.updateTranscriptPath(agentName, candidate);
241
+ } finally {
242
+ writeStore.close();
243
+ }
244
+ } catch {
245
+ // Non-fatal: cache write failure should not break transcript resolution
246
+ }
208
247
  return candidate;
209
248
  }
210
249
  }
@@ -14,8 +14,9 @@ import { Command } from "commander";
14
14
  import { loadConfig } from "../config.ts";
15
15
  import { ValidationError } from "../errors.ts";
16
16
  import { jsonOutput } from "../json.ts";
17
- import type { ColorFn } from "../logging/color.ts";
18
17
  import { color } from "../logging/color.ts";
18
+ import { formatAbsoluteTime, formatDate, logLevelColor, logLevelLabel } from "../logging/format.ts";
19
+ import { renderHeader } from "../logging/theme.ts";
19
20
  import type { LogEvent } from "../types.ts";
20
21
 
21
22
  /**
@@ -52,30 +53,6 @@ function parseRelativeTime(timeStr: string): Date {
52
53
  return new Date(timeStr);
53
54
  }
54
55
 
55
- /**
56
- * Format the date portion of an ISO timestamp.
57
- * Returns "YYYY-MM-DD".
58
- */
59
- function formatDate(timestamp: string): string {
60
- const match = /^(\d{4}-\d{2}-\d{2})/.exec(timestamp);
61
- if (match?.[1]) {
62
- return match[1];
63
- }
64
- return "";
65
- }
66
-
67
- /**
68
- * Format an absolute time from an ISO timestamp.
69
- * Returns "HH:MM:SS" portion.
70
- */
71
- function formatAbsoluteTime(timestamp: string): string {
72
- const match = /T(\d{2}:\d{2}:\d{2})/.exec(timestamp);
73
- if (match?.[1]) {
74
- return match[1];
75
- }
76
- return timestamp;
77
- }
78
-
79
56
  /**
80
57
  * Build a detail string for a log event based on its data.
81
58
  */
@@ -235,46 +212,13 @@ function filterEvents(
235
212
  });
236
213
  }
237
214
 
238
- /** Resolve a log level string to a color function. */
239
- function getLevelColor(level: string): ColorFn {
240
- switch (level) {
241
- case "debug":
242
- return color.gray;
243
- case "info":
244
- return color.blue;
245
- case "warn":
246
- return color.yellow;
247
- case "error":
248
- return color.red;
249
- default:
250
- return color.gray;
251
- }
252
- }
253
-
254
- /** Resolve a log level to its label string. */
255
- function getLevelLabel(level: string): string {
256
- switch (level) {
257
- case "debug":
258
- return "DBG";
259
- case "info":
260
- return "INF";
261
- case "warn":
262
- return "WRN";
263
- case "error":
264
- return "ERR";
265
- default:
266
- return String(level).slice(0, 3).toUpperCase();
267
- }
268
- }
269
-
270
215
  /**
271
216
  * Print log events with ANSI colors and date separators.
272
217
  */
273
218
  function printLogs(events: LogEvent[]): void {
274
219
  const w = process.stdout.write.bind(process.stdout);
275
220
 
276
- w(`${color.bold("Logs")}\n`);
277
- w(`${"=".repeat(70)}\n`);
221
+ w(`${renderHeader("Logs")}\n`);
278
222
 
279
223
  if (events.length === 0) {
280
224
  w(`${color.dim("No log files found.")}\n`);
@@ -297,8 +241,8 @@ function printLogs(events: LogEvent[]): void {
297
241
  }
298
242
 
299
243
  const time = formatAbsoluteTime(event.timestamp);
300
- const levelColorFn = getLevelColor(event.level);
301
- const levelStr = getLevelLabel(event.level);
244
+ const levelColorFn = logLevelColor(event.level);
245
+ const levelStr = logLevelLabel(event.level);
302
246
 
303
247
  const agentLabel = event.agentName ? `[${event.agentName}]` : "[unknown]";
304
248
  const detail = buildLogDetail(event);
@@ -373,8 +317,8 @@ async function followLogs(
373
317
 
374
318
  // Print immediately
375
319
  const time = formatAbsoluteTime(event.timestamp);
376
- const levelColorFn = getLevelColor(event.level);
377
- const levelStr = getLevelLabel(event.level);
320
+ const levelColorFn = logLevelColor(event.level);
321
+ const levelStr = logLevelLabel(event.level);
378
322
 
379
323
  const agentLabel = event.agentName ? `[${event.agentName}]` : "[unknown]";
380
324
  const detail = buildLogDetail(event);