@os-eco/overstory-cli 0.7.2 → 0.7.4
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 +21 -9
- package/agents/builder.md +6 -0
- package/agents/coordinator.md +2 -2
- package/agents/lead.md +4 -1
- package/agents/merger.md +3 -2
- package/agents/monitor.md +1 -1
- package/agents/reviewer.md +1 -0
- package/agents/scout.md +1 -0
- package/package.json +2 -2
- package/src/agents/hooks-deployer.test.ts +6 -5
- package/src/agents/identity.test.ts +3 -2
- package/src/agents/manifest.test.ts +4 -3
- package/src/agents/overlay.test.ts +3 -2
- package/src/commands/agents.test.ts +5 -4
- package/src/commands/agents.ts +18 -8
- package/src/commands/completions.test.ts +8 -5
- package/src/commands/completions.ts +37 -1
- package/src/commands/costs.test.ts +4 -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/inspect.test.ts +3 -2
- package/src/commands/log.test.ts +248 -8
- package/src/commands/log.ts +193 -110
- package/src/commands/logs.test.ts +3 -2
- package/src/commands/mail.test.ts +3 -2
- package/src/commands/metrics.test.ts +4 -3
- package/src/commands/nudge.test.ts +3 -2
- package/src/commands/prime.test.ts +3 -2
- package/src/commands/prime.ts +1 -16
- package/src/commands/replay.test.ts +3 -2
- package/src/commands/run.test.ts +2 -1
- package/src/commands/sling.test.ts +127 -0
- package/src/commands/sling.ts +101 -3
- package/src/commands/status.test.ts +8 -8
- package/src/commands/trace.test.ts +3 -2
- package/src/commands/watch.test.ts +3 -2
- package/src/config.test.ts +3 -3
- package/src/doctor/agents.test.ts +3 -2
- package/src/doctor/logs.test.ts +3 -2
- package/src/doctor/structure.test.ts +3 -2
- 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/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 +1 -1
- package/src/metrics/pricing.ts +80 -0
- package/src/metrics/transcript.test.ts +58 -1
- package/src/metrics/transcript.ts +9 -68
- package/src/mulch/client.test.ts +63 -2
- package/src/mulch/client.ts +62 -1
- package/src/runtimes/claude.test.ts +4 -3
- package/src/runtimes/pi-guards.test.ts +55 -2
- package/src/runtimes/pi-guards.ts +26 -9
- package/src/schema-consistency.test.ts +4 -2
- package/src/sessions/compat.test.ts +3 -2
- package/src/sessions/store.test.ts +3 -2
- package/src/test-helpers.ts +20 -1
- package/src/tracker/beads.test.ts +454 -0
- package/src/tracker/seeds.test.ts +461 -0
- package/src/watchdog/daemon.test.ts +4 -3
- package/src/watchdog/triage.test.ts +3 -2
package/src/commands/log.ts
CHANGED
|
@@ -370,6 +370,76 @@ export async function autoRecordExpertise(params: {
|
|
|
370
370
|
return recordedDomains;
|
|
371
371
|
}
|
|
372
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
|
+
|
|
373
443
|
/**
|
|
374
444
|
* Core implementation for the log command.
|
|
375
445
|
*/
|
|
@@ -427,29 +497,28 @@ async function runLog(opts: {
|
|
|
427
497
|
logger.toolStart(toolName, toolInput ?? {});
|
|
428
498
|
updateLastActivity(config.project.root, opts.agent);
|
|
429
499
|
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
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
|
|
453
522
|
}
|
|
454
523
|
break;
|
|
455
524
|
}
|
|
@@ -458,79 +527,78 @@ async function runLog(opts: {
|
|
|
458
527
|
logger.toolEnd(toolName, 0);
|
|
459
528
|
updateLastActivity(config.project.root, opts.agent);
|
|
460
529
|
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
eventStore.close();
|
|
485
|
-
} catch {
|
|
486
|
-
// 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);
|
|
487
552
|
}
|
|
553
|
+
eventStore.close();
|
|
554
|
+
} catch {
|
|
555
|
+
// Non-fatal: EventStore write should not break hook execution
|
|
556
|
+
}
|
|
488
557
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
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;
|
|
503
571
|
}
|
|
572
|
+
}
|
|
504
573
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
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()));
|
|
530
598
|
}
|
|
531
|
-
} catch {
|
|
532
|
-
// Non-fatal: snapshot recording should not break tool-end handling
|
|
533
599
|
}
|
|
600
|
+
} catch {
|
|
601
|
+
// Non-fatal: snapshot recording should not break tool-end handling
|
|
534
602
|
}
|
|
535
603
|
}
|
|
536
604
|
break;
|
|
@@ -681,29 +749,44 @@ async function runLog(opts: {
|
|
|
681
749
|
// Non-fatal: mulch learn/record should not break session-end handling
|
|
682
750
|
}
|
|
683
751
|
}
|
|
684
|
-
}
|
|
685
752
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
});
|
|
702
|
-
eventStore.close();
|
|
703
|
-
} catch {
|
|
704
|
-
// 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
|
+
}
|
|
705
768
|
}
|
|
706
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
|
+
}
|
|
707
790
|
}
|
|
708
791
|
// Clear the current session marker
|
|
709
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", () => {
|
|
@@ -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 {
|
|
@@ -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
|
/**
|
|
@@ -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 {
|
|
@@ -366,6 +366,7 @@ recentTasks: []
|
|
|
366
366
|
!hooks.json
|
|
367
367
|
!groups.json
|
|
368
368
|
!agent-defs/
|
|
369
|
+
!README.md
|
|
369
370
|
`;
|
|
370
371
|
|
|
371
372
|
test("creates .overstory/.gitignore if missing", async () => {
|
package/src/commands/prime.ts
CHANGED
|
@@ -18,22 +18,7 @@ import { createMulchClient } from "../mulch/client.ts";
|
|
|
18
18
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
19
19
|
import type { AgentIdentity, AgentManifest, SessionCheckpoint, SessionMetrics } from "../types.ts";
|
|
20
20
|
import { getCurrentSessionName } from "../worktree/tmux.ts";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Gitignore content for .overstory/.gitignore.
|
|
24
|
-
* TODO: Import from init.ts once it's exported (parallel branch change).
|
|
25
|
-
* Wildcard+whitelist pattern: ignore everything except tracked config files.
|
|
26
|
-
*/
|
|
27
|
-
const OVERSTORY_GITIGNORE = `# Wildcard+whitelist: ignore everything, whitelist tracked files
|
|
28
|
-
# Auto-healed by ov prime on each session start
|
|
29
|
-
*
|
|
30
|
-
!.gitignore
|
|
31
|
-
!config.yaml
|
|
32
|
-
!agent-manifest.json
|
|
33
|
-
!hooks.json
|
|
34
|
-
!groups.json
|
|
35
|
-
!agent-defs/
|
|
36
|
-
`;
|
|
21
|
+
import { OVERSTORY_GITIGNORE } from "./init.ts";
|
|
37
22
|
|
|
38
23
|
export interface PrimeOptions {
|
|
39
24
|
agent?: string;
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
-
import { mkdtemp
|
|
12
|
+
import { mkdtemp } from "node:fs/promises";
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { ValidationError } from "../errors.ts";
|
|
16
16
|
import { createEventStore } from "../events/store.ts";
|
|
17
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
17
18
|
import type { InsertEvent } from "../types.ts";
|
|
18
19
|
import { replayCommand } from "./replay.ts";
|
|
19
20
|
|
|
@@ -64,7 +65,7 @@ describe("replayCommand", () => {
|
|
|
64
65
|
afterEach(async () => {
|
|
65
66
|
process.stdout.write = originalWrite;
|
|
66
67
|
process.chdir(originalCwd);
|
|
67
|
-
await
|
|
68
|
+
await cleanupTempDir(tempDir);
|
|
68
69
|
});
|
|
69
70
|
|
|
70
71
|
function output(): string {
|
package/src/commands/run.test.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { tmpdir } from "node:os";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import type { SessionStore } from "../sessions/store.ts";
|
|
13
13
|
import { createRunStore, createSessionStore } from "../sessions/store.ts";
|
|
14
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
14
15
|
import type { AgentSession, InsertRun, RunStore } from "../types.ts";
|
|
15
16
|
|
|
16
17
|
let tempDir: string;
|
|
@@ -31,7 +32,7 @@ beforeEach(async () => {
|
|
|
31
32
|
afterEach(async () => {
|
|
32
33
|
runStore.close();
|
|
33
34
|
sessionStore.close();
|
|
34
|
-
await
|
|
35
|
+
await cleanupTempDir(tempDir);
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
/** Write a run ID to current-run.txt. */
|
|
@@ -14,9 +14,11 @@ import {
|
|
|
14
14
|
checkParentAgentLimit,
|
|
15
15
|
checkRunSessionLimit,
|
|
16
16
|
checkTaskLock,
|
|
17
|
+
extractMulchRecordIds,
|
|
17
18
|
inferDomainsFromFiles,
|
|
18
19
|
isRunningAsRoot,
|
|
19
20
|
parentHasScouts,
|
|
21
|
+
shouldShowScoutWarning,
|
|
20
22
|
validateHierarchy,
|
|
21
23
|
} from "./sling.ts";
|
|
22
24
|
|
|
@@ -275,6 +277,65 @@ describe("parentHasScouts", () => {
|
|
|
275
277
|
});
|
|
276
278
|
});
|
|
277
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Tests for shouldShowScoutWarning (overstory-6eyw).
|
|
282
|
+
*
|
|
283
|
+
* shouldShowScoutWarning determines whether the "spawning builder without scouts"
|
|
284
|
+
* warning should be emitted. It is a pure function extracted from slingCommand
|
|
285
|
+
* so it can be suppressed via --no-scout-check or --skip-scout.
|
|
286
|
+
*/
|
|
287
|
+
|
|
288
|
+
describe("shouldShowScoutWarning", () => {
|
|
289
|
+
function makeSession(
|
|
290
|
+
parentAgent: string | null,
|
|
291
|
+
capability: string,
|
|
292
|
+
): { parentAgent: string | null; capability: string } {
|
|
293
|
+
return { parentAgent, capability };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const withScout = [makeSession("lead-alpha", "scout"), makeSession("lead-alpha", "builder")];
|
|
297
|
+
const withoutScout = [makeSession("lead-alpha", "builder")];
|
|
298
|
+
const empty: { parentAgent: string | null; capability: string }[] = [];
|
|
299
|
+
|
|
300
|
+
test("returns true when builder has parent but no scouts", () => {
|
|
301
|
+
expect(shouldShowScoutWarning("builder", "lead-alpha", withoutScout, false, false)).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("returns false when builder has parent and scouts exist", () => {
|
|
305
|
+
expect(shouldShowScoutWarning("builder", "lead-alpha", withScout, false, false)).toBe(false);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("returns false when capability is not builder", () => {
|
|
309
|
+
expect(shouldShowScoutWarning("scout", "lead-alpha", empty, false, false)).toBe(false);
|
|
310
|
+
expect(shouldShowScoutWarning("reviewer", "lead-alpha", empty, false, false)).toBe(false);
|
|
311
|
+
expect(shouldShowScoutWarning("lead", "lead-alpha", empty, false, false)).toBe(false);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("returns false when parentAgent is null (coordinator spawn)", () => {
|
|
315
|
+
expect(shouldShowScoutWarning("builder", null, withoutScout, false, false)).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("returns false when noScoutCheck is true (flag suppresses warning)", () => {
|
|
319
|
+
expect(shouldShowScoutWarning("builder", "lead-alpha", withoutScout, true, false)).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("returns false when skipScout is true (lead opted out of scouting)", () => {
|
|
323
|
+
expect(shouldShowScoutWarning("builder", "lead-alpha", withoutScout, false, true)).toBe(false);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("returns false when both noScoutCheck and skipScout are true", () => {
|
|
327
|
+
expect(shouldShowScoutWarning("builder", "lead-alpha", withoutScout, true, true)).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("returns false with empty sessions and no parent", () => {
|
|
331
|
+
expect(shouldShowScoutWarning("builder", null, empty, false, false)).toBe(false);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("returns true with empty sessions and a parent (no scouts ever spawned)", () => {
|
|
335
|
+
expect(shouldShowScoutWarning("builder", "lead-alpha", empty, false, false)).toBe(true);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
278
339
|
/**
|
|
279
340
|
* Tests for hierarchy validation in sling.
|
|
280
341
|
*
|
|
@@ -1147,3 +1208,69 @@ describe("sling runtime integration", () => {
|
|
|
1147
1208
|
expect(state.phase).toBe("loading");
|
|
1148
1209
|
});
|
|
1149
1210
|
});
|
|
1211
|
+
|
|
1212
|
+
describe("extractMulchRecordIds", () => {
|
|
1213
|
+
test("returns empty array for empty string", () => {
|
|
1214
|
+
expect(extractMulchRecordIds("")).toEqual([]);
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
test("returns empty when no mx-IDs present", () => {
|
|
1218
|
+
const text = "## agents (2 records)\n- convention without ID";
|
|
1219
|
+
expect(extractMulchRecordIds(text)).toEqual([]);
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
test("extracts single ID from a domain", () => {
|
|
1223
|
+
const text = "## agents (1 records)\n- [convention] Some. (mx-abc123)";
|
|
1224
|
+
expect(extractMulchRecordIds(text)).toEqual([{ id: "mx-abc123", domain: "agents" }]);
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
test("extracts multiple IDs from same domain", () => {
|
|
1228
|
+
const text = ["## typescript", "- first. (mx-aaa111)", "- second. (mx-bbb222)"].join("\n");
|
|
1229
|
+
expect(extractMulchRecordIds(text)).toEqual([
|
|
1230
|
+
{ id: "mx-aaa111", domain: "typescript" },
|
|
1231
|
+
{ id: "mx-bbb222", domain: "typescript" },
|
|
1232
|
+
]);
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
test("extracts IDs from multiple domains", () => {
|
|
1236
|
+
const text = ["## agents", "- agent. (mx-111aaa)", "## typescript", "- ts. (mx-222bbb)"].join(
|
|
1237
|
+
"\n",
|
|
1238
|
+
);
|
|
1239
|
+
expect(extractMulchRecordIds(text)).toEqual([
|
|
1240
|
+
{ id: "mx-111aaa", domain: "agents" },
|
|
1241
|
+
{ id: "mx-222bbb", domain: "typescript" },
|
|
1242
|
+
]);
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
test("ignores non-domain headings with no mx-IDs", () => {
|
|
1246
|
+
const text = [
|
|
1247
|
+
"## Quick Reference",
|
|
1248
|
+
"- use mulch search",
|
|
1249
|
+
"## agents",
|
|
1250
|
+
"- real. (mx-deadbeef)",
|
|
1251
|
+
].join("\n");
|
|
1252
|
+
expect(extractMulchRecordIds(text)).toEqual([{ id: "mx-deadbeef", domain: "agents" }]);
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
test("deduplicates repeated pairs", () => {
|
|
1256
|
+
const text = ["## agents", "- first. (mx-aabbcc)", "- dup. (mx-aabbcc)"].join("\n");
|
|
1257
|
+
expect(extractMulchRecordIds(text)).toEqual([{ id: "mx-aabbcc", domain: "agents" }]);
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
test("handles realistic ml prime output", () => {
|
|
1261
|
+
const text = [
|
|
1262
|
+
"## agents (3 records, updated just now)",
|
|
1263
|
+
"- [convention] lead.md convention. (mx-636708)",
|
|
1264
|
+
"- [convention] writeOverlay(). (mx-b7fa3d)",
|
|
1265
|
+
"## typescript (2 records, updated just now)",
|
|
1266
|
+
"- [convention] No any types. (mx-2ce43d)",
|
|
1267
|
+
"## Quick Reference",
|
|
1268
|
+
"- mulch search",
|
|
1269
|
+
].join("\n");
|
|
1270
|
+
const result = extractMulchRecordIds(text);
|
|
1271
|
+
expect(result).toHaveLength(3);
|
|
1272
|
+
expect(result).toContainEqual({ id: "mx-636708", domain: "agents" });
|
|
1273
|
+
expect(result).toContainEqual({ id: "mx-b7fa3d", domain: "agents" });
|
|
1274
|
+
expect(result).toContainEqual({ id: "mx-2ce43d", domain: "typescript" });
|
|
1275
|
+
});
|
|
1276
|
+
});
|