@os-eco/overstory-cli 0.6.1 → 0.6.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.
Files changed (110) hide show
  1. package/README.md +8 -7
  2. package/package.json +12 -4
  3. package/src/agents/checkpoint.test.ts +2 -2
  4. package/src/agents/hooks-deployer.test.ts +131 -16
  5. package/src/agents/hooks-deployer.ts +33 -1
  6. package/src/agents/identity.test.ts +27 -27
  7. package/src/agents/identity.ts +10 -10
  8. package/src/agents/lifecycle.test.ts +6 -6
  9. package/src/agents/lifecycle.ts +2 -2
  10. package/src/agents/manifest.test.ts +86 -0
  11. package/src/agents/overlay.test.ts +9 -9
  12. package/src/agents/overlay.ts +4 -4
  13. package/src/commands/agents.test.ts +8 -8
  14. package/src/commands/agents.ts +62 -91
  15. package/src/commands/clean.test.ts +36 -51
  16. package/src/commands/clean.ts +28 -49
  17. package/src/commands/completions.ts +14 -0
  18. package/src/commands/coordinator.test.ts +133 -26
  19. package/src/commands/coordinator.ts +101 -64
  20. package/src/commands/costs.test.ts +47 -47
  21. package/src/commands/costs.ts +96 -75
  22. package/src/commands/dashboard.test.ts +2 -2
  23. package/src/commands/dashboard.ts +75 -95
  24. package/src/commands/doctor.test.ts +2 -2
  25. package/src/commands/doctor.ts +92 -79
  26. package/src/commands/errors.test.ts +2 -2
  27. package/src/commands/errors.ts +56 -50
  28. package/src/commands/feed.test.ts +2 -2
  29. package/src/commands/feed.ts +86 -83
  30. package/src/commands/group.ts +167 -177
  31. package/src/commands/hooks.test.ts +2 -2
  32. package/src/commands/hooks.ts +52 -42
  33. package/src/commands/init.test.ts +19 -19
  34. package/src/commands/init.ts +7 -16
  35. package/src/commands/inspect.test.ts +18 -18
  36. package/src/commands/inspect.ts +55 -58
  37. package/src/commands/log.test.ts +26 -31
  38. package/src/commands/log.ts +97 -91
  39. package/src/commands/logs.test.ts +1 -1
  40. package/src/commands/logs.ts +101 -104
  41. package/src/commands/mail.test.ts +5 -5
  42. package/src/commands/mail.ts +157 -169
  43. package/src/commands/merge.test.ts +28 -66
  44. package/src/commands/merge.ts +21 -51
  45. package/src/commands/metrics.test.ts +8 -8
  46. package/src/commands/metrics.ts +34 -35
  47. package/src/commands/monitor.test.ts +3 -3
  48. package/src/commands/monitor.ts +57 -62
  49. package/src/commands/nudge.test.ts +1 -1
  50. package/src/commands/nudge.ts +41 -89
  51. package/src/commands/prime.test.ts +19 -51
  52. package/src/commands/prime.ts +13 -50
  53. package/src/commands/replay.test.ts +2 -2
  54. package/src/commands/replay.ts +79 -86
  55. package/src/commands/run.test.ts +1 -1
  56. package/src/commands/run.ts +97 -77
  57. package/src/commands/sling.test.ts +201 -5
  58. package/src/commands/sling.ts +37 -64
  59. package/src/commands/spec.test.ts +14 -40
  60. package/src/commands/spec.ts +32 -101
  61. package/src/commands/status.test.ts +97 -1
  62. package/src/commands/status.ts +63 -58
  63. package/src/commands/stop.test.ts +22 -40
  64. package/src/commands/stop.ts +18 -33
  65. package/src/commands/supervisor.test.ts +12 -14
  66. package/src/commands/supervisor.ts +144 -165
  67. package/src/commands/trace.test.ts +15 -15
  68. package/src/commands/trace.ts +59 -82
  69. package/src/commands/watch.test.ts +2 -2
  70. package/src/commands/watch.ts +38 -45
  71. package/src/commands/worktree.test.ts +213 -37
  72. package/src/commands/worktree.ts +110 -55
  73. package/src/config.test.ts +96 -0
  74. package/src/doctor/consistency.test.ts +14 -14
  75. package/src/doctor/databases.test.ts +22 -2
  76. package/src/doctor/databases.ts +16 -0
  77. package/src/doctor/dependencies.test.ts +55 -1
  78. package/src/doctor/dependencies.ts +113 -18
  79. package/src/doctor/merge-queue.test.ts +4 -4
  80. package/src/e2e/init-sling-lifecycle.test.ts +8 -8
  81. package/src/errors.ts +1 -1
  82. package/src/index.ts +223 -213
  83. package/src/logging/color.test.ts +74 -91
  84. package/src/logging/color.ts +52 -46
  85. package/src/logging/reporter.test.ts +10 -10
  86. package/src/logging/reporter.ts +6 -5
  87. package/src/mail/broadcast.test.ts +1 -1
  88. package/src/mail/client.test.ts +6 -6
  89. package/src/mail/store.test.ts +3 -3
  90. package/src/merge/queue.test.ts +73 -7
  91. package/src/merge/queue.ts +17 -2
  92. package/src/merge/resolver.test.ts +159 -7
  93. package/src/merge/resolver.ts +46 -2
  94. package/src/metrics/store.test.ts +44 -44
  95. package/src/metrics/store.ts +2 -2
  96. package/src/metrics/summary.test.ts +35 -35
  97. package/src/mulch/client.test.ts +1 -1
  98. package/src/schema-consistency.test.ts +239 -0
  99. package/src/sessions/compat.test.ts +3 -3
  100. package/src/sessions/compat.ts +2 -2
  101. package/src/sessions/store.test.ts +41 -4
  102. package/src/sessions/store.ts +13 -2
  103. package/src/types.ts +14 -14
  104. package/src/watchdog/daemon.test.ts +10 -10
  105. package/src/watchdog/daemon.ts +1 -1
  106. package/src/watchdog/health.test.ts +1 -1
  107. package/src/worktree/manager.test.ts +20 -20
  108. package/src/worktree/manager.ts +120 -4
  109. package/src/worktree/tmux.test.ts +98 -9
  110. package/src/worktree/tmux.ts +18 -0
@@ -88,7 +88,7 @@ describe("logCommand", () => {
88
88
  await logCommand(["--help"]);
89
89
  const out = output();
90
90
 
91
- expect(out).toContain("overstory log");
91
+ expect(out).toContain("log");
92
92
  expect(out).toContain("tool-start");
93
93
  expect(out).toContain("tool-end");
94
94
  expect(out).toContain("session-end");
@@ -99,23 +99,18 @@ describe("logCommand", () => {
99
99
  await logCommand(["-h"]);
100
100
  const out = output();
101
101
 
102
- expect(out).toContain("overstory log");
102
+ expect(out).toContain("log");
103
103
  expect(out).toContain("tool-start");
104
104
  expect(out).toContain("tool-end");
105
105
  expect(out).toContain("session-end");
106
106
  expect(out).toContain("--agent");
107
107
  });
108
108
 
109
- test("missing event with only flags throws ValidationError", async () => {
110
- // The code finds first non-flag arg. Passing only flags should trigger "Event is required"
111
- // Note: the implementation checks for undefined event
109
+ test("missing event argument throws when required argument missing", async () => {
110
+ // Commander throws when a required positional argument is missing
112
111
  await expect(async () => {
113
112
  await logCommand([]);
114
- }).toThrow(ValidationError);
115
-
116
- await expect(async () => {
117
- await logCommand([]);
118
- }).toThrow("Event is required");
113
+ }).toThrow();
119
114
  });
120
115
 
121
116
  test("invalid event name throws ValidationError", async () => {
@@ -223,7 +218,7 @@ describe("logCommand", () => {
223
218
  capability: "builder",
224
219
  worktreePath: "/tmp/test",
225
220
  branchName: "test-branch",
226
- beadId: "bead-001",
221
+ taskId: "bead-001",
227
222
  tmuxSession: "test-tmux",
228
223
  state: "working",
229
224
  pid: 12345,
@@ -278,7 +273,7 @@ describe("logCommand", () => {
278
273
  capability: "scout",
279
274
  worktreePath: "/tmp/metrics",
280
275
  branchName: "metrics-branch",
281
- beadId: "bead-002",
276
+ taskId: "bead-002",
282
277
  tmuxSession: "metrics-tmux",
283
278
  state: "working",
284
279
  pid: 54321,
@@ -304,7 +299,7 @@ describe("logCommand", () => {
304
299
 
305
300
  expect(metrics).toHaveLength(1);
306
301
  expect(metrics[0]?.agentName).toBe("metrics-agent");
307
- expect(metrics[0]?.beadId).toBe("bead-002");
302
+ expect(metrics[0]?.taskId).toBe("bead-002");
308
303
  expect(metrics[0]?.capability).toBe("scout");
309
304
  expect(metrics[0]?.parentAgent).toBe("parent-agent");
310
305
  });
@@ -318,7 +313,7 @@ describe("logCommand", () => {
318
313
  capability: "coordinator",
319
314
  worktreePath: tempDir,
320
315
  branchName: "main",
321
- beadId: "",
316
+ taskId: "",
322
317
  tmuxSession: "overstory-coordinator",
323
318
  state: "working",
324
319
  pid: 11111,
@@ -357,7 +352,7 @@ describe("logCommand", () => {
357
352
  capability: "monitor",
358
353
  worktreePath: tempDir,
359
354
  branchName: "main",
360
- beadId: "",
355
+ taskId: "",
361
356
  tmuxSession: "overstory-monitor",
362
357
  state: "working",
363
358
  pid: 22222,
@@ -394,7 +389,7 @@ describe("logCommand", () => {
394
389
  capability: "coordinator",
395
390
  worktreePath: tempDir,
396
391
  branchName: "main",
397
- beadId: "",
392
+ taskId: "",
398
393
  tmuxSession: "overstory-coordinator",
399
394
  state: "working",
400
395
  pid: 11111,
@@ -451,7 +446,7 @@ describe("logCommand", () => {
451
446
  capability: "coordinator",
452
447
  worktreePath: tempDir,
453
448
  branchName: "main",
454
- beadId: "",
449
+ taskId: "",
455
450
  tmuxSession: "overstory-coordinator-no-run",
456
451
  state: "working",
457
452
  pid: 11112,
@@ -481,7 +476,7 @@ describe("logCommand", () => {
481
476
  capability: "builder",
482
477
  worktreePath: tempDir,
483
478
  branchName: "builder-branch",
484
- beadId: "bead-builder-001",
479
+ taskId: "bead-builder-001",
485
480
  tmuxSession: "overstory-builder",
486
481
  state: "working",
487
482
  pid: 11113,
@@ -535,7 +530,7 @@ describe("logCommand", () => {
535
530
  capability: "coordinator",
536
531
  worktreePath: tempDir,
537
532
  branchName: "main",
538
- beadId: "",
533
+ taskId: "",
539
534
  tmuxSession: "overstory-coordinator-completed",
540
535
  state: "working",
541
536
  pid: 11114,
@@ -588,7 +583,7 @@ describe("logCommand", () => {
588
583
  capability: "lead",
589
584
  worktreePath: tempDir,
590
585
  branchName: "lead-alpha-branch",
591
- beadId: "bead-lead-001",
586
+ taskId: "bead-lead-001",
592
587
  tmuxSession: "overstory-lead-alpha",
593
588
  state: "working",
594
589
  pid: 33333,
@@ -628,7 +623,7 @@ describe("logCommand", () => {
628
623
  capability: "builder",
629
624
  worktreePath: tempDir,
630
625
  branchName: "builder-beta-branch",
631
- beadId: "bead-builder-001",
626
+ taskId: "bead-builder-001",
632
627
  tmuxSession: "overstory-builder-beta",
633
628
  state: "working",
634
629
  pid: 44444,
@@ -670,7 +665,7 @@ describe("logCommand", () => {
670
665
  capability: "builder",
671
666
  worktreePath: "/tmp/activity",
672
667
  branchName: "activity-branch",
673
- beadId: "bead-003",
668
+ taskId: "bead-003",
674
669
  tmuxSession: "activity-tmux",
675
670
  state: "working",
676
671
  pid: 99999,
@@ -709,7 +704,7 @@ describe("logCommand", () => {
709
704
  capability: "builder",
710
705
  worktreePath: "/tmp/booting",
711
706
  branchName: "booting-branch",
712
- beadId: "bead-004",
707
+ taskId: "bead-004",
713
708
  tmuxSession: "booting-tmux",
714
709
  state: "booting",
715
710
  pid: 11111,
@@ -794,7 +789,7 @@ describe("logCommand", () => {
794
789
  capability: "builder",
795
790
  worktreePath: tempDir,
796
791
  branchName: "mulch-fail-branch",
797
- beadId: "bead-mulch-001",
792
+ taskId: "bead-mulch-001",
798
793
  tmuxSession: "overstory-mulch-fail",
799
794
  state: "working",
800
795
  pid: 55555,
@@ -833,7 +828,7 @@ describe("logCommand", () => {
833
828
  capability: "coordinator",
834
829
  worktreePath: tempDir,
835
830
  branchName: "main",
836
- beadId: "",
831
+ taskId: "",
837
832
  tmuxSession: "overstory-coordinator-mulch",
838
833
  state: "working",
839
834
  pid: 66666,
@@ -880,7 +875,7 @@ describe("logCommand", () => {
880
875
  mulchClient: client,
881
876
  agentName: "test-builder",
882
877
  capability: "builder",
883
- beadId: "bead-123",
878
+ taskId: "bead-123",
884
879
  mailDbPath,
885
880
  parentAgent: "parent-lead",
886
881
  projectRoot: tempDir,
@@ -913,7 +908,7 @@ describe("logCommand", () => {
913
908
  mulchClient: client,
914
909
  agentName: "test-builder",
915
910
  capability: "builder",
916
- beadId: "bead-456",
911
+ taskId: "bead-456",
917
912
  mailDbPath,
918
913
  parentAgent: "parent-lead",
919
914
  projectRoot: tempDir,
@@ -945,7 +940,7 @@ describe("logCommand", () => {
945
940
  mulchClient: client,
946
941
  agentName: "test-builder",
947
942
  capability: "builder",
948
- beadId: null,
943
+ taskId: null,
949
944
  mailDbPath,
950
945
  parentAgent: null,
951
946
  projectRoot: tempDir,
@@ -973,7 +968,7 @@ describe("logCommand", () => {
973
968
  mulchClient: client,
974
969
  agentName: "test-builder",
975
970
  capability: "builder",
976
- beadId: null,
971
+ taskId: null,
977
972
  mailDbPath,
978
973
  parentAgent: null,
979
974
  projectRoot: tempDir,
@@ -1052,7 +1047,7 @@ describe("logCommand", () => {
1052
1047
  mulchClient: client,
1053
1048
  agentName: "insight-agent",
1054
1049
  capability: "builder",
1055
- beadId: "bead-insight",
1050
+ taskId: "bead-insight",
1056
1051
  mailDbPath,
1057
1052
  parentAgent: "parent-agent",
1058
1053
  projectRoot: tempDir,
@@ -1132,7 +1127,7 @@ describe("logCommand", () => {
1132
1127
  mulchClient: client,
1133
1128
  agentName: "mail-insight-agent",
1134
1129
  capability: "scout",
1135
- beadId: "bead-mail",
1130
+ taskId: "bead-mail",
1136
1131
  mailDbPath,
1137
1132
  parentAgent: "parent-agent",
1138
1133
  projectRoot: tempDir,
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { join } from "node:path";
14
+ import { Command } from "commander";
14
15
  import { updateIdentity } from "../agents/identity.ts";
15
16
  import { loadConfig } from "../config.ts";
16
17
  import { ValidationError } from "../errors.ts";
@@ -27,17 +28,6 @@ import { openSessionStore } from "../sessions/compat.ts";
27
28
  import { createRunStore } from "../sessions/store.ts";
28
29
  import type { AgentSession } from "../types.ts";
29
30
 
30
- /**
31
- * Parse a named flag value from args.
32
- */
33
- function getFlag(args: string[], flag: string): string | undefined {
34
- const idx = args.indexOf(flag);
35
- if (idx === -1 || idx + 1 >= args.length) {
36
- return undefined;
37
- }
38
- return args[idx + 1];
39
- }
40
-
41
31
  /**
42
32
  * Get or create a session timestamp directory for the agent.
43
33
  * Uses a file-based marker to track the current session directory.
@@ -237,7 +227,7 @@ export async function autoRecordExpertise(params: {
237
227
  mulchClient: MulchClient;
238
228
  agentName: string;
239
229
  capability: string;
240
- beadId: string | null;
230
+ taskId: string | null;
241
231
  mailDbPath: string;
242
232
  parentAgent: string | null;
243
233
  projectRoot: string;
@@ -257,7 +247,7 @@ export async function autoRecordExpertise(params: {
257
247
  type: "reference",
258
248
  description: `${params.capability} agent ${params.agentName} completed work in this domain. Files: ${filesList}`,
259
249
  tags: ["auto-session-end", params.capability],
260
- evidenceBead: params.beadId ?? undefined,
250
+ evidenceBead: params.taskId ?? undefined,
261
251
  });
262
252
  recordedDomains.push(domain);
263
253
  } catch {
@@ -296,7 +286,7 @@ export async function autoRecordExpertise(params: {
296
286
  type: insight.type,
297
287
  description: insight.description,
298
288
  tags: insight.tags,
299
- evidenceBead: params.beadId ?? undefined,
289
+ evidenceBead: params.taskId ?? undefined,
300
290
  });
301
291
  if (!recordedDomains.includes(insight.domain)) {
302
292
  recordedDomains.push(insight.domain);
@@ -342,63 +332,32 @@ export async function autoRecordExpertise(params: {
342
332
  }
343
333
 
344
334
  /**
345
- * Entry point for `overstory log <event> --agent <name>`.
335
+ * Core implementation for the log command.
346
336
  */
347
- const LOG_HELP = `overstory log — Log a hook event
348
-
349
- Usage: overstory log <event> --agent <name> [--stdin]
350
-
351
- Arguments:
352
- <event> Event type: tool-start, tool-end, session-end
353
-
354
- Options:
355
- --agent <name> Agent name (required)
356
- --tool-name <name> Tool name (for tool-start/tool-end events, legacy)
357
- --transcript <path> Path to Claude Code transcript JSONL (for session-end, legacy)
358
- --stdin Read hook payload JSON from stdin (preferred)
359
- --help, -h Show this help`;
360
-
361
- export async function logCommand(args: string[]): Promise<void> {
362
- if (args.includes("--help") || args.includes("-h")) {
363
- process.stdout.write(`${LOG_HELP}\n`);
364
- return;
365
- }
366
-
367
- const event = args.find((a) => !a.startsWith("--"));
368
- const agentName = getFlag(args, "--agent");
369
- const useStdin = args.includes("--stdin");
370
- const toolNameFlag = getFlag(args, "--tool-name") ?? "unknown";
371
- const transcriptPathFlag = getFlag(args, "--transcript");
372
-
373
- if (!event) {
374
- throw new ValidationError("Event is required: overstory log <event> --agent <name>", {
375
- field: "event",
376
- });
377
- }
378
-
337
+ async function runLog(opts: {
338
+ event: string;
339
+ agent: string;
340
+ toolName: string;
341
+ transcript: string | undefined;
342
+ stdin: boolean;
343
+ }): Promise<void> {
379
344
  const validEvents = ["tool-start", "tool-end", "session-end"];
380
- if (!validEvents.includes(event)) {
381
- throw new ValidationError(`Invalid event "${event}". Valid: ${validEvents.join(", ")}`, {
345
+ if (!validEvents.includes(opts.event)) {
346
+ throw new ValidationError(`Invalid event "${opts.event}". Valid: ${validEvents.join(", ")}`, {
382
347
  field: "event",
383
- value: event,
384
- });
385
- }
386
-
387
- if (!agentName) {
388
- throw new ValidationError("--agent is required for log command", {
389
- field: "agent",
348
+ value: opts.event,
390
349
  });
391
350
  }
392
351
 
393
352
  // Read stdin payload if --stdin flag is set
394
353
  let stdinPayload: Record<string, unknown> | null = null;
395
- if (useStdin) {
354
+ if (opts.stdin) {
396
355
  stdinPayload = await readStdinJson();
397
356
  }
398
357
 
399
358
  // Extract fields from stdin payload (preferred) or fall back to flags
400
359
  const toolName =
401
- typeof stdinPayload?.tool_name === "string" ? stdinPayload.tool_name : toolNameFlag;
360
+ typeof stdinPayload?.tool_name === "string" ? stdinPayload.tool_name : opts.toolName;
402
361
  const toolInput =
403
362
  stdinPayload?.tool_input !== undefined &&
404
363
  stdinPayload?.tool_input !== null &&
@@ -409,28 +368,28 @@ export async function logCommand(args: string[]): Promise<void> {
409
368
  const transcriptPath =
410
369
  typeof stdinPayload?.transcript_path === "string"
411
370
  ? stdinPayload.transcript_path
412
- : transcriptPathFlag;
371
+ : opts.transcript;
413
372
 
414
373
  const cwd = process.cwd();
415
374
  const config = await loadConfig(cwd);
416
375
  const logsBase = join(config.project.root, ".overstory", "logs");
417
- const sessionDir = await getSessionDir(logsBase, agentName);
376
+ const sessionDir = await getSessionDir(logsBase, opts.agent);
418
377
 
419
378
  const logger = createLogger({
420
379
  logDir: sessionDir,
421
- agentName,
380
+ agentName: opts.agent,
422
381
  verbose: config.logging.verbose,
423
382
  redactSecrets: config.logging.redactSecrets,
424
383
  });
425
384
 
426
- switch (event) {
385
+ switch (opts.event) {
427
386
  case "tool-start": {
428
387
  // Backward compatibility: always write to per-agent log files
429
388
  logger.toolStart(toolName, toolInput ?? {});
430
- updateLastActivity(config.project.root, agentName);
389
+ updateLastActivity(config.project.root, opts.agent);
431
390
 
432
391
  // When --stdin is used, also write to EventStore for structured observability
433
- if (useStdin) {
392
+ if (opts.stdin) {
434
393
  try {
435
394
  const eventsDbPath = join(config.project.root, ".overstory", "events.db");
436
395
  const eventStore = createEventStore(eventsDbPath);
@@ -439,7 +398,7 @@ export async function logCommand(args: string[]): Promise<void> {
439
398
  : { args: {}, summary: toolName };
440
399
  eventStore.insert({
441
400
  runId: null,
442
- agentName,
401
+ agentName: opts.agent,
443
402
  sessionId,
444
403
  eventType: "tool_start",
445
404
  toolName,
@@ -458,10 +417,10 @@ export async function logCommand(args: string[]): Promise<void> {
458
417
  case "tool-end": {
459
418
  // Backward compatibility: always write to per-agent log files
460
419
  logger.toolEnd(toolName, 0);
461
- updateLastActivity(config.project.root, agentName);
420
+ updateLastActivity(config.project.root, opts.agent);
462
421
 
463
422
  // When --stdin is used, write to EventStore and correlate with tool-start
464
- if (useStdin) {
423
+ if (opts.stdin) {
465
424
  try {
466
425
  const eventsDbPath = join(config.project.root, ".overstory", "events.db");
467
426
  const eventStore = createEventStore(eventsDbPath);
@@ -470,7 +429,7 @@ export async function logCommand(args: string[]): Promise<void> {
470
429
  : { args: {}, summary: toolName };
471
430
  eventStore.insert({
472
431
  runId: null,
473
- agentName,
432
+ agentName: opts.agent,
474
433
  sessionId,
475
434
  eventType: "tool_end",
476
435
  toolName,
@@ -479,7 +438,7 @@ export async function logCommand(args: string[]): Promise<void> {
479
438
  level: "info",
480
439
  data: JSON.stringify({ summary: filtered.summary }),
481
440
  });
482
- const correlation = eventStore.correlateToolEnd(agentName, toolName);
441
+ const correlation = eventStore.correlateToolEnd(opts.agent, toolName);
483
442
  if (correlation) {
484
443
  logger.toolEnd(toolName, correlation.durationMs);
485
444
  }
@@ -492,7 +451,7 @@ export async function logCommand(args: string[]): Promise<void> {
492
451
  if (sessionId) {
493
452
  try {
494
453
  // Throttle check
495
- const snapshotMarkerPath = join(logsBase, agentName, ".last-snapshot");
454
+ const snapshotMarkerPath = join(logsBase, opts.agent, ".last-snapshot");
496
455
  const SNAPSHOT_INTERVAL_MS = 30_000;
497
456
  const snapshotMarkerFile = Bun.file(snapshotMarkerPath);
498
457
  let shouldSnapshot = true;
@@ -505,19 +464,19 @@ export async function logCommand(args: string[]): Promise<void> {
505
464
  }
506
465
 
507
466
  if (shouldSnapshot) {
508
- const transcriptPath = await resolveTranscriptPath(
467
+ const resolvedTranscriptPath = await resolveTranscriptPath(
509
468
  config.project.root,
510
469
  sessionId,
511
470
  logsBase,
512
- agentName,
471
+ opts.agent,
513
472
  );
514
- if (transcriptPath) {
515
- const usage = await parseTranscriptUsage(transcriptPath);
473
+ if (resolvedTranscriptPath) {
474
+ const usage = await parseTranscriptUsage(resolvedTranscriptPath);
516
475
  const cost = estimateCost(usage);
517
476
  const metricsDbPath = join(config.project.root, ".overstory", "metrics.db");
518
477
  const metricsStore = createMetricsStore(metricsDbPath);
519
478
  metricsStore.recordSnapshot({
520
- agentName,
479
+ agentName: opts.agent,
521
480
  inputTokens: usage.inputTokens,
522
481
  outputTokens: usage.outputTokens,
523
482
  cacheReadTokens: usage.cacheReadTokens,
@@ -538,20 +497,20 @@ export async function logCommand(args: string[]): Promise<void> {
538
497
  break;
539
498
  }
540
499
  case "session-end":
541
- logger.info("session.end", { agentName });
500
+ logger.info("session.end", { agentName: opts.agent });
542
501
  // Transition agent state to completed
543
- transitionToCompleted(config.project.root, agentName);
502
+ transitionToCompleted(config.project.root, opts.agent);
544
503
  // Look up agent session for identity update and metrics recording
545
504
  {
546
- const agentSession = getAgentSession(config.project.root, agentName);
547
- const beadId = agentSession?.beadId ?? null;
505
+ const agentSession = getAgentSession(config.project.root, opts.agent);
506
+ const taskId = agentSession?.taskId ?? null;
548
507
 
549
508
  // Update agent identity with completed session
550
509
  const identityBaseDir = join(config.project.root, ".overstory", "agents");
551
510
  try {
552
- await updateIdentity(identityBaseDir, agentName, {
511
+ await updateIdentity(identityBaseDir, opts.agent, {
553
512
  sessionsCompleted: 1,
554
- completedTask: beadId ? { beadId, summary: `Completed task ${beadId}` } : undefined,
513
+ completedTask: taskId ? { taskId, summary: `Completed task ${taskId}` } : undefined,
555
514
  });
556
515
  } catch {
557
516
  // Non-fatal: identity may not exist for this agent
@@ -567,10 +526,10 @@ export async function logCommand(args: string[]): Promise<void> {
567
526
  await mkdir(nudgesDir, { recursive: true });
568
527
  const markerPath = join(nudgesDir, "coordinator.json");
569
528
  const marker = {
570
- from: agentName,
529
+ from: opts.agent,
571
530
  reason: "lead_completed",
572
- subject: `Lead ${agentName} completed — check mail for merge_ready/worker_done`,
573
- messageId: `auto-nudge-${agentName}-${Date.now()}`,
531
+ subject: `Lead ${opts.agent} completed — check mail for merge_ready/worker_done`,
532
+ messageId: `auto-nudge-${opts.agent}-${Date.now()}`,
574
533
  createdAt: new Date().toISOString(),
575
534
  };
576
535
  await Bun.write(markerPath, `${JSON.stringify(marker, null, "\t")}\n`);
@@ -641,8 +600,8 @@ export async function logCommand(args: string[]): Promise<void> {
641
600
  }
642
601
 
643
602
  metricsStore.recordSession({
644
- agentName,
645
- beadId: agentSession.beadId,
603
+ agentName: opts.agent,
604
+ taskId: agentSession.taskId,
646
605
  capability: agentSession.capability,
647
606
  startedAt: agentSession.startedAt,
648
607
  completedAt: now,
@@ -671,9 +630,9 @@ export async function logCommand(args: string[]): Promise<void> {
671
630
  const mailDbPath = join(config.project.root, ".overstory", "mail.db");
672
631
  await autoRecordExpertise({
673
632
  mulchClient,
674
- agentName,
633
+ agentName: opts.agent,
675
634
  capability: agentSession.capability,
676
- beadId,
635
+ taskId,
677
636
  mailDbPath,
678
637
  parentAgent: agentSession.parentAgent,
679
638
  projectRoot: config.project.root,
@@ -686,13 +645,13 @@ export async function logCommand(args: string[]): Promise<void> {
686
645
  }
687
646
 
688
647
  // Write session-end event to EventStore when --stdin is used
689
- if (useStdin) {
648
+ if (opts.stdin) {
690
649
  try {
691
650
  const eventsDbPath = join(config.project.root, ".overstory", "events.db");
692
651
  const eventStore = createEventStore(eventsDbPath);
693
652
  eventStore.insert({
694
653
  runId: null,
695
- agentName,
654
+ agentName: opts.agent,
696
655
  sessionId,
697
656
  eventType: "session_end",
698
657
  toolName: null,
@@ -709,7 +668,7 @@ export async function logCommand(args: string[]): Promise<void> {
709
668
  }
710
669
  // Clear the current session marker
711
670
  {
712
- const markerPath = join(logsBase, agentName, ".current-session");
671
+ const markerPath = join(logsBase, opts.agent, ".current-session");
713
672
  try {
714
673
  const { unlink } = await import("node:fs/promises");
715
674
  await unlink(markerPath);
@@ -722,3 +681,50 @@ export async function logCommand(args: string[]): Promise<void> {
722
681
 
723
682
  logger.close();
724
683
  }
684
+
685
+ export function createLogCommand(): Command {
686
+ return new Command("log")
687
+ .description("Log a hook event")
688
+ .argument("<event>", "Event type: tool-start, tool-end, session-end")
689
+ .option("--agent <name>", "Agent name (required)")
690
+ .option("--tool-name <name>", "Tool name (for tool-start/tool-end events, legacy)")
691
+ .option("--transcript <path>", "Path to Claude Code transcript JSONL (for session-end, legacy)")
692
+ .option("--stdin", "Read hook payload JSON from stdin (preferred)")
693
+ .action(
694
+ async (
695
+ event: string,
696
+ opts: { agent?: string; toolName?: string; transcript?: string; stdin?: boolean },
697
+ ) => {
698
+ if (!opts.agent) {
699
+ throw new ValidationError("--agent is required for log command", { field: "agent" });
700
+ }
701
+ await runLog({
702
+ event,
703
+ agent: opts.agent,
704
+ toolName: opts.toolName ?? "unknown",
705
+ transcript: opts.transcript,
706
+ stdin: opts.stdin ?? false,
707
+ });
708
+ },
709
+ );
710
+ }
711
+
712
+ /**
713
+ * Entry point for `overstory log <event> --agent <name>`.
714
+ */
715
+ export async function logCommand(args: string[]): Promise<void> {
716
+ const cmd = createLogCommand();
717
+ cmd.exitOverride();
718
+
719
+ try {
720
+ await cmd.parseAsync(args, { from: "user" });
721
+ } catch (err: unknown) {
722
+ if (err && typeof err === "object" && "code" in err) {
723
+ const code = (err as { code: string }).code;
724
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
725
+ return;
726
+ }
727
+ }
728
+ throw err;
729
+ }
730
+ }
@@ -95,7 +95,7 @@ describe("logsCommand", () => {
95
95
  await logsCommand(["--help"]);
96
96
  });
97
97
 
98
- expect(output).toContain("overstory logs");
98
+ expect(output).toContain("logs");
99
99
  expect(output).toContain("--agent");
100
100
  expect(output).toContain("--level");
101
101
  expect(output).toContain("--since");