@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
|
@@ -773,6 +773,7 @@ describe("mailCommand", () => {
|
|
|
773
773
|
lastActivity: new Date().toISOString(),
|
|
774
774
|
escalationLevel: 0,
|
|
775
775
|
stalledSince: null,
|
|
776
|
+
transcriptPath: null,
|
|
776
777
|
},
|
|
777
778
|
{
|
|
778
779
|
id: "session-builder-1",
|
|
@@ -791,6 +792,7 @@ describe("mailCommand", () => {
|
|
|
791
792
|
lastActivity: new Date().toISOString(),
|
|
792
793
|
escalationLevel: 0,
|
|
793
794
|
stalledSince: null,
|
|
795
|
+
transcriptPath: null,
|
|
794
796
|
},
|
|
795
797
|
{
|
|
796
798
|
id: "session-builder-2",
|
|
@@ -809,6 +811,7 @@ describe("mailCommand", () => {
|
|
|
809
811
|
lastActivity: new Date().toISOString(),
|
|
810
812
|
escalationLevel: 0,
|
|
811
813
|
stalledSince: null,
|
|
814
|
+
transcriptPath: null,
|
|
812
815
|
},
|
|
813
816
|
{
|
|
814
817
|
id: "session-scout-1",
|
|
@@ -827,6 +830,7 @@ describe("mailCommand", () => {
|
|
|
827
830
|
lastActivity: new Date().toISOString(),
|
|
828
831
|
escalationLevel: 0,
|
|
829
832
|
stalledSince: null,
|
|
833
|
+
transcriptPath: null,
|
|
830
834
|
},
|
|
831
835
|
];
|
|
832
836
|
|
|
@@ -1147,6 +1151,7 @@ describe("mailCommand", () => {
|
|
|
1147
1151
|
lastActivity: new Date().toISOString(),
|
|
1148
1152
|
escalationLevel: 0,
|
|
1149
1153
|
stalledSince: null,
|
|
1154
|
+
transcriptPath: null,
|
|
1150
1155
|
});
|
|
1151
1156
|
}
|
|
1152
1157
|
|
|
@@ -443,7 +443,7 @@ describe("formatDuration helper", () => {
|
|
|
443
443
|
expect(out).toContain("1h 2m");
|
|
444
444
|
});
|
|
445
445
|
|
|
446
|
-
test("3600000ms formats as 1h
|
|
446
|
+
test("3600000ms formats as 1h", async () => {
|
|
447
447
|
const dbPath = join(tempDir, ".overstory", "metrics.db");
|
|
448
448
|
const store = createMetricsStore(dbPath);
|
|
449
449
|
store.recordSession(makeSession(3_600_000));
|
|
@@ -452,6 +452,6 @@ describe("formatDuration helper", () => {
|
|
|
452
452
|
await metricsCommand([]);
|
|
453
453
|
const out = output();
|
|
454
454
|
|
|
455
|
-
expect(out).toContain("1h
|
|
455
|
+
expect(out).toContain("1h");
|
|
456
456
|
});
|
|
457
457
|
});
|
package/src/commands/metrics.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { join } from "node:path";
|
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
import { loadConfig } from "../config.ts";
|
|
11
11
|
import { jsonOutput } from "../json.ts";
|
|
12
|
+
import { formatDuration } from "../logging/format.ts";
|
|
13
|
+
import { renderHeader } from "../logging/theme.ts";
|
|
12
14
|
import { createMetricsStore } from "../metrics/store.ts";
|
|
13
15
|
|
|
14
16
|
interface MetricsOpts {
|
|
@@ -16,21 +18,6 @@ interface MetricsOpts {
|
|
|
16
18
|
json?: boolean;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
/**
|
|
20
|
-
* Format milliseconds as human-readable duration.
|
|
21
|
-
*/
|
|
22
|
-
function formatDuration(ms: number): string {
|
|
23
|
-
if (ms === 0) return "0s";
|
|
24
|
-
const seconds = Math.floor(ms / 1000);
|
|
25
|
-
if (seconds < 60) return `${seconds}s`;
|
|
26
|
-
const minutes = Math.floor(seconds / 60);
|
|
27
|
-
const remainSec = seconds % 60;
|
|
28
|
-
if (minutes < 60) return `${minutes}m ${remainSec}s`;
|
|
29
|
-
const hours = Math.floor(minutes / 60);
|
|
30
|
-
const remainMin = minutes % 60;
|
|
31
|
-
return `${hours}h ${remainMin}m`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
21
|
async function executeMetrics(opts: MetricsOpts): Promise<void> {
|
|
35
22
|
const limit = opts.last ? Number.parseInt(opts.last, 10) : 20;
|
|
36
23
|
const json = opts.json ?? false;
|
|
@@ -64,8 +51,7 @@ async function executeMetrics(opts: MetricsOpts): Promise<void> {
|
|
|
64
51
|
return;
|
|
65
52
|
}
|
|
66
53
|
|
|
67
|
-
process.stdout.write("Session Metrics\n
|
|
68
|
-
process.stdout.write(`${"═".repeat(60)}\n\n`);
|
|
54
|
+
process.stdout.write(`${renderHeader("Session Metrics")}\n\n`);
|
|
69
55
|
|
|
70
56
|
// Summary stats
|
|
71
57
|
const completed = sessions.filter((s) => s.completedAt !== null);
|
package/src/commands/monitor.ts
CHANGED
|
@@ -16,13 +16,13 @@
|
|
|
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";
|
|
23
22
|
import { AgentError, ValidationError } from "../errors.ts";
|
|
24
23
|
import { jsonOutput } from "../json.ts";
|
|
25
24
|
import { printHint, printSuccess } from "../logging/color.ts";
|
|
25
|
+
import { getRuntime } from "../runtimes/registry.ts";
|
|
26
26
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
27
27
|
import type { AgentSession } from "../types.ts";
|
|
28
28
|
import { createSession, isSessionAlive, killSession, sendKeys } from "../worktree/tmux.ts";
|
|
@@ -110,8 +110,21 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
|
|
|
110
110
|
store.updateState(MONITOR_NAME, "completed");
|
|
111
111
|
}
|
|
112
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
|
+
|
|
113
122
|
// Deploy monitor-specific hooks to the project root's .claude/ directory.
|
|
114
|
-
await
|
|
123
|
+
await runtime.deployConfig(projectRoot, undefined, {
|
|
124
|
+
agentName: MONITOR_NAME,
|
|
125
|
+
capability: "monitor",
|
|
126
|
+
worktreePath: projectRoot,
|
|
127
|
+
});
|
|
115
128
|
|
|
116
129
|
// Create monitor identity if first run
|
|
117
130
|
const identityBaseDir = join(projectRoot, ".overstory", "agents");
|
|
@@ -128,25 +141,25 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
|
|
|
128
141
|
});
|
|
129
142
|
}
|
|
130
143
|
|
|
131
|
-
// Resolve model from config > manifest > fallback
|
|
132
|
-
const manifestLoader = createManifestLoader(
|
|
133
|
-
join(projectRoot, config.agents.manifestPath),
|
|
134
|
-
join(projectRoot, config.agents.baseDir),
|
|
135
|
-
);
|
|
136
|
-
const manifest = await manifestLoader.load();
|
|
137
|
-
const { model, env } = resolveModel(config, manifest, "monitor", "sonnet");
|
|
138
|
-
|
|
139
144
|
// Spawn tmux session at project root with Claude Code (interactive mode).
|
|
140
145
|
const agentDefPath = join(projectRoot, ".overstory", "agent-defs", "monitor.md");
|
|
141
146
|
const agentDefFile = Bun.file(agentDefPath);
|
|
142
|
-
let
|
|
147
|
+
let appendSystemPrompt: string | undefined;
|
|
143
148
|
if (await agentDefFile.exists()) {
|
|
144
|
-
|
|
145
|
-
const escaped = agentDef.replace(/'/g, "'\\''");
|
|
146
|
-
claudeCmd += ` --append-system-prompt '${escaped}'`;
|
|
149
|
+
appendSystemPrompt = await agentDefFile.text();
|
|
147
150
|
}
|
|
148
|
-
const
|
|
149
|
-
|
|
151
|
+
const spawnCmd = runtime.buildSpawnCommand({
|
|
152
|
+
model: resolvedModel.model,
|
|
153
|
+
permissionMode: "bypass",
|
|
154
|
+
cwd: projectRoot,
|
|
155
|
+
appendSystemPrompt,
|
|
156
|
+
env: {
|
|
157
|
+
...runtime.buildEnv(resolvedModel),
|
|
158
|
+
OVERSTORY_AGENT_NAME: MONITOR_NAME,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
const pid = await createSession(tmuxSession, projectRoot, spawnCmd, {
|
|
162
|
+
...runtime.buildEnv(resolvedModel),
|
|
150
163
|
OVERSTORY_AGENT_NAME: MONITOR_NAME,
|
|
151
164
|
});
|
|
152
165
|
|
|
@@ -169,6 +182,7 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
|
|
|
169
182
|
lastActivity: new Date().toISOString(),
|
|
170
183
|
escalationLevel: 0,
|
|
171
184
|
stalledSince: null,
|
|
185
|
+
transcriptPath: null,
|
|
172
186
|
};
|
|
173
187
|
|
|
174
188
|
store.upsert(session);
|
|
@@ -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
|
|
|
@@ -228,7 +228,7 @@ describe("replayCommand", () => {
|
|
|
228
228
|
await replayCommand(["--run", "run-001"]);
|
|
229
229
|
const out = output();
|
|
230
230
|
|
|
231
|
-
expect(out).toContain("
|
|
231
|
+
expect(out).toContain("─".repeat(70));
|
|
232
232
|
});
|
|
233
233
|
|
|
234
234
|
test("shows event count", async () => {
|
|
@@ -742,7 +742,7 @@ describe("replayCommand", () => {
|
|
|
742
742
|
const out = output();
|
|
743
743
|
|
|
744
744
|
expect(out).not.toContain(longValue);
|
|
745
|
-
expect(out).toContain("
|
|
745
|
+
expect(out).toContain("…");
|
|
746
746
|
});
|
|
747
747
|
});
|
|
748
748
|
});
|
package/src/commands/replay.ts
CHANGED
|
@@ -12,135 +12,16 @@ import { loadConfig } from "../config.ts";
|
|
|
12
12
|
import { ValidationError } from "../errors.ts";
|
|
13
13
|
import { createEventStore } from "../events/store.ts";
|
|
14
14
|
import { jsonOutput } from "../json.ts";
|
|
15
|
-
import type { ColorFn } from "../logging/color.ts";
|
|
16
15
|
import { color } from "../logging/color.ts";
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
mail_received: { label: "MAIL RECV ", color: color.cyan },
|
|
27
|
-
spawn: { label: "SPAWN ", color: color.magenta },
|
|
28
|
-
error: { label: "ERROR ", color: color.red },
|
|
29
|
-
custom: { label: "CUSTOM ", color: color.gray },
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/** Color functions assigned to agents in order of first appearance. */
|
|
33
|
-
const AGENT_COLORS: readonly ColorFn[] = [
|
|
34
|
-
color.blue,
|
|
35
|
-
color.green,
|
|
36
|
-
color.yellow,
|
|
37
|
-
color.cyan,
|
|
38
|
-
color.magenta,
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Format a relative time string from a timestamp.
|
|
43
|
-
* Returns strings like "2m ago", "1h ago", "3d ago".
|
|
44
|
-
*/
|
|
45
|
-
function formatRelativeTime(timestamp: string): string {
|
|
46
|
-
const eventTime = new Date(timestamp).getTime();
|
|
47
|
-
const now = Date.now();
|
|
48
|
-
const diffMs = now - eventTime;
|
|
49
|
-
|
|
50
|
-
if (diffMs < 0) return "just now";
|
|
51
|
-
|
|
52
|
-
const seconds = Math.floor(diffMs / 1000);
|
|
53
|
-
if (seconds < 60) return `${seconds}s ago`;
|
|
54
|
-
|
|
55
|
-
const minutes = Math.floor(seconds / 60);
|
|
56
|
-
if (minutes < 60) return `${minutes}m ago`;
|
|
57
|
-
|
|
58
|
-
const hours = Math.floor(minutes / 60);
|
|
59
|
-
if (hours < 24) return `${hours}h ago`;
|
|
60
|
-
|
|
61
|
-
const days = Math.floor(hours / 24);
|
|
62
|
-
return `${days}d ago`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Format an absolute time from an ISO timestamp.
|
|
67
|
-
* Returns "HH:MM:SS" portion.
|
|
68
|
-
*/
|
|
69
|
-
function formatAbsoluteTime(timestamp: string): string {
|
|
70
|
-
const match = /T(\d{2}:\d{2}:\d{2})/.exec(timestamp);
|
|
71
|
-
if (match?.[1]) {
|
|
72
|
-
return match[1];
|
|
73
|
-
}
|
|
74
|
-
return timestamp;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Format the date portion of an ISO timestamp.
|
|
79
|
-
* Returns "YYYY-MM-DD".
|
|
80
|
-
*/
|
|
81
|
-
function formatDate(timestamp: string): string {
|
|
82
|
-
const match = /^(\d{4}-\d{2}-\d{2})/.exec(timestamp);
|
|
83
|
-
if (match?.[1]) {
|
|
84
|
-
return match[1];
|
|
85
|
-
}
|
|
86
|
-
return "";
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Build a detail string for a timeline event based on its type and fields.
|
|
91
|
-
*/
|
|
92
|
-
function buildEventDetail(event: StoredEvent): string {
|
|
93
|
-
const parts: string[] = [];
|
|
94
|
-
|
|
95
|
-
if (event.toolName) {
|
|
96
|
-
parts.push(`tool=${event.toolName}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (event.toolDurationMs !== null) {
|
|
100
|
-
parts.push(`duration=${event.toolDurationMs}ms`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (event.data) {
|
|
104
|
-
try {
|
|
105
|
-
const parsed: unknown = JSON.parse(event.data);
|
|
106
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
107
|
-
const data = parsed as Record<string, unknown>;
|
|
108
|
-
for (const [key, value] of Object.entries(data)) {
|
|
109
|
-
if (value !== null && value !== undefined) {
|
|
110
|
-
const strValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
111
|
-
// Truncate long values
|
|
112
|
-
const truncated = strValue.length > 80 ? `${strValue.slice(0, 77)}...` : strValue;
|
|
113
|
-
parts.push(`${key}=${truncated}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
} catch {
|
|
118
|
-
// data is not valid JSON; show it raw if short enough
|
|
119
|
-
if (event.data.length <= 80) {
|
|
120
|
-
parts.push(event.data);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return parts.join(" ");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Assign a stable color function to each agent based on order of first appearance.
|
|
130
|
-
*/
|
|
131
|
-
function buildAgentColorMap(events: StoredEvent[]): Map<string, ColorFn> {
|
|
132
|
-
const colorMap = new Map<string, ColorFn>();
|
|
133
|
-
for (const event of events) {
|
|
134
|
-
if (!colorMap.has(event.agentName)) {
|
|
135
|
-
const colorIndex = colorMap.size % AGENT_COLORS.length;
|
|
136
|
-
const agentColorFn = AGENT_COLORS[colorIndex];
|
|
137
|
-
if (agentColorFn !== undefined) {
|
|
138
|
-
colorMap.set(event.agentName, agentColorFn);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return colorMap;
|
|
143
|
-
}
|
|
16
|
+
import {
|
|
17
|
+
buildAgentColorMap,
|
|
18
|
+
buildEventDetail,
|
|
19
|
+
formatAbsoluteTime,
|
|
20
|
+
formatDate,
|
|
21
|
+
formatRelativeTime,
|
|
22
|
+
} from "../logging/format.ts";
|
|
23
|
+
import { eventLabel, renderHeader } from "../logging/theme.ts";
|
|
24
|
+
import type { StoredEvent } from "../types.ts";
|
|
144
25
|
|
|
145
26
|
/**
|
|
146
27
|
* Print events as an interleaved timeline with ANSI colors and agent labels.
|
|
@@ -148,8 +29,7 @@ function buildAgentColorMap(events: StoredEvent[]): Map<string, ColorFn> {
|
|
|
148
29
|
function printReplay(events: StoredEvent[], useAbsoluteTime: boolean): void {
|
|
149
30
|
const w = process.stdout.write.bind(process.stdout);
|
|
150
31
|
|
|
151
|
-
w(`${
|
|
152
|
-
w(`${"=".repeat(70)}\n`);
|
|
32
|
+
w(`${renderHeader("Replay")}\n`);
|
|
153
33
|
|
|
154
34
|
if (events.length === 0) {
|
|
155
35
|
w(`${color.dim("No events found.")}\n`);
|
|
@@ -176,10 +56,7 @@ function printReplay(events: StoredEvent[], useAbsoluteTime: boolean): void {
|
|
|
176
56
|
? formatAbsoluteTime(event.createdAt)
|
|
177
57
|
: formatRelativeTime(event.createdAt);
|
|
178
58
|
|
|
179
|
-
const
|
|
180
|
-
label: event.eventType.padEnd(10),
|
|
181
|
-
color: color.gray,
|
|
182
|
-
};
|
|
59
|
+
const label = eventLabel(event.eventType);
|
|
183
60
|
|
|
184
61
|
const levelColorFn =
|
|
185
62
|
event.level === "error" ? color.red : event.level === "warn" ? color.yellow : null;
|
|
@@ -193,7 +70,7 @@ function printReplay(events: StoredEvent[], useAbsoluteTime: boolean): void {
|
|
|
193
70
|
|
|
194
71
|
w(
|
|
195
72
|
`${color.dim(timeStr.padStart(10))} ` +
|
|
196
|
-
`${applyLevel(
|
|
73
|
+
`${applyLevel(label.color(color.bold(label.full)))}` +
|
|
197
74
|
`${agentLabel}${detailSuffix}\n`,
|
|
198
75
|
);
|
|
199
76
|
}
|
package/src/commands/run.test.ts
CHANGED
package/src/commands/run.ts
CHANGED
|
@@ -17,24 +17,11 @@ import { loadConfig } from "../config.ts";
|
|
|
17
17
|
import { ValidationError } from "../errors.ts";
|
|
18
18
|
import { jsonError, jsonOutput } from "../json.ts";
|
|
19
19
|
import { accent, printError, printHint, printSuccess } from "../logging/color.ts";
|
|
20
|
+
import { formatDuration } from "../logging/format.ts";
|
|
21
|
+
import { renderHeader, separator } from "../logging/theme.ts";
|
|
20
22
|
import { createRunStore, createSessionStore } from "../sessions/store.ts";
|
|
21
23
|
import type { AgentSession, Run } from "../types.ts";
|
|
22
24
|
|
|
23
|
-
/**
|
|
24
|
-
* Format milliseconds as human-readable duration.
|
|
25
|
-
*/
|
|
26
|
-
function formatDuration(ms: number): string {
|
|
27
|
-
if (ms === 0) return "0s";
|
|
28
|
-
const seconds = Math.floor(ms / 1000);
|
|
29
|
-
if (seconds < 60) return `${seconds}s`;
|
|
30
|
-
const minutes = Math.floor(seconds / 60);
|
|
31
|
-
const remainSec = seconds % 60;
|
|
32
|
-
if (minutes < 60) return `${minutes}m ${remainSec}s`;
|
|
33
|
-
const hours = Math.floor(minutes / 60);
|
|
34
|
-
const remainMin = minutes % 60;
|
|
35
|
-
return `${hours}h ${remainMin}m`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
25
|
/**
|
|
39
26
|
* Get the path to the current-run.txt file.
|
|
40
27
|
*/
|
|
@@ -109,8 +96,7 @@ async function showCurrentRun(overstoryDir: string, json: boolean): Promise<void
|
|
|
109
96
|
return;
|
|
110
97
|
}
|
|
111
98
|
|
|
112
|
-
process.stdout.write("Current Run\n
|
|
113
|
-
process.stdout.write(`${"=".repeat(50)}\n`);
|
|
99
|
+
process.stdout.write(`${renderHeader("Current Run")}\n`);
|
|
114
100
|
process.stdout.write(` ID: ${accent(run.id)}\n`);
|
|
115
101
|
process.stdout.write(` Status: ${run.status}\n`);
|
|
116
102
|
process.stdout.write(` Started: ${run.startedAt}\n`);
|
|
@@ -151,12 +137,11 @@ async function listRuns(overstoryDir: string, limit: number, json: boolean): Pro
|
|
|
151
137
|
return;
|
|
152
138
|
}
|
|
153
139
|
|
|
154
|
-
process.stdout.write("Recent Runs\n
|
|
155
|
-
process.stdout.write(`${"=".repeat(70)}\n`);
|
|
140
|
+
process.stdout.write(`${renderHeader("Recent Runs")}\n`);
|
|
156
141
|
process.stdout.write(
|
|
157
142
|
`${"ID".padEnd(36)} ${"Status".padEnd(10)} ${"Agents".padEnd(7)} Duration\n`,
|
|
158
143
|
);
|
|
159
|
-
process.stdout.write(`${
|
|
144
|
+
process.stdout.write(`${separator()}\n`);
|
|
160
145
|
|
|
161
146
|
for (const run of runs) {
|
|
162
147
|
const id = accent(run.id.length > 35 ? `${run.id.slice(0, 32)}...` : run.id.padEnd(36));
|
|
@@ -245,8 +230,7 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
|
|
|
245
230
|
return;
|
|
246
231
|
}
|
|
247
232
|
|
|
248
|
-
process.stdout.write("Run Details\n
|
|
249
|
-
process.stdout.write(`${"=".repeat(60)}\n`);
|
|
233
|
+
process.stdout.write(`${renderHeader("Run Details")}\n`);
|
|
250
234
|
process.stdout.write(` ID: ${accent(run.id)}\n`);
|
|
251
235
|
process.stdout.write(` Status: ${run.status}\n`);
|
|
252
236
|
process.stdout.write(` Started: ${run.startedAt}\n`);
|
|
@@ -258,7 +242,7 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
|
|
|
258
242
|
|
|
259
243
|
if (agents.length > 0) {
|
|
260
244
|
process.stdout.write(`\nAgents (${agents.length}):\n`);
|
|
261
|
-
process.stdout.write(`${
|
|
245
|
+
process.stdout.write(`${separator()}\n`);
|
|
262
246
|
for (const agent of agents) {
|
|
263
247
|
const agentDuration = formatAgentDuration(agent);
|
|
264
248
|
process.stdout.write(
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { resolveModel, resolveProviderEnv } from "../agents/manifest.ts";
|
|
3
3
|
import { HierarchyError } from "../errors.ts";
|
|
4
|
+
import { ClaudeRuntime } from "../runtimes/claude.ts";
|
|
5
|
+
import { getRuntime } from "../runtimes/registry.ts";
|
|
4
6
|
import type { AgentManifest, OverstoryConfig } from "../types.ts";
|
|
5
7
|
import {
|
|
6
8
|
type AutoDispatchOptions,
|
|
@@ -367,6 +369,7 @@ function makeBeaconOpts(overrides?: Partial<BeaconOptions>): BeaconOptions {
|
|
|
367
369
|
taskId: "overstory-abc",
|
|
368
370
|
parentAgent: null,
|
|
369
371
|
depth: 0,
|
|
372
|
+
instructionPath: ".claude/CLAUDE.md",
|
|
370
373
|
...overrides,
|
|
371
374
|
};
|
|
372
375
|
}
|
|
@@ -407,12 +410,20 @@ describe("buildBeacon", () => {
|
|
|
407
410
|
const opts = makeBeaconOpts({ agentName: "scout-1", taskId: "overstory-xyz" });
|
|
408
411
|
const beacon = buildBeacon(opts);
|
|
409
412
|
|
|
410
|
-
expect(beacon).toContain(
|
|
413
|
+
expect(beacon).toContain(`read ${opts.instructionPath}`);
|
|
411
414
|
expect(beacon).toContain("mulch prime");
|
|
412
415
|
expect(beacon).toContain("ov mail check --agent scout-1");
|
|
413
416
|
expect(beacon).toContain("begin task overstory-xyz");
|
|
414
417
|
});
|
|
415
418
|
|
|
419
|
+
test("uses custom instructionPath in startup instructions", () => {
|
|
420
|
+
const opts = makeBeaconOpts({ instructionPath: "AGENTS.md" });
|
|
421
|
+
const beacon = buildBeacon(opts);
|
|
422
|
+
|
|
423
|
+
expect(beacon).toContain("read AGENTS.md");
|
|
424
|
+
expect(beacon).not.toContain(".claude/CLAUDE.md");
|
|
425
|
+
});
|
|
426
|
+
|
|
416
427
|
test("uses agent name in mail check command", () => {
|
|
417
428
|
const beacon = buildBeacon(makeBeaconOpts({ agentName: "reviewer-beta" }));
|
|
418
429
|
|
|
@@ -999,6 +1010,7 @@ function makeAutoDispatchOpts(overrides?: Partial<AutoDispatchOptions>): AutoDis
|
|
|
999
1010
|
capability: "builder",
|
|
1000
1011
|
specPath: "/path/to/spec.md",
|
|
1001
1012
|
parentAgent: "lead-alpha",
|
|
1013
|
+
instructionPath: ".claude/CLAUDE.md",
|
|
1002
1014
|
...overrides,
|
|
1003
1015
|
};
|
|
1004
1016
|
}
|
|
@@ -1011,6 +1023,7 @@ describe("buildAutoDispatch", () => {
|
|
|
1011
1023
|
capability: "builder",
|
|
1012
1024
|
specPath: "/path/to/spec.md",
|
|
1013
1025
|
parentAgent: "lead-alpha",
|
|
1026
|
+
instructionPath: ".claude/CLAUDE.md",
|
|
1014
1027
|
});
|
|
1015
1028
|
expect(dispatch.from).toBe("lead-alpha");
|
|
1016
1029
|
expect(dispatch.to).toBe("builder-1");
|
|
@@ -1025,6 +1038,7 @@ describe("buildAutoDispatch", () => {
|
|
|
1025
1038
|
capability: "lead",
|
|
1026
1039
|
specPath: null,
|
|
1027
1040
|
parentAgent: null,
|
|
1041
|
+
instructionPath: ".claude/CLAUDE.md",
|
|
1028
1042
|
});
|
|
1029
1043
|
expect(dispatch.from).toBe("orchestrator");
|
|
1030
1044
|
expect(dispatch.body).toContain("No spec file");
|
|
@@ -1037,6 +1051,7 @@ describe("buildAutoDispatch", () => {
|
|
|
1037
1051
|
capability: "scout",
|
|
1038
1052
|
specPath: null,
|
|
1039
1053
|
parentAgent: "lead-alpha",
|
|
1054
|
+
instructionPath: ".claude/CLAUDE.md",
|
|
1040
1055
|
});
|
|
1041
1056
|
expect(dispatch.body).toContain("scout");
|
|
1042
1057
|
});
|
|
@@ -1048,6 +1063,7 @@ describe("buildAutoDispatch", () => {
|
|
|
1048
1063
|
capability: "builder",
|
|
1049
1064
|
specPath: "/abs/path/to/spec.md",
|
|
1050
1065
|
parentAgent: "lead-alpha",
|
|
1066
|
+
instructionPath: ".claude/CLAUDE.md",
|
|
1051
1067
|
});
|
|
1052
1068
|
expect(dispatch.body).toContain("/abs/path/to/spec.md");
|
|
1053
1069
|
});
|
|
@@ -1080,3 +1096,54 @@ describe("buildAutoDispatch", () => {
|
|
|
1080
1096
|
* Integration coverage: The beacon loop has been validated through production
|
|
1081
1097
|
* agent spawns. Failure mode is agents stuck at welcome screen (overstory-3271).
|
|
1082
1098
|
*/
|
|
1099
|
+
|
|
1100
|
+
describe("sling runtime integration", () => {
|
|
1101
|
+
test("runtime.buildSpawnCommand produces identical command to old hardcoded string", () => {
|
|
1102
|
+
const runtime = getRuntime("claude");
|
|
1103
|
+
const cmd = runtime.buildSpawnCommand({
|
|
1104
|
+
model: "sonnet",
|
|
1105
|
+
permissionMode: "bypass",
|
|
1106
|
+
cwd: "/tmp/worktree",
|
|
1107
|
+
env: {},
|
|
1108
|
+
});
|
|
1109
|
+
expect(cmd).toBe("claude --model sonnet --permission-mode bypassPermissions");
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
test("runtime.buildSpawnCommand with opus model", () => {
|
|
1113
|
+
const runtime = getRuntime("claude");
|
|
1114
|
+
const cmd = runtime.buildSpawnCommand({
|
|
1115
|
+
model: "opus",
|
|
1116
|
+
permissionMode: "bypass",
|
|
1117
|
+
cwd: "/tmp/worktree",
|
|
1118
|
+
env: {},
|
|
1119
|
+
});
|
|
1120
|
+
expect(cmd).toBe("claude --model opus --permission-mode bypassPermissions");
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
test("runtime.buildEnv returns empty object for native model", () => {
|
|
1124
|
+
const runtime = new ClaudeRuntime();
|
|
1125
|
+
const env = runtime.buildEnv({ model: "sonnet" });
|
|
1126
|
+
expect(env).toEqual({});
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
test("runtime.buildEnv passes through provider env vars", () => {
|
|
1130
|
+
const runtime = new ClaudeRuntime();
|
|
1131
|
+
const env = runtime.buildEnv({
|
|
1132
|
+
model: "sonnet",
|
|
1133
|
+
env: { ANTHROPIC_BASE_URL: "https://example.com" },
|
|
1134
|
+
});
|
|
1135
|
+
expect(env).toEqual({ ANTHROPIC_BASE_URL: "https://example.com" });
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
test("runtime.detectReady returns ready for idle Claude prompt", () => {
|
|
1139
|
+
const runtime = new ClaudeRuntime();
|
|
1140
|
+
const state = runtime.detectReady('Try "hello world"\n\nbypass permissions');
|
|
1141
|
+
expect(state.phase).toBe("ready");
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
test("runtime.detectReady returns loading when agent is processing", () => {
|
|
1145
|
+
const runtime = new ClaudeRuntime();
|
|
1146
|
+
const state = runtime.detectReady("Running tool: Read\nbypass permissions");
|
|
1147
|
+
expect(state.phase).toBe("loading");
|
|
1148
|
+
});
|
|
1149
|
+
});
|