@os-eco/overstory-cli 0.6.11 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -13
- package/agents/builder.md +1 -1
- package/agents/coordinator.md +12 -11
- package/agents/lead.md +25 -24
- 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 +5 -3
- package/src/agents/guard-rules.ts +97 -0
- package/src/agents/hooks-deployer.ts +7 -90
- package/src/agents/overlay.test.ts +30 -7
- package/src/agents/overlay.ts +10 -9
- package/src/commands/agents.test.ts +5 -0
- package/src/commands/clean.test.ts +3 -0
- package/src/commands/completions.ts +1 -1
- package/src/commands/coordinator.test.ts +1 -0
- package/src/commands/coordinator.ts +34 -18
- package/src/commands/costs.test.ts +6 -1
- package/src/commands/costs.ts +13 -20
- package/src/commands/dashboard.ts +38 -138
- package/src/commands/doctor.test.ts +1 -1
- package/src/commands/doctor.ts +2 -2
- package/src/commands/ecosystem.ts +2 -1
- package/src/commands/errors.test.ts +4 -5
- package/src/commands/errors.ts +4 -62
- package/src/commands/feed.test.ts +2 -2
- package/src/commands/feed.ts +12 -106
- package/src/commands/init.test.ts +1 -2
- package/src/commands/init.ts +1 -8
- package/src/commands/inspect.test.ts +14 -0
- package/src/commands/inspect.ts +10 -44
- package/src/commands/log.test.ts +14 -0
- package/src/commands/log.ts +39 -0
- package/src/commands/logs.ts +7 -63
- package/src/commands/mail.test.ts +5 -0
- package/src/commands/metrics.test.ts +2 -2
- package/src/commands/metrics.ts +3 -17
- package/src/commands/monitor.ts +30 -16
- package/src/commands/nudge.test.ts +1 -0
- package/src/commands/prime.test.ts +2 -0
- package/src/commands/prime.ts +6 -2
- package/src/commands/replay.test.ts +2 -2
- package/src/commands/replay.ts +12 -135
- package/src/commands/run.test.ts +1 -0
- package/src/commands/run.ts +7 -23
- package/src/commands/sling.test.ts +68 -1
- package/src/commands/sling.ts +62 -24
- package/src/commands/status.test.ts +1 -0
- package/src/commands/status.ts +4 -17
- package/src/commands/stop.test.ts +1 -0
- package/src/commands/supervisor.ts +35 -18
- package/src/commands/trace.test.ts +6 -6
- package/src/commands/trace.ts +11 -109
- package/src/commands/worktree.test.ts +9 -0
- package/src/config.ts +39 -0
- package/src/doctor/consistency.test.ts +14 -0
- package/src/e2e/init-sling-lifecycle.test.ts +3 -5
- package/src/index.ts +2 -1
- package/src/logging/format.ts +214 -0
- package/src/logging/theme.ts +132 -0
- package/src/mail/broadcast.test.ts +1 -0
- package/src/merge/resolver.ts +23 -4
- package/src/metrics/store.test.ts +46 -0
- package/src/metrics/store.ts +11 -0
- package/src/mulch/client.test.ts +20 -0
- package/src/mulch/client.ts +312 -45
- package/src/runtimes/claude.test.ts +616 -0
- package/src/runtimes/claude.ts +218 -0
- package/src/runtimes/pi-guards.test.ts +433 -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 +86 -0
- package/src/runtimes/registry.ts +46 -0
- package/src/runtimes/types.ts +188 -0
- package/src/schema-consistency.test.ts +1 -0
- package/src/sessions/compat.ts +1 -0
- package/src/sessions/store.test.ts +31 -0
- package/src/sessions/store.ts +37 -4
- package/src/types.ts +21 -0
- package/src/watchdog/daemon.test.ts +7 -4
- package/src/watchdog/daemon.ts +1 -1
- package/src/watchdog/health.test.ts +1 -0
- package/src/watchdog/triage.ts +14 -4
- package/src/worktree/tmux.test.ts +28 -13
- package/src/worktree/tmux.ts +14 -28
package/src/config.ts
CHANGED
|
@@ -62,6 +62,17 @@ export const DEFAULT_CONFIG: OverstoryConfig = {
|
|
|
62
62
|
verbose: false,
|
|
63
63
|
redactSecrets: true,
|
|
64
64
|
},
|
|
65
|
+
runtime: {
|
|
66
|
+
default: "claude",
|
|
67
|
+
pi: {
|
|
68
|
+
provider: "anthropic",
|
|
69
|
+
modelMap: {
|
|
70
|
+
opus: "anthropic/claude-opus-4-6",
|
|
71
|
+
sonnet: "anthropic/claude-sonnet-4-6",
|
|
72
|
+
haiku: "anthropic/claude-haiku-4-5",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
65
76
|
};
|
|
66
77
|
|
|
67
78
|
const CONFIG_FILENAME = "config.yaml";
|
|
@@ -625,6 +636,34 @@ function validateConfig(config: OverstoryConfig): void {
|
|
|
625
636
|
}
|
|
626
637
|
}
|
|
627
638
|
|
|
639
|
+
// runtime.default must be a string if present
|
|
640
|
+
if (config.runtime !== undefined && typeof config.runtime.default !== "string") {
|
|
641
|
+
process.stderr.write(
|
|
642
|
+
`[overstory] WARNING: runtime.default must be a string. Got: ${typeof config.runtime.default}. Ignoring.\n`,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// runtime.pi: validate provider and modelMap if present
|
|
647
|
+
if (config.runtime?.pi) {
|
|
648
|
+
const pi = config.runtime.pi;
|
|
649
|
+
if (!pi.provider || typeof pi.provider !== "string") {
|
|
650
|
+
throw new ValidationError("runtime.pi.provider must be a non-empty string", {
|
|
651
|
+
field: "runtime.pi.provider",
|
|
652
|
+
value: pi.provider,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
if (pi.modelMap && typeof pi.modelMap === "object") {
|
|
656
|
+
for (const [alias, qualified] of Object.entries(pi.modelMap)) {
|
|
657
|
+
if (!qualified || typeof qualified !== "string") {
|
|
658
|
+
throw new ValidationError(`runtime.pi.modelMap.${alias} must be a non-empty string`, {
|
|
659
|
+
field: `runtime.pi.modelMap.${alias}`,
|
|
660
|
+
value: qualified,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
628
667
|
// models: validate each value — accepts aliases and provider-prefixed refs
|
|
629
668
|
const validAliases = ["sonnet", "opus", "haiku"];
|
|
630
669
|
const toolHeavyRoles = ["builder", "scout"];
|
|
@@ -207,6 +207,7 @@ describe("checkConsistency", () => {
|
|
|
207
207
|
lastActivity: new Date().toISOString(),
|
|
208
208
|
escalationLevel: 0,
|
|
209
209
|
stalledSince: null,
|
|
210
|
+
transcriptPath: null,
|
|
210
211
|
});
|
|
211
212
|
store.close();
|
|
212
213
|
|
|
@@ -243,6 +244,7 @@ describe("checkConsistency", () => {
|
|
|
243
244
|
lastActivity: new Date().toISOString(),
|
|
244
245
|
escalationLevel: 0,
|
|
245
246
|
stalledSince: null,
|
|
247
|
+
transcriptPath: null,
|
|
246
248
|
});
|
|
247
249
|
store.close();
|
|
248
250
|
|
|
@@ -278,6 +280,7 @@ describe("checkConsistency", () => {
|
|
|
278
280
|
lastActivity: new Date().toISOString(),
|
|
279
281
|
escalationLevel: 0,
|
|
280
282
|
stalledSince: null,
|
|
283
|
+
transcriptPath: null,
|
|
281
284
|
});
|
|
282
285
|
store.close();
|
|
283
286
|
|
|
@@ -314,6 +317,7 @@ describe("checkConsistency", () => {
|
|
|
314
317
|
lastActivity: new Date().toISOString(),
|
|
315
318
|
escalationLevel: 0,
|
|
316
319
|
stalledSince: null,
|
|
320
|
+
transcriptPath: null,
|
|
317
321
|
});
|
|
318
322
|
store.close();
|
|
319
323
|
|
|
@@ -353,6 +357,7 @@ describe("checkConsistency", () => {
|
|
|
353
357
|
lastActivity: new Date().toISOString(),
|
|
354
358
|
escalationLevel: 0,
|
|
355
359
|
stalledSince: null,
|
|
360
|
+
transcriptPath: null,
|
|
356
361
|
});
|
|
357
362
|
store.close();
|
|
358
363
|
|
|
@@ -426,6 +431,7 @@ describe("checkConsistency", () => {
|
|
|
426
431
|
lastActivity: new Date().toISOString(),
|
|
427
432
|
escalationLevel: 0,
|
|
428
433
|
stalledSince: null,
|
|
434
|
+
transcriptPath: null,
|
|
429
435
|
});
|
|
430
436
|
|
|
431
437
|
store.upsert({
|
|
@@ -445,6 +451,7 @@ describe("checkConsistency", () => {
|
|
|
445
451
|
lastActivity: new Date().toISOString(),
|
|
446
452
|
escalationLevel: 0,
|
|
447
453
|
stalledSince: null,
|
|
454
|
+
transcriptPath: null,
|
|
448
455
|
});
|
|
449
456
|
store.close();
|
|
450
457
|
|
|
@@ -481,6 +488,7 @@ describe("checkConsistency", () => {
|
|
|
481
488
|
lastActivity: new Date().toISOString(),
|
|
482
489
|
escalationLevel: 0,
|
|
483
490
|
stalledSince: null,
|
|
491
|
+
transcriptPath: null,
|
|
484
492
|
});
|
|
485
493
|
}
|
|
486
494
|
|
|
@@ -501,6 +509,7 @@ describe("checkConsistency", () => {
|
|
|
501
509
|
lastActivity: new Date().toISOString(),
|
|
502
510
|
escalationLevel: 0,
|
|
503
511
|
stalledSince: null,
|
|
512
|
+
transcriptPath: null,
|
|
504
513
|
});
|
|
505
514
|
store.close();
|
|
506
515
|
|
|
@@ -535,6 +544,7 @@ describe("checkConsistency", () => {
|
|
|
535
544
|
lastActivity: new Date().toISOString(),
|
|
536
545
|
escalationLevel: 0,
|
|
537
546
|
stalledSince: null,
|
|
547
|
+
transcriptPath: null,
|
|
538
548
|
});
|
|
539
549
|
|
|
540
550
|
store.upsert({
|
|
@@ -554,6 +564,7 @@ describe("checkConsistency", () => {
|
|
|
554
564
|
lastActivity: new Date().toISOString(),
|
|
555
565
|
escalationLevel: 0,
|
|
556
566
|
stalledSince: null,
|
|
567
|
+
transcriptPath: null,
|
|
557
568
|
});
|
|
558
569
|
}
|
|
559
570
|
store.close();
|
|
@@ -597,6 +608,7 @@ describe("checkConsistency", () => {
|
|
|
597
608
|
lastActivity: new Date().toISOString(),
|
|
598
609
|
escalationLevel: 0,
|
|
599
610
|
stalledSince: null,
|
|
611
|
+
transcriptPath: null,
|
|
600
612
|
});
|
|
601
613
|
|
|
602
614
|
store.upsert({
|
|
@@ -616,6 +628,7 @@ describe("checkConsistency", () => {
|
|
|
616
628
|
lastActivity: new Date().toISOString(),
|
|
617
629
|
escalationLevel: 0,
|
|
618
630
|
stalledSince: null,
|
|
631
|
+
transcriptPath: null,
|
|
619
632
|
});
|
|
620
633
|
|
|
621
634
|
// Lead-2 has builders only (bad)
|
|
@@ -636,6 +649,7 @@ describe("checkConsistency", () => {
|
|
|
636
649
|
lastActivity: new Date().toISOString(),
|
|
637
650
|
escalationLevel: 0,
|
|
638
651
|
stalledSince: null,
|
|
652
|
+
transcriptPath: null,
|
|
639
653
|
});
|
|
640
654
|
store.close();
|
|
641
655
|
|
|
@@ -27,7 +27,6 @@ const EXPECTED_AGENT_DEFS = [
|
|
|
27
27
|
"monitor.md",
|
|
28
28
|
"reviewer.md",
|
|
29
29
|
"scout.md",
|
|
30
|
-
"supervisor.md",
|
|
31
30
|
];
|
|
32
31
|
|
|
33
32
|
describe("E2E: init→sling lifecycle on external project", () => {
|
|
@@ -77,7 +76,7 @@ describe("E2E: init→sling lifecycle on external project", () => {
|
|
|
77
76
|
const gitignoreFile = Bun.file(join(overstoryDir, ".gitignore"));
|
|
78
77
|
expect(await gitignoreFile.exists()).toBe(true);
|
|
79
78
|
|
|
80
|
-
// agent-defs/ contains all
|
|
79
|
+
// agent-defs/ contains all 7 agent definition files (supervisor deprecated)
|
|
81
80
|
const agentDefsDir = join(overstoryDir, "agent-defs");
|
|
82
81
|
const agentDefFiles = (await readdir(agentDefsDir)).filter((f) => f.endsWith(".md")).sort();
|
|
83
82
|
expect(agentDefFiles).toEqual(EXPECTED_AGENT_DEFS);
|
|
@@ -109,7 +108,7 @@ describe("E2E: init→sling lifecycle on external project", () => {
|
|
|
109
108
|
expect(config.project.name).toBeTruthy();
|
|
110
109
|
});
|
|
111
110
|
|
|
112
|
-
test("manifest loads successfully with all
|
|
111
|
+
test("manifest loads successfully with all 7 agents (supervisor deprecated)", async () => {
|
|
113
112
|
await initCommand({});
|
|
114
113
|
|
|
115
114
|
const manifestPath = join(tempDir, ".overstory", "agent-manifest.json");
|
|
@@ -118,7 +117,7 @@ describe("E2E: init→sling lifecycle on external project", () => {
|
|
|
118
117
|
|
|
119
118
|
const manifest = await loader.load();
|
|
120
119
|
|
|
121
|
-
// All
|
|
120
|
+
// All 7 agents present (supervisor removed: deprecated, use lead instead)
|
|
122
121
|
const agentNames = Object.keys(manifest.agents).sort();
|
|
123
122
|
expect(agentNames).toEqual([
|
|
124
123
|
"builder",
|
|
@@ -128,7 +127,6 @@ describe("E2E: init→sling lifecycle on external project", () => {
|
|
|
128
127
|
"monitor",
|
|
129
128
|
"reviewer",
|
|
130
129
|
"scout",
|
|
131
|
-
"supervisor",
|
|
132
130
|
]);
|
|
133
131
|
|
|
134
132
|
// Each agent has a valid file reference
|
package/src/index.ts
CHANGED
|
@@ -45,7 +45,7 @@ import { OverstoryError, WorktreeError } from "./errors.ts";
|
|
|
45
45
|
import { jsonError } from "./json.ts";
|
|
46
46
|
import { brand, chalk, muted, setQuiet } from "./logging/color.ts";
|
|
47
47
|
|
|
48
|
-
export const VERSION = "0.
|
|
48
|
+
export const VERSION = "0.7.2";
|
|
49
49
|
|
|
50
50
|
const rawArgs = process.argv.slice(2);
|
|
51
51
|
|
|
@@ -255,6 +255,7 @@ program
|
|
|
255
255
|
.option("--max-agents <n>", "Max children per lead (overrides config)")
|
|
256
256
|
.option("--skip-review", "Skip review phase for lead agents")
|
|
257
257
|
.option("--dispatch-max-agents <n>", "Per-lead max agents ceiling (injected into overlay)")
|
|
258
|
+
.option("--runtime <name>", "Runtime adapter (default: config or claude)")
|
|
258
259
|
.option("--json", "Output result as JSON")
|
|
259
260
|
.action(async (taskId, opts) => {
|
|
260
261
|
await slingCommand(taskId, opts);
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatting utilities for overstory CLI output.
|
|
3
|
+
*
|
|
4
|
+
* Duration, timestamp, event detail, agent color mapping, and status color
|
|
5
|
+
* helpers used across all observability commands.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { StoredEvent } from "../types.ts";
|
|
9
|
+
import type { ColorFn } from "./color.ts";
|
|
10
|
+
import { color, noColor } from "./color.ts";
|
|
11
|
+
import { AGENT_COLORS } from "./theme.ts";
|
|
12
|
+
|
|
13
|
+
// === Duration ===
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Formats a duration in milliseconds to a human-readable string.
|
|
17
|
+
* Examples: "0s", "12s", "3m 45s", "2h 15m"
|
|
18
|
+
*/
|
|
19
|
+
export function formatDuration(ms: number): string {
|
|
20
|
+
if (ms === 0) return "0s";
|
|
21
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
22
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
23
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
24
|
+
const seconds = totalSeconds % 60;
|
|
25
|
+
if (hours > 0) {
|
|
26
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
27
|
+
}
|
|
28
|
+
if (minutes > 0) {
|
|
29
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
30
|
+
}
|
|
31
|
+
return `${seconds}s`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// === Timestamps ===
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extracts "HH:MM:SS" from an ISO 8601 timestamp string.
|
|
38
|
+
* Returns the raw substring if the timestamp is well-formed.
|
|
39
|
+
*/
|
|
40
|
+
export function formatAbsoluteTime(timestamp: string): string {
|
|
41
|
+
// ISO format: "YYYY-MM-DDTHH:MM:SS..." or "YYYY-MM-DD HH:MM:SS..."
|
|
42
|
+
const match = timestamp.match(/T?(\d{2}:\d{2}:\d{2})/);
|
|
43
|
+
return match?.[1] ?? timestamp;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extracts "YYYY-MM-DD" from an ISO 8601 timestamp string.
|
|
48
|
+
*/
|
|
49
|
+
export function formatDate(timestamp: string): string {
|
|
50
|
+
const match = timestamp.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
51
|
+
return match?.[1] ?? timestamp;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Formats a timestamp as a human-readable relative time string.
|
|
56
|
+
* Examples: "12s ago", "3m ago", "2h ago", "5d ago"
|
|
57
|
+
*/
|
|
58
|
+
export function formatRelativeTime(timestamp: string): string {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
const then = new Date(timestamp).getTime();
|
|
61
|
+
const diffMs = now - then;
|
|
62
|
+
if (diffMs < 0) return "just now";
|
|
63
|
+
const diffSeconds = Math.floor(diffMs / 1000);
|
|
64
|
+
const diffMinutes = Math.floor(diffSeconds / 60);
|
|
65
|
+
const diffHours = Math.floor(diffMinutes / 60);
|
|
66
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
67
|
+
if (diffDays > 0) return `${diffDays}d ago`;
|
|
68
|
+
if (diffHours > 0) return `${diffHours}h ago`;
|
|
69
|
+
if (diffMinutes > 0) return `${diffMinutes}m ago`;
|
|
70
|
+
return `${diffSeconds}s ago`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// === Event Details ===
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Builds a compact "key=value" detail string from a StoredEvent's fields.
|
|
77
|
+
* Values are truncated to maxValueLen (default 80) characters.
|
|
78
|
+
*/
|
|
79
|
+
export function buildEventDetail(event: StoredEvent, maxValueLen = 80): string {
|
|
80
|
+
const parts: string[] = [];
|
|
81
|
+
|
|
82
|
+
if (event.toolName) {
|
|
83
|
+
parts.push(`tool=${event.toolName}`);
|
|
84
|
+
}
|
|
85
|
+
if (event.toolArgs) {
|
|
86
|
+
const truncated =
|
|
87
|
+
event.toolArgs.length > maxValueLen
|
|
88
|
+
? `${event.toolArgs.slice(0, maxValueLen)}…`
|
|
89
|
+
: event.toolArgs;
|
|
90
|
+
parts.push(`args=${truncated}`);
|
|
91
|
+
}
|
|
92
|
+
if (event.toolDurationMs !== null && event.toolDurationMs !== undefined) {
|
|
93
|
+
parts.push(`dur=${event.toolDurationMs}ms`);
|
|
94
|
+
}
|
|
95
|
+
if (event.data) {
|
|
96
|
+
const truncated =
|
|
97
|
+
event.data.length > maxValueLen ? `${event.data.slice(0, maxValueLen)}…` : event.data;
|
|
98
|
+
parts.push(`data=${truncated}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return parts.join(" ");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// === Agent Color Mapping ===
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Builds a stable color map for agents by first-appearance order in events.
|
|
108
|
+
* Agents are assigned colors from AGENT_COLORS cycling as needed.
|
|
109
|
+
*/
|
|
110
|
+
export function buildAgentColorMap(events: StoredEvent[]): Map<string, ColorFn> {
|
|
111
|
+
const colorMap = new Map<string, ColorFn>();
|
|
112
|
+
let idx = 0;
|
|
113
|
+
for (const event of events) {
|
|
114
|
+
if (!colorMap.has(event.agentName)) {
|
|
115
|
+
const colorFn = AGENT_COLORS[idx % AGENT_COLORS.length] ?? noColor;
|
|
116
|
+
colorMap.set(event.agentName, colorFn);
|
|
117
|
+
idx++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return colorMap;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extends an existing agent color map with new agents from the given events.
|
|
125
|
+
* Used in follow mode to add agents discovered in incremental event batches.
|
|
126
|
+
*/
|
|
127
|
+
export function extendAgentColorMap(colorMap: Map<string, ColorFn>, events: StoredEvent[]): void {
|
|
128
|
+
let idx = colorMap.size;
|
|
129
|
+
for (const event of events) {
|
|
130
|
+
if (!colorMap.has(event.agentName)) {
|
|
131
|
+
const colorFn = AGENT_COLORS[idx % AGENT_COLORS.length] ?? noColor;
|
|
132
|
+
colorMap.set(event.agentName, colorFn);
|
|
133
|
+
idx++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// === Status Colors ===
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns a color function for a merge status string.
|
|
142
|
+
* pending=yellow, merging=blue, conflict=red, merged=green
|
|
143
|
+
*/
|
|
144
|
+
export function mergeStatusColor(status: string): ColorFn {
|
|
145
|
+
switch (status) {
|
|
146
|
+
case "pending":
|
|
147
|
+
return color.yellow;
|
|
148
|
+
case "merging":
|
|
149
|
+
return color.blue;
|
|
150
|
+
case "conflict":
|
|
151
|
+
return color.red;
|
|
152
|
+
case "merged":
|
|
153
|
+
return color.green;
|
|
154
|
+
default:
|
|
155
|
+
return (text: string) => text;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Returns a color function for a priority string.
|
|
161
|
+
* urgent=red, high=yellow, normal=identity, low=dim
|
|
162
|
+
*/
|
|
163
|
+
export function priorityColor(priority: string): ColorFn {
|
|
164
|
+
switch (priority) {
|
|
165
|
+
case "urgent":
|
|
166
|
+
return color.red;
|
|
167
|
+
case "high":
|
|
168
|
+
return color.yellow;
|
|
169
|
+
case "normal":
|
|
170
|
+
return (text: string) => text;
|
|
171
|
+
case "low":
|
|
172
|
+
return color.dim;
|
|
173
|
+
default:
|
|
174
|
+
return (text: string) => text;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Returns a color function for a log level string.
|
|
180
|
+
* debug=gray, info=blue, warn=yellow, error=red
|
|
181
|
+
*/
|
|
182
|
+
export function logLevelColor(level: string): ColorFn {
|
|
183
|
+
switch (level) {
|
|
184
|
+
case "debug":
|
|
185
|
+
return color.gray;
|
|
186
|
+
case "info":
|
|
187
|
+
return color.blue;
|
|
188
|
+
case "warn":
|
|
189
|
+
return color.yellow;
|
|
190
|
+
case "error":
|
|
191
|
+
return color.red;
|
|
192
|
+
default:
|
|
193
|
+
return (text: string) => text;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Returns a 3-character label for a log level string.
|
|
199
|
+
* debug="DBG", info="INF", warn="WRN", error="ERR"
|
|
200
|
+
*/
|
|
201
|
+
export function logLevelLabel(level: string): string {
|
|
202
|
+
switch (level) {
|
|
203
|
+
case "debug":
|
|
204
|
+
return "DBG";
|
|
205
|
+
case "info":
|
|
206
|
+
return "INF";
|
|
207
|
+
case "warn":
|
|
208
|
+
return "WRN";
|
|
209
|
+
case "error":
|
|
210
|
+
return "ERR";
|
|
211
|
+
default:
|
|
212
|
+
return level.slice(0, 3).toUpperCase();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical visual theme for overstory CLI output.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for state colors, event labels, agent palette,
|
|
5
|
+
* separators, and header rendering. All observability commands import from here.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentState, EventType } from "../types.ts";
|
|
9
|
+
import type { ColorFn } from "./color.ts";
|
|
10
|
+
import { brand, color, noColor, visibleLength } from "./color.ts";
|
|
11
|
+
|
|
12
|
+
// === Agent State Theme ===
|
|
13
|
+
|
|
14
|
+
/** Maps agent states to their visual color functions. */
|
|
15
|
+
const STATE_COLORS: Record<AgentState, ColorFn> = {
|
|
16
|
+
working: color.green,
|
|
17
|
+
booting: color.yellow,
|
|
18
|
+
stalled: color.red,
|
|
19
|
+
zombie: color.dim,
|
|
20
|
+
completed: color.cyan,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Maps agent states to their icon characters. */
|
|
24
|
+
const STATE_ICONS: Record<AgentState, string> = {
|
|
25
|
+
working: ">",
|
|
26
|
+
booting: "~",
|
|
27
|
+
stalled: "!",
|
|
28
|
+
zombie: "x",
|
|
29
|
+
completed: "\u2713",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Returns the color function for a given agent state. Falls back to noColor. */
|
|
33
|
+
export function stateColor(state: string): ColorFn {
|
|
34
|
+
return STATE_COLORS[state as AgentState] ?? noColor;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Returns the raw icon character for a given agent state. Falls back to "?". */
|
|
38
|
+
export function stateIcon(state: string): string {
|
|
39
|
+
return STATE_ICONS[state as AgentState] ?? "?";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Returns a colored icon string for a given agent state. */
|
|
43
|
+
export function stateIconColored(state: string): string {
|
|
44
|
+
return stateColor(state)(stateIcon(state));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// === Event Label Theme ===
|
|
48
|
+
|
|
49
|
+
export interface EventLabel {
|
|
50
|
+
/** 5-character compact label (for feed). */
|
|
51
|
+
compact: string;
|
|
52
|
+
/** 10-character full label (for trace/replay). */
|
|
53
|
+
full: string;
|
|
54
|
+
/** Color function for this event type. */
|
|
55
|
+
color: ColorFn;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Maps event types to their compact (5-char) and full (10-char) labels, plus color. */
|
|
59
|
+
const EVENT_LABELS: Record<EventType, EventLabel> = {
|
|
60
|
+
tool_start: { compact: "TOOL+", full: "TOOL START", color: color.blue },
|
|
61
|
+
tool_end: { compact: "TOOL-", full: "TOOL END ", color: color.blue },
|
|
62
|
+
session_start: { compact: "SESS+", full: "SESSION +", color: color.green },
|
|
63
|
+
session_end: { compact: "SESS-", full: "SESSION -", color: color.yellow },
|
|
64
|
+
mail_sent: { compact: "MAIL>", full: "MAIL SENT ", color: color.cyan },
|
|
65
|
+
mail_received: { compact: "MAIL<", full: "MAIL RECV ", color: color.cyan },
|
|
66
|
+
spawn: { compact: "SPAWN", full: "SPAWN ", color: color.magenta },
|
|
67
|
+
error: { compact: "ERROR", full: "ERROR ", color: color.red },
|
|
68
|
+
custom: { compact: "CUSTM", full: "CUSTOM ", color: color.gray },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Returns the EventLabel for a given event type. */
|
|
72
|
+
export function eventLabel(eventType: EventType): EventLabel {
|
|
73
|
+
return EVENT_LABELS[eventType];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// === Agent Colors (for multi-agent displays) ===
|
|
77
|
+
|
|
78
|
+
/** Stable palette for assigning distinct colors to agents in multi-agent displays. */
|
|
79
|
+
export const AGENT_COLORS: readonly ColorFn[] = [
|
|
80
|
+
color.blue,
|
|
81
|
+
color.green,
|
|
82
|
+
color.yellow,
|
|
83
|
+
color.cyan,
|
|
84
|
+
color.magenta,
|
|
85
|
+
] as const;
|
|
86
|
+
|
|
87
|
+
// === Separators ===
|
|
88
|
+
|
|
89
|
+
/** Unicode thin horizontal box-drawing character. */
|
|
90
|
+
export const SEPARATOR_CHAR = "\u2500";
|
|
91
|
+
|
|
92
|
+
/** Unicode double horizontal box-drawing character (thick). */
|
|
93
|
+
export const THICK_SEPARATOR_CHAR = "\u2550";
|
|
94
|
+
|
|
95
|
+
/** Default line width for separators and headers. */
|
|
96
|
+
export const DEFAULT_WIDTH = 70;
|
|
97
|
+
|
|
98
|
+
/** Returns a thin separator line of the given width (default 70). */
|
|
99
|
+
export function separator(width?: number): string {
|
|
100
|
+
return SEPARATOR_CHAR.repeat(width ?? DEFAULT_WIDTH);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Returns a thick (double-line) separator of the given width (default 70). */
|
|
104
|
+
export function thickSeparator(width?: number): string {
|
|
105
|
+
return THICK_SEPARATOR_CHAR.repeat(width ?? DEFAULT_WIDTH);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// === Header Rendering ===
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Pads a string to the given visible width, accounting for ANSI escape codes.
|
|
112
|
+
* If the string is already wider than width, returns it unchanged.
|
|
113
|
+
*/
|
|
114
|
+
export function padVisible(str: string, width: number): string {
|
|
115
|
+
const visible = visibleLength(str);
|
|
116
|
+
if (visible >= width) return str;
|
|
117
|
+
return str + " ".repeat(width - visible);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Renders a primary header: brand bold title + newline + thin separator.
|
|
122
|
+
*/
|
|
123
|
+
export function renderHeader(title: string, width?: number): string {
|
|
124
|
+
return `${brand.bold(title)}\n${separator(width)}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Renders a secondary header: color bold title + newline + dim thin separator.
|
|
129
|
+
*/
|
|
130
|
+
export function renderSubHeader(title: string, width?: number): string {
|
|
131
|
+
return `${color.bold(title)}\n${color.dim(separator(width))}`;
|
|
132
|
+
}
|
package/src/merge/resolver.ts
CHANGED
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
|
|
14
14
|
import { MergeError } from "../errors.ts";
|
|
15
15
|
import type { MulchClient } from "../mulch/client.ts";
|
|
16
|
+
import { getRuntime } from "../runtimes/registry.ts";
|
|
16
17
|
import type {
|
|
17
18
|
ConflictHistory,
|
|
18
19
|
MergeEntry,
|
|
19
20
|
MergeResult,
|
|
21
|
+
OverstoryConfig,
|
|
20
22
|
ParsedConflictPattern,
|
|
21
23
|
ResolutionTier,
|
|
22
24
|
} from "../types.ts";
|
|
@@ -243,6 +245,7 @@ async function tryAiResolve(
|
|
|
243
245
|
conflictFiles: string[],
|
|
244
246
|
repoRoot: string,
|
|
245
247
|
pastResolutions?: string[],
|
|
248
|
+
config?: OverstoryConfig,
|
|
246
249
|
): Promise<{ success: boolean; remainingConflicts: string[] }> {
|
|
247
250
|
const remainingConflicts: string[] = [];
|
|
248
251
|
|
|
@@ -265,7 +268,9 @@ async function tryAiResolve(
|
|
|
265
268
|
content,
|
|
266
269
|
].join(" ");
|
|
267
270
|
|
|
268
|
-
const
|
|
271
|
+
const runtime = getRuntime(config?.runtime?.printCommand ?? config?.runtime?.default, config);
|
|
272
|
+
const argv = runtime.buildPrintCommand(prompt);
|
|
273
|
+
const proc = Bun.spawn(argv, {
|
|
269
274
|
cwd: repoRoot,
|
|
270
275
|
stdout: "pipe",
|
|
271
276
|
stderr: "pipe",
|
|
@@ -315,6 +320,7 @@ async function tryReimagine(
|
|
|
315
320
|
entry: MergeEntry,
|
|
316
321
|
canonicalBranch: string,
|
|
317
322
|
repoRoot: string,
|
|
323
|
+
config?: OverstoryConfig,
|
|
318
324
|
): Promise<{ success: boolean }> {
|
|
319
325
|
// Abort the current merge
|
|
320
326
|
await runGit(repoRoot, ["merge", "--abort"]);
|
|
@@ -348,7 +354,9 @@ async function tryReimagine(
|
|
|
348
354
|
branchContent,
|
|
349
355
|
].join("");
|
|
350
356
|
|
|
351
|
-
const
|
|
357
|
+
const runtime = getRuntime(config?.runtime?.printCommand ?? config?.runtime?.default, config);
|
|
358
|
+
const argv = runtime.buildPrintCommand(prompt);
|
|
359
|
+
const proc = Bun.spawn(argv, {
|
|
352
360
|
cwd: repoRoot,
|
|
353
361
|
stdout: "pipe",
|
|
354
362
|
stderr: "pipe",
|
|
@@ -556,6 +564,7 @@ export function createMergeResolver(options: {
|
|
|
556
564
|
aiResolveEnabled: boolean;
|
|
557
565
|
reimagineEnabled: boolean;
|
|
558
566
|
mulchClient?: MulchClient;
|
|
567
|
+
config?: OverstoryConfig;
|
|
559
568
|
}): MergeResolver {
|
|
560
569
|
return {
|
|
561
570
|
async resolve(
|
|
@@ -632,7 +641,12 @@ export function createMergeResolver(options: {
|
|
|
632
641
|
// Tier 3: AI-resolve
|
|
633
642
|
if (options.aiResolveEnabled && !history.skipTiers.includes("ai-resolve")) {
|
|
634
643
|
lastTier = "ai-resolve";
|
|
635
|
-
const aiResult = await tryAiResolve(
|
|
644
|
+
const aiResult = await tryAiResolve(
|
|
645
|
+
conflictFiles,
|
|
646
|
+
repoRoot,
|
|
647
|
+
history.pastResolutions,
|
|
648
|
+
options.config,
|
|
649
|
+
);
|
|
636
650
|
if (aiResult.success) {
|
|
637
651
|
if (options.mulchClient) {
|
|
638
652
|
recordConflictPattern(options.mulchClient, entry, "ai-resolve", conflictFiles, true);
|
|
@@ -651,7 +665,12 @@ export function createMergeResolver(options: {
|
|
|
651
665
|
// Tier 4: Re-imagine
|
|
652
666
|
if (options.reimagineEnabled && !history.skipTiers.includes("reimagine")) {
|
|
653
667
|
lastTier = "reimagine";
|
|
654
|
-
const reimagineResult = await tryReimagine(
|
|
668
|
+
const reimagineResult = await tryReimagine(
|
|
669
|
+
entry,
|
|
670
|
+
canonicalBranch,
|
|
671
|
+
repoRoot,
|
|
672
|
+
options.config,
|
|
673
|
+
);
|
|
655
674
|
if (reimagineResult.success) {
|
|
656
675
|
if (options.mulchClient) {
|
|
657
676
|
recordConflictPattern(options.mulchClient, entry, "reimagine", conflictFiles, true);
|