@os-eco/overstory-cli 0.7.0 → 0.7.3

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 (91) hide show
  1. package/README.md +7 -6
  2. package/agents/builder.md +1 -1
  3. package/agents/coordinator.md +12 -11
  4. package/agents/lead.md +6 -6
  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 +1 -1
  10. package/src/agents/guard-rules.ts +97 -0
  11. package/src/agents/hooks-deployer.test.ts +6 -5
  12. package/src/agents/hooks-deployer.ts +7 -90
  13. package/src/agents/identity.test.ts +3 -2
  14. package/src/agents/manifest.test.ts +4 -3
  15. package/src/agents/overlay.test.ts +10 -9
  16. package/src/agents/overlay.ts +5 -5
  17. package/src/commands/agents.test.ts +10 -4
  18. package/src/commands/clean.test.ts +3 -0
  19. package/src/commands/completions.test.ts +8 -5
  20. package/src/commands/completions.ts +38 -2
  21. package/src/commands/coordinator.test.ts +1 -0
  22. package/src/commands/coordinator.ts +15 -11
  23. package/src/commands/costs.test.ts +9 -3
  24. package/src/commands/dashboard.test.ts +265 -6
  25. package/src/commands/dashboard.ts +367 -64
  26. package/src/commands/doctor.test.ts +3 -2
  27. package/src/commands/errors.test.ts +3 -2
  28. package/src/commands/feed.test.ts +3 -2
  29. package/src/commands/feed.ts +2 -29
  30. package/src/commands/init.test.ts +1 -2
  31. package/src/commands/init.ts +1 -8
  32. package/src/commands/inspect.test.ts +17 -2
  33. package/src/commands/log.test.ts +262 -8
  34. package/src/commands/log.ts +232 -110
  35. package/src/commands/logs.test.ts +3 -2
  36. package/src/commands/mail.test.ts +8 -2
  37. package/src/commands/metrics.test.ts +4 -3
  38. package/src/commands/monitor.ts +15 -11
  39. package/src/commands/nudge.test.ts +4 -2
  40. package/src/commands/prime.test.ts +4 -2
  41. package/src/commands/prime.ts +6 -2
  42. package/src/commands/replay.test.ts +3 -2
  43. package/src/commands/run.test.ts +3 -1
  44. package/src/commands/sling.test.ts +142 -1
  45. package/src/commands/sling.ts +145 -24
  46. package/src/commands/status.test.ts +9 -8
  47. package/src/commands/stop.test.ts +1 -0
  48. package/src/commands/supervisor.ts +19 -12
  49. package/src/commands/trace.test.ts +4 -2
  50. package/src/commands/watch.test.ts +3 -2
  51. package/src/commands/worktree.test.ts +9 -0
  52. package/src/config.test.ts +3 -3
  53. package/src/config.ts +29 -0
  54. package/src/doctor/agents.test.ts +3 -2
  55. package/src/doctor/consistency.test.ts +14 -0
  56. package/src/doctor/logs.test.ts +3 -2
  57. package/src/doctor/structure.test.ts +3 -2
  58. package/src/e2e/init-sling-lifecycle.test.ts +3 -5
  59. package/src/index.ts +3 -1
  60. package/src/logging/color.ts +1 -1
  61. package/src/logging/format.test.ts +110 -0
  62. package/src/logging/format.ts +42 -1
  63. package/src/logging/logger.test.ts +3 -2
  64. package/src/mail/broadcast.test.ts +1 -0
  65. package/src/mail/client.test.ts +3 -2
  66. package/src/mail/store.test.ts +3 -2
  67. package/src/merge/queue.test.ts +3 -2
  68. package/src/merge/resolver.test.ts +39 -0
  69. package/src/merge/resolver.ts +24 -5
  70. package/src/mulch/client.test.ts +63 -2
  71. package/src/mulch/client.ts +62 -1
  72. package/src/runtimes/claude.test.ts +5 -4
  73. package/src/runtimes/pi-guards.test.ts +457 -0
  74. package/src/runtimes/pi-guards.ts +349 -0
  75. package/src/runtimes/pi.test.ts +620 -0
  76. package/src/runtimes/pi.ts +244 -0
  77. package/src/runtimes/registry.test.ts +33 -0
  78. package/src/runtimes/registry.ts +15 -2
  79. package/src/runtimes/types.ts +63 -0
  80. package/src/schema-consistency.test.ts +5 -2
  81. package/src/sessions/compat.test.ts +3 -2
  82. package/src/sessions/compat.ts +1 -0
  83. package/src/sessions/store.test.ts +34 -2
  84. package/src/sessions/store.ts +37 -4
  85. package/src/test-helpers.ts +20 -1
  86. package/src/types.ts +17 -0
  87. package/src/watchdog/daemon.test.ts +11 -7
  88. package/src/watchdog/daemon.ts +1 -1
  89. package/src/watchdog/health.test.ts +1 -0
  90. package/src/watchdog/triage.test.ts +3 -2
  91. package/src/watchdog/triage.ts +14 -4
@@ -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
  }
@@ -331,6 +370,76 @@ export async function autoRecordExpertise(params: {
331
370
  return recordedDomains;
332
371
  }
333
372
 
373
+ interface AppliedRecordsData {
374
+ taskId: string | null;
375
+ agentName: string;
376
+ capability: string;
377
+ records: Array<{ id: string; domain: string }>;
378
+ }
379
+
380
+ /**
381
+ * Append outcome entries to the mulch records that were applied when this agent was spawned.
382
+ *
383
+ * At spawn time, sling.ts writes .overstory/agents/{name}/applied-records.json listing
384
+ * the mx-* IDs from the prime output. At session-end, this function reads that file,
385
+ * appends a "success" outcome to each record, and deletes the file.
386
+ *
387
+ * @returns Number of records successfully updated.
388
+ */
389
+ export async function appendOutcomeToAppliedRecords(params: {
390
+ mulchClient: MulchClient;
391
+ agentName: string;
392
+ capability: string;
393
+ taskId: string | null;
394
+ projectRoot: string;
395
+ }): Promise<number> {
396
+ const appliedRecordsPath = join(
397
+ params.projectRoot,
398
+ ".overstory",
399
+ "agents",
400
+ params.agentName,
401
+ "applied-records.json",
402
+ );
403
+ const appliedFile = Bun.file(appliedRecordsPath);
404
+ if (!(await appliedFile.exists())) return 0;
405
+
406
+ let data: AppliedRecordsData;
407
+ try {
408
+ data = JSON.parse(await appliedFile.text()) as AppliedRecordsData;
409
+ } catch {
410
+ return 0;
411
+ }
412
+
413
+ const { records } = data;
414
+ if (!records || records.length === 0) return 0;
415
+
416
+ const taskSuffix = params.taskId ? ` for task ${params.taskId}` : "";
417
+ const outcome = {
418
+ status: "success" as const,
419
+ agent: params.agentName,
420
+ notes: `Applied by ${params.capability} agent ${params.agentName}${taskSuffix}. Session completed.`,
421
+ };
422
+
423
+ let appended = 0;
424
+ for (const { id, domain } of records) {
425
+ try {
426
+ await params.mulchClient.appendOutcome(domain, id, outcome);
427
+ appended++;
428
+ } catch {
429
+ // Non-fatal per record
430
+ }
431
+ }
432
+
433
+ try {
434
+ const { unlink } = await import("node:fs/promises");
435
+ await unlink(appliedRecordsPath);
436
+ } catch {
437
+ // Non-fatal: file may already be gone
438
+ }
439
+
440
+ return appended;
441
+ }
442
+
334
443
  /**
335
444
  * Core implementation for the log command.
336
445
  */
@@ -388,29 +497,28 @@ async function runLog(opts: {
388
497
  logger.toolStart(toolName, toolInput ?? {});
389
498
  updateLastActivity(config.project.root, opts.agent);
390
499
 
391
- // When --stdin is used, also write to EventStore for structured observability
392
- if (opts.stdin) {
393
- try {
394
- const eventsDbPath = join(config.project.root, ".overstory", "events.db");
395
- const eventStore = createEventStore(eventsDbPath);
396
- const filtered = toolInput
397
- ? filterToolArgs(toolName, toolInput)
398
- : { args: {}, summary: toolName };
399
- eventStore.insert({
400
- runId: null,
401
- agentName: opts.agent,
402
- sessionId,
403
- eventType: "tool_start",
404
- toolName,
405
- toolArgs: JSON.stringify(filtered.args),
406
- toolDurationMs: null,
407
- level: "info",
408
- data: JSON.stringify({ summary: filtered.summary }),
409
- });
410
- eventStore.close();
411
- } catch {
412
- // Non-fatal: EventStore write should not break hook execution
413
- }
500
+ // Always write to EventStore for structured observability
501
+ // (works for both Claude Code --stdin and Pi runtime --tool-name agents)
502
+ try {
503
+ const eventsDbPath = join(config.project.root, ".overstory", "events.db");
504
+ const eventStore = createEventStore(eventsDbPath);
505
+ const filtered = toolInput
506
+ ? filterToolArgs(toolName, toolInput)
507
+ : { args: {}, summary: toolName };
508
+ eventStore.insert({
509
+ runId: null,
510
+ agentName: opts.agent,
511
+ sessionId,
512
+ eventType: "tool_start",
513
+ toolName,
514
+ toolArgs: JSON.stringify(filtered.args),
515
+ toolDurationMs: null,
516
+ level: "info",
517
+ data: JSON.stringify({ summary: filtered.summary }),
518
+ });
519
+ eventStore.close();
520
+ } catch {
521
+ // Non-fatal: EventStore write should not break hook execution
414
522
  }
415
523
  break;
416
524
  }
@@ -419,79 +527,78 @@ async function runLog(opts: {
419
527
  logger.toolEnd(toolName, 0);
420
528
  updateLastActivity(config.project.root, opts.agent);
421
529
 
422
- // When --stdin is used, write to EventStore and correlate with tool-start
423
- if (opts.stdin) {
424
- try {
425
- const eventsDbPath = join(config.project.root, ".overstory", "events.db");
426
- const eventStore = createEventStore(eventsDbPath);
427
- const filtered = toolInput
428
- ? filterToolArgs(toolName, toolInput)
429
- : { args: {}, summary: toolName };
430
- eventStore.insert({
431
- runId: null,
432
- agentName: opts.agent,
433
- sessionId,
434
- eventType: "tool_end",
435
- toolName,
436
- toolArgs: JSON.stringify(filtered.args),
437
- toolDurationMs: null,
438
- level: "info",
439
- data: JSON.stringify({ summary: filtered.summary }),
440
- });
441
- const correlation = eventStore.correlateToolEnd(opts.agent, toolName);
442
- if (correlation) {
443
- logger.toolEnd(toolName, correlation.durationMs);
444
- }
445
- eventStore.close();
446
- } catch {
447
- // Non-fatal: EventStore write should not break hook execution
530
+ // Always write to EventStore for structured observability
531
+ // (works for both Claude Code --stdin and Pi runtime --tool-name agents)
532
+ try {
533
+ const eventsDbPath = join(config.project.root, ".overstory", "events.db");
534
+ const eventStore = createEventStore(eventsDbPath);
535
+ const filtered = toolInput
536
+ ? filterToolArgs(toolName, toolInput)
537
+ : { args: {}, summary: toolName };
538
+ eventStore.insert({
539
+ runId: null,
540
+ agentName: opts.agent,
541
+ sessionId,
542
+ eventType: "tool_end",
543
+ toolName,
544
+ toolArgs: JSON.stringify(filtered.args),
545
+ toolDurationMs: null,
546
+ level: "info",
547
+ data: JSON.stringify({ summary: filtered.summary }),
548
+ });
549
+ const correlation = eventStore.correlateToolEnd(opts.agent, toolName);
550
+ if (correlation) {
551
+ logger.toolEnd(toolName, correlation.durationMs);
448
552
  }
553
+ eventStore.close();
554
+ } catch {
555
+ // Non-fatal: EventStore write should not break hook execution
556
+ }
449
557
 
450
- // Throttled token snapshot recording
451
- if (sessionId) {
452
- try {
453
- // Throttle check
454
- const snapshotMarkerPath = join(logsBase, opts.agent, ".last-snapshot");
455
- const SNAPSHOT_INTERVAL_MS = 30_000;
456
- const snapshotMarkerFile = Bun.file(snapshotMarkerPath);
457
- let shouldSnapshot = true;
458
-
459
- if (await snapshotMarkerFile.exists()) {
460
- const lastTs = Number.parseInt(await snapshotMarkerFile.text(), 10);
461
- if (!Number.isNaN(lastTs) && Date.now() - lastTs < SNAPSHOT_INTERVAL_MS) {
462
- shouldSnapshot = false;
463
- }
558
+ // Throttled token snapshot recording (requires sessionId from --stdin; skipped for Pi agents)
559
+ if (sessionId) {
560
+ try {
561
+ // Throttle check
562
+ const snapshotMarkerPath = join(logsBase, opts.agent, ".last-snapshot");
563
+ const SNAPSHOT_INTERVAL_MS = 30_000;
564
+ const snapshotMarkerFile = Bun.file(snapshotMarkerPath);
565
+ let shouldSnapshot = true;
566
+
567
+ if (await snapshotMarkerFile.exists()) {
568
+ const lastTs = Number.parseInt(await snapshotMarkerFile.text(), 10);
569
+ if (!Number.isNaN(lastTs) && Date.now() - lastTs < SNAPSHOT_INTERVAL_MS) {
570
+ shouldSnapshot = false;
464
571
  }
572
+ }
465
573
 
466
- if (shouldSnapshot) {
467
- const resolvedTranscriptPath = await resolveTranscriptPath(
468
- config.project.root,
469
- sessionId,
470
- logsBase,
471
- opts.agent,
472
- );
473
- if (resolvedTranscriptPath) {
474
- const usage = await parseTranscriptUsage(resolvedTranscriptPath);
475
- const cost = estimateCost(usage);
476
- const metricsDbPath = join(config.project.root, ".overstory", "metrics.db");
477
- const metricsStore = createMetricsStore(metricsDbPath);
478
- metricsStore.recordSnapshot({
479
- agentName: opts.agent,
480
- inputTokens: usage.inputTokens,
481
- outputTokens: usage.outputTokens,
482
- cacheReadTokens: usage.cacheReadTokens,
483
- cacheCreationTokens: usage.cacheCreationTokens,
484
- estimatedCostUsd: cost,
485
- modelUsed: usage.modelUsed,
486
- createdAt: new Date().toISOString(),
487
- });
488
- metricsStore.close();
489
- await Bun.write(snapshotMarkerPath, String(Date.now()));
490
- }
574
+ if (shouldSnapshot) {
575
+ const resolvedTranscriptPath = await resolveTranscriptPath(
576
+ config.project.root,
577
+ sessionId,
578
+ logsBase,
579
+ opts.agent,
580
+ );
581
+ if (resolvedTranscriptPath) {
582
+ const usage = await parseTranscriptUsage(resolvedTranscriptPath);
583
+ const cost = estimateCost(usage);
584
+ const metricsDbPath = join(config.project.root, ".overstory", "metrics.db");
585
+ const metricsStore = createMetricsStore(metricsDbPath);
586
+ metricsStore.recordSnapshot({
587
+ agentName: opts.agent,
588
+ inputTokens: usage.inputTokens,
589
+ outputTokens: usage.outputTokens,
590
+ cacheReadTokens: usage.cacheReadTokens,
591
+ cacheCreationTokens: usage.cacheCreationTokens,
592
+ estimatedCostUsd: cost,
593
+ modelUsed: usage.modelUsed,
594
+ createdAt: new Date().toISOString(),
595
+ });
596
+ metricsStore.close();
597
+ await Bun.write(snapshotMarkerPath, String(Date.now()));
491
598
  }
492
- } catch {
493
- // Non-fatal: snapshot recording should not break tool-end handling
494
599
  }
600
+ } catch {
601
+ // Non-fatal: snapshot recording should not break tool-end handling
495
602
  }
496
603
  }
497
604
  break;
@@ -642,29 +749,44 @@ async function runLog(opts: {
642
749
  // Non-fatal: mulch learn/record should not break session-end handling
643
750
  }
644
751
  }
645
- }
646
752
 
647
- // Write session-end event to EventStore when --stdin is used
648
- if (opts.stdin) {
649
- try {
650
- const eventsDbPath = join(config.project.root, ".overstory", "events.db");
651
- const eventStore = createEventStore(eventsDbPath);
652
- eventStore.insert({
653
- runId: null,
654
- agentName: opts.agent,
655
- sessionId,
656
- eventType: "session_end",
657
- toolName: null,
658
- toolArgs: null,
659
- toolDurationMs: null,
660
- level: "info",
661
- data: transcriptPath ? JSON.stringify({ transcriptPath }) : null,
662
- });
663
- eventStore.close();
664
- } catch {
665
- // Non-fatal: EventStore write should not break session-end
753
+ // Append outcomes to applied mulch records (outcome feedback loop).
754
+ // Reads applied-records.json written by sling.ts at spawn time.
755
+ if (!PERSISTENT_CAPABILITIES.has(agentSession.capability)) {
756
+ try {
757
+ const mulchClient = createMulchClient(config.project.root);
758
+ await appendOutcomeToAppliedRecords({
759
+ mulchClient,
760
+ agentName: opts.agent,
761
+ capability: agentSession.capability,
762
+ taskId,
763
+ projectRoot: config.project.root,
764
+ });
765
+ } catch {
766
+ // Non-fatal
767
+ }
666
768
  }
667
769
  }
770
+
771
+ // Always write session-end event to EventStore (not just when --stdin is used)
772
+ try {
773
+ const eventsDbPath = join(config.project.root, ".overstory", "events.db");
774
+ const eventStore = createEventStore(eventsDbPath);
775
+ eventStore.insert({
776
+ runId: null,
777
+ agentName: opts.agent,
778
+ sessionId,
779
+ eventType: "session_end",
780
+ toolName: null,
781
+ toolArgs: null,
782
+ toolDurationMs: null,
783
+ level: "info",
784
+ data: transcriptPath ? JSON.stringify({ transcriptPath }) : null,
785
+ });
786
+ eventStore.close();
787
+ } catch {
788
+ // Non-fatal: EventStore write should not break session-end
789
+ }
668
790
  }
669
791
  // Clear the current session marker
670
792
  {
@@ -1,8 +1,9 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdir, rm, writeFile } from "node:fs/promises";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { ValidationError } from "../errors.ts";
6
+ import { cleanupTempDir } from "../test-helpers.ts";
6
7
  import type { LogEvent } from "../types.ts";
7
8
  import { logsCommand } from "./logs.ts";
8
9
 
@@ -51,7 +52,7 @@ describe("logsCommand", () => {
51
52
 
52
53
  // Clean up temp directory
53
54
  try {
54
- await rm(tmpDir, { recursive: true, force: true });
55
+ await cleanupTempDir(tmpDir);
55
56
  } catch {
56
57
  // Ignore cleanup errors
57
58
  }
@@ -6,13 +6,14 @@
6
6
  */
7
7
 
8
8
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
- import { mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
9
+ import { mkdir, mkdtemp, readdir } from "node:fs/promises";
10
10
  import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { createEventStore } from "../events/store.ts";
13
13
  import { stripAnsi } from "../logging/color.ts";
14
14
  import { createMailClient } from "../mail/client.ts";
15
15
  import { createMailStore } from "../mail/store.ts";
16
+ import { cleanupTempDir } from "../test-helpers.ts";
16
17
  import type { StoredEvent } from "../types.ts";
17
18
  import { AUTO_NUDGE_TYPES, isDispatchNudge, mailCommand, shouldAutoNudge } from "./mail.ts";
18
19
 
@@ -70,7 +71,7 @@ describe("mailCommand", () => {
70
71
  process.stdout.write = origWrite;
71
72
  process.stderr.write = origStderrWrite;
72
73
  process.chdir(origCwd);
73
- await rm(tempDir, { recursive: true, force: true });
74
+ await cleanupTempDir(tempDir);
74
75
  });
75
76
 
76
77
  describe("list", () => {
@@ -773,6 +774,7 @@ describe("mailCommand", () => {
773
774
  lastActivity: new Date().toISOString(),
774
775
  escalationLevel: 0,
775
776
  stalledSince: null,
777
+ transcriptPath: null,
776
778
  },
777
779
  {
778
780
  id: "session-builder-1",
@@ -791,6 +793,7 @@ describe("mailCommand", () => {
791
793
  lastActivity: new Date().toISOString(),
792
794
  escalationLevel: 0,
793
795
  stalledSince: null,
796
+ transcriptPath: null,
794
797
  },
795
798
  {
796
799
  id: "session-builder-2",
@@ -809,6 +812,7 @@ describe("mailCommand", () => {
809
812
  lastActivity: new Date().toISOString(),
810
813
  escalationLevel: 0,
811
814
  stalledSince: null,
815
+ transcriptPath: null,
812
816
  },
813
817
  {
814
818
  id: "session-scout-1",
@@ -827,6 +831,7 @@ describe("mailCommand", () => {
827
831
  lastActivity: new Date().toISOString(),
828
832
  escalationLevel: 0,
829
833
  stalledSince: null,
834
+ transcriptPath: null,
830
835
  },
831
836
  ];
832
837
 
@@ -1147,6 +1152,7 @@ describe("mailCommand", () => {
1147
1152
  lastActivity: new Date().toISOString(),
1148
1153
  escalationLevel: 0,
1149
1154
  stalledSince: null,
1155
+ transcriptPath: null,
1150
1156
  });
1151
1157
  }
1152
1158
 
@@ -1,8 +1,9 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdtemp, rm } from "node:fs/promises";
2
+ import { mkdtemp } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { createMetricsStore } from "../metrics/store.ts";
6
+ import { cleanupTempDir } from "../test-helpers.ts";
6
7
  import type { SessionMetrics } from "../types.ts";
7
8
  import { metricsCommand } from "./metrics.ts";
8
9
 
@@ -44,7 +45,7 @@ describe("metricsCommand", () => {
44
45
  afterEach(async () => {
45
46
  process.stdout.write = originalWrite;
46
47
  process.chdir(originalCwd);
47
- await rm(tempDir, { recursive: true, force: true });
48
+ await cleanupTempDir(tempDir);
48
49
  });
49
50
 
50
51
  function output(): string {
@@ -366,7 +367,7 @@ describe("formatDuration helper", () => {
366
367
  afterEach(async () => {
367
368
  process.stdout.write = originalWrite;
368
369
  process.chdir(originalCwd);
369
- await rm(tempDir, { recursive: true, force: true });
370
+ await cleanupTempDir(tempDir);
370
371
  });
371
372
 
372
373
  function output(): string {
@@ -16,7 +16,6 @@
16
16
  import { mkdir } from "node:fs/promises";
17
17
  import { join } from "node:path";
18
18
  import { Command } from "commander";
19
- import { deployHooks } from "../agents/hooks-deployer.ts";
20
19
  import { createIdentity, loadIdentity } from "../agents/identity.ts";
21
20
  import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
22
21
  import { loadConfig } from "../config.ts";
@@ -111,8 +110,21 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
111
110
  store.updateState(MONITOR_NAME, "completed");
112
111
  }
113
112
 
113
+ // Resolve model and runtime early (needed for deployConfig and spawn)
114
+ const manifestLoader = createManifestLoader(
115
+ join(projectRoot, config.agents.manifestPath),
116
+ join(projectRoot, config.agents.baseDir),
117
+ );
118
+ const manifest = await manifestLoader.load();
119
+ const resolvedModel = resolveModel(config, manifest, "monitor", "sonnet");
120
+ const runtime = getRuntime(undefined, config);
121
+
114
122
  // Deploy monitor-specific hooks to the project root's .claude/ directory.
115
- await deployHooks(projectRoot, MONITOR_NAME, "monitor");
123
+ await runtime.deployConfig(projectRoot, undefined, {
124
+ agentName: MONITOR_NAME,
125
+ capability: "monitor",
126
+ worktreePath: projectRoot,
127
+ });
116
128
 
117
129
  // Create monitor identity if first run
118
130
  const identityBaseDir = join(projectRoot, ".overstory", "agents");
@@ -129,15 +141,6 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
129
141
  });
130
142
  }
131
143
 
132
- // Resolve model from config > manifest > fallback
133
- const manifestLoader = createManifestLoader(
134
- join(projectRoot, config.agents.manifestPath),
135
- join(projectRoot, config.agents.baseDir),
136
- );
137
- const manifest = await manifestLoader.load();
138
- const resolvedModel = resolveModel(config, manifest, "monitor", "sonnet");
139
- const runtime = getRuntime(undefined, config);
140
-
141
144
  // Spawn tmux session at project root with Claude Code (interactive mode).
142
145
  const agentDefPath = join(projectRoot, ".overstory", "agent-defs", "monitor.md");
143
146
  const agentDefFile = Bun.file(agentDefPath);
@@ -179,6 +182,7 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
179
182
  lastActivity: new Date().toISOString(),
180
183
  escalationLevel: 0,
181
184
  stalledSince: null,
185
+ transcriptPath: null,
182
186
  };
183
187
 
184
188
  store.upsert(session);
@@ -1,10 +1,11 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { mkdirSync } from "node:fs";
3
- import { mkdtemp, rm } from "node:fs/promises";
3
+ import { mkdtemp } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { createEventStore } from "../events/store.ts";
7
7
  import { createSessionStore } from "../sessions/store.ts";
8
+ import { cleanupTempDir } from "../test-helpers.ts";
8
9
  import type { AgentSession, StoredEvent } from "../types.ts";
9
10
 
10
11
  /**
@@ -22,7 +23,7 @@ beforeEach(async () => {
22
23
  });
23
24
 
24
25
  afterEach(async () => {
25
- await rm(tempDir, { recursive: true, force: true });
26
+ await cleanupTempDir(tempDir);
26
27
  });
27
28
 
28
29
  /**
@@ -57,6 +58,7 @@ function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
57
58
  lastActivity: new Date().toISOString(),
58
59
  escalationLevel: 0,
59
60
  stalledSince: null,
61
+ transcriptPath: null,
60
62
  ...overrides,
61
63
  };
62
64
  }
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm } from "node:fs/promises";
2
+ import { mkdir, mkdtemp } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { cleanupTempDir, createTempGitRepo } from "../test-helpers.ts";
@@ -55,7 +55,7 @@ describe("primeCommand", () => {
55
55
  process.stdout.write = originalWrite;
56
56
  process.stderr.write = originalStderrWrite;
57
57
  process.chdir(originalCwd);
58
- await rm(tempDir, { recursive: true, force: true });
58
+ await cleanupTempDir(tempDir);
59
59
  });
60
60
 
61
61
  function output(): string {
@@ -167,6 +167,7 @@ recentTasks:
167
167
  lastActivity: new Date().toISOString(),
168
168
  escalationLevel: 0,
169
169
  stalledSince: null,
170
+ transcriptPath: null,
170
171
  },
171
172
  ];
172
173
 
@@ -204,6 +205,7 @@ recentTasks:
204
205
  lastActivity: new Date().toISOString(),
205
206
  escalationLevel: 0,
206
207
  stalledSince: null,
208
+ transcriptPath: null,
207
209
  },
208
210
  ];
209
211
 
@@ -38,6 +38,8 @@ const OVERSTORY_GITIGNORE = `# Wildcard+whitelist: ignore everything, whitelist
38
38
  export interface PrimeOptions {
39
39
  agent?: string;
40
40
  compact?: boolean;
41
+ /** Override the instruction path referenced in agent activation context. Defaults to ".claude/CLAUDE.md". */
42
+ instructionPath?: string;
41
43
  }
42
44
 
43
45
  /**
@@ -138,6 +140,7 @@ async function healGitignore(overstoryDir: string): Promise<void> {
138
140
  export async function primeCommand(opts: PrimeOptions): Promise<void> {
139
141
  const agentName = opts.agent ?? null;
140
142
  const compact = opts.compact ?? false;
143
+ const instructionPath = opts.instructionPath ?? ".claude/CLAUDE.md";
141
144
 
142
145
  // 1. Load config
143
146
  const config = await loadConfig(process.cwd());
@@ -161,7 +164,7 @@ export async function primeCommand(opts: PrimeOptions): Promise<void> {
161
164
  // 4. Output context (orchestrator or agent)
162
165
  if (agentName !== null) {
163
166
  // === Agent priming ===
164
- await outputAgentContext(config, agentName, compact, expertiseOutput);
167
+ await outputAgentContext(config, agentName, compact, expertiseOutput, instructionPath);
165
168
  } else {
166
169
  // === Orchestrator priming ===
167
170
  await outputOrchestratorContext(config, compact, expertiseOutput);
@@ -176,6 +179,7 @@ async function outputAgentContext(
176
179
  agentName: string,
177
180
  compact: boolean,
178
181
  expertiseOutput: string | null,
182
+ instructionPath: string,
179
183
  ): Promise<void> {
180
184
  const sections: string[] = [];
181
185
 
@@ -226,7 +230,7 @@ async function outputAgentContext(
226
230
  if (boundSession) {
227
231
  sections.push("\n## Activation");
228
232
  sections.push(`You have a bound task: **${boundSession.taskId}**`);
229
- sections.push("Read your overlay at `.claude/CLAUDE.md` and begin working immediately.");
233
+ sections.push(`Read your overlay at \`${instructionPath}\` and begin working immediately.`);
230
234
  sections.push("Do not wait for dispatch mail. Your assignment was bound at spawn time.");
231
235
  }
232
236