@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.
- package/README.md +7 -6
- package/agents/builder.md +1 -1
- package/agents/coordinator.md +12 -11
- package/agents/lead.md +6 -6
- package/agents/monitor.md +4 -4
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +5 -5
- package/agents/supervisor.md +36 -32
- package/package.json +1 -1
- package/src/agents/guard-rules.ts +97 -0
- package/src/agents/hooks-deployer.test.ts +6 -5
- package/src/agents/hooks-deployer.ts +7 -90
- package/src/agents/identity.test.ts +3 -2
- package/src/agents/manifest.test.ts +4 -3
- package/src/agents/overlay.test.ts +10 -9
- package/src/agents/overlay.ts +5 -5
- package/src/commands/agents.test.ts +10 -4
- package/src/commands/clean.test.ts +3 -0
- package/src/commands/completions.test.ts +8 -5
- package/src/commands/completions.ts +38 -2
- package/src/commands/coordinator.test.ts +1 -0
- package/src/commands/coordinator.ts +15 -11
- package/src/commands/costs.test.ts +9 -3
- package/src/commands/dashboard.test.ts +265 -6
- package/src/commands/dashboard.ts +367 -64
- package/src/commands/doctor.test.ts +3 -2
- package/src/commands/errors.test.ts +3 -2
- package/src/commands/feed.test.ts +3 -2
- package/src/commands/feed.ts +2 -29
- package/src/commands/init.test.ts +1 -2
- package/src/commands/init.ts +1 -8
- package/src/commands/inspect.test.ts +17 -2
- package/src/commands/log.test.ts +262 -8
- package/src/commands/log.ts +232 -110
- package/src/commands/logs.test.ts +3 -2
- package/src/commands/mail.test.ts +8 -2
- package/src/commands/metrics.test.ts +4 -3
- package/src/commands/monitor.ts +15 -11
- package/src/commands/nudge.test.ts +4 -2
- package/src/commands/prime.test.ts +4 -2
- package/src/commands/prime.ts +6 -2
- package/src/commands/replay.test.ts +3 -2
- package/src/commands/run.test.ts +3 -1
- package/src/commands/sling.test.ts +142 -1
- package/src/commands/sling.ts +145 -24
- package/src/commands/status.test.ts +9 -8
- package/src/commands/stop.test.ts +1 -0
- package/src/commands/supervisor.ts +19 -12
- package/src/commands/trace.test.ts +4 -2
- package/src/commands/watch.test.ts +3 -2
- package/src/commands/worktree.test.ts +9 -0
- package/src/config.test.ts +3 -3
- package/src/config.ts +29 -0
- package/src/doctor/agents.test.ts +3 -2
- package/src/doctor/consistency.test.ts +14 -0
- package/src/doctor/logs.test.ts +3 -2
- package/src/doctor/structure.test.ts +3 -2
- package/src/e2e/init-sling-lifecycle.test.ts +3 -5
- package/src/index.ts +3 -1
- package/src/logging/color.ts +1 -1
- package/src/logging/format.test.ts +110 -0
- package/src/logging/format.ts +42 -1
- package/src/logging/logger.test.ts +3 -2
- package/src/mail/broadcast.test.ts +1 -0
- package/src/mail/client.test.ts +3 -2
- package/src/mail/store.test.ts +3 -2
- package/src/merge/queue.test.ts +3 -2
- package/src/merge/resolver.test.ts +39 -0
- package/src/merge/resolver.ts +24 -5
- package/src/mulch/client.test.ts +63 -2
- package/src/mulch/client.ts +62 -1
- package/src/runtimes/claude.test.ts +5 -4
- package/src/runtimes/pi-guards.test.ts +457 -0
- package/src/runtimes/pi-guards.ts +349 -0
- package/src/runtimes/pi.test.ts +620 -0
- package/src/runtimes/pi.ts +244 -0
- package/src/runtimes/registry.test.ts +33 -0
- package/src/runtimes/registry.ts +15 -2
- package/src/runtimes/types.ts +63 -0
- package/src/schema-consistency.test.ts +5 -2
- package/src/sessions/compat.test.ts +3 -2
- package/src/sessions/compat.ts +1 -0
- package/src/sessions/store.test.ts +34 -2
- package/src/sessions/store.ts +37 -4
- package/src/test-helpers.ts +20 -1
- package/src/types.ts +17 -0
- package/src/watchdog/daemon.test.ts +11 -7
- package/src/watchdog/daemon.ts +1 -1
- package/src/watchdog/health.test.ts +1 -0
- package/src/watchdog/triage.test.ts +3 -2
- package/src/watchdog/triage.ts +14 -4
package/src/commands/log.ts
CHANGED
|
@@ -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
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
370
|
+
await cleanupTempDir(tempDir);
|
|
370
371
|
});
|
|
371
372
|
|
|
372
373
|
function output(): string {
|
package/src/commands/monitor.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/src/commands/prime.ts
CHANGED
|
@@ -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(
|
|
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
|
|