@katyella/legio 0.1.0
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/CHANGELOG.md +422 -0
- package/LICENSE +21 -0
- package/README.md +555 -0
- package/agents/builder.md +141 -0
- package/agents/coordinator.md +351 -0
- package/agents/cto.md +196 -0
- package/agents/gateway.md +276 -0
- package/agents/lead.md +281 -0
- package/agents/merger.md +156 -0
- package/agents/monitor.md +212 -0
- package/agents/reviewer.md +142 -0
- package/agents/scout.md +131 -0
- package/agents/supervisor.md +416 -0
- package/bin/legio.mjs +38 -0
- package/package.json +77 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +102 -0
- package/src/agents/hooks-deployer.test.ts +1820 -0
- package/src/agents/hooks-deployer.ts +574 -0
- package/src/agents/identity.test.ts +614 -0
- package/src/agents/identity.ts +385 -0
- package/src/agents/lifecycle.test.ts +202 -0
- package/src/agents/lifecycle.ts +184 -0
- package/src/agents/manifest.test.ts +558 -0
- package/src/agents/manifest.ts +297 -0
- package/src/agents/overlay.test.ts +592 -0
- package/src/agents/overlay.ts +316 -0
- package/src/beads/client.test.ts +210 -0
- package/src/beads/client.ts +227 -0
- package/src/beads/molecules.test.ts +320 -0
- package/src/beads/molecules.ts +209 -0
- package/src/commands/agents.test.ts +325 -0
- package/src/commands/agents.ts +286 -0
- package/src/commands/clean.test.ts +730 -0
- package/src/commands/clean.ts +653 -0
- package/src/commands/completions.test.ts +346 -0
- package/src/commands/completions.ts +950 -0
- package/src/commands/coordinator.test.ts +1524 -0
- package/src/commands/coordinator.ts +880 -0
- package/src/commands/costs.test.ts +1015 -0
- package/src/commands/costs.ts +473 -0
- package/src/commands/dashboard.test.ts +94 -0
- package/src/commands/dashboard.ts +607 -0
- package/src/commands/doctor.test.ts +295 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/down.test.ts +308 -0
- package/src/commands/down.ts +124 -0
- package/src/commands/errors.test.ts +648 -0
- package/src/commands/errors.ts +255 -0
- package/src/commands/feed.test.ts +579 -0
- package/src/commands/feed.ts +368 -0
- package/src/commands/gateway.test.ts +698 -0
- package/src/commands/gateway.ts +419 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +539 -0
- package/src/commands/hooks.test.ts +292 -0
- package/src/commands/hooks.ts +210 -0
- package/src/commands/init.test.ts +211 -0
- package/src/commands/init.ts +622 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +455 -0
- package/src/commands/log.test.ts +1556 -0
- package/src/commands/log.ts +752 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +544 -0
- package/src/commands/mail.test.ts +1726 -0
- package/src/commands/mail.ts +926 -0
- package/src/commands/merge.test.ts +676 -0
- package/src/commands/merge.ts +374 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +150 -0
- package/src/commands/monitor.test.ts +151 -0
- package/src/commands/monitor.ts +394 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +373 -0
- package/src/commands/prime.test.ts +467 -0
- package/src/commands/prime.ts +386 -0
- package/src/commands/replay.test.ts +742 -0
- package/src/commands/replay.ts +367 -0
- package/src/commands/run.test.ts +443 -0
- package/src/commands/run.ts +365 -0
- package/src/commands/server.test.ts +626 -0
- package/src/commands/server.ts +298 -0
- package/src/commands/sling.test.ts +810 -0
- package/src/commands/sling.ts +700 -0
- package/src/commands/spec.test.ts +206 -0
- package/src/commands/spec.ts +171 -0
- package/src/commands/status.test.ts +276 -0
- package/src/commands/status.ts +339 -0
- package/src/commands/stop.test.ts +357 -0
- package/src/commands/stop.ts +119 -0
- package/src/commands/supervisor.test.ts +186 -0
- package/src/commands/supervisor.ts +544 -0
- package/src/commands/trace.test.ts +746 -0
- package/src/commands/trace.ts +332 -0
- package/src/commands/up.test.ts +597 -0
- package/src/commands/up.ts +275 -0
- package/src/commands/watch.test.ts +152 -0
- package/src/commands/watch.ts +238 -0
- package/src/commands/worktree.test.ts +648 -0
- package/src/commands/worktree.ts +266 -0
- package/src/config.test.ts +496 -0
- package/src/config.ts +616 -0
- package/src/doctor/agents.test.ts +448 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +184 -0
- package/src/doctor/config-check.ts +185 -0
- package/src/doctor/consistency.test.ts +645 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +284 -0
- package/src/doctor/databases.ts +211 -0
- package/src/doctor/dependencies.test.ts +150 -0
- package/src/doctor/dependencies.ts +179 -0
- package/src/doctor/logs.test.ts +244 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +210 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +285 -0
- package/src/doctor/structure.ts +195 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +130 -0
- package/src/doctor/version.ts +131 -0
- package/src/e2e/chat-flow.test.ts +346 -0
- package/src/e2e/init-sling-lifecycle.test.ts +288 -0
- package/src/errors.test.ts +21 -0
- package/src/errors.ts +246 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +344 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/global-setup.ts +14 -0
- package/src/index.ts +339 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +118 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +812 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +258 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +873 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +815 -0
- package/src/mail/store.ts +402 -0
- package/src/merge/queue.test.ts +449 -0
- package/src/merge/queue.ts +262 -0
- package/src/merge/resolver.test.ts +1453 -0
- package/src/merge/resolver.ts +759 -0
- package/src/metrics/store.test.ts +1167 -0
- package/src/metrics/store.ts +511 -0
- package/src/metrics/summary.test.ts +397 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +643 -0
- package/src/metrics/transcript.ts +351 -0
- package/src/mulch/client.test.ts +547 -0
- package/src/mulch/client.ts +416 -0
- package/src/server/audit-store.test.ts +384 -0
- package/src/server/audit-store.ts +257 -0
- package/src/server/headless.test.ts +180 -0
- package/src/server/headless.ts +151 -0
- package/src/server/index.test.ts +241 -0
- package/src/server/index.ts +317 -0
- package/src/server/public/app.js +187 -0
- package/src/server/public/apple-touch-icon.png +0 -0
- package/src/server/public/components/agent-badge.js +37 -0
- package/src/server/public/components/data-table.js +114 -0
- package/src/server/public/components/gateway-chat.js +256 -0
- package/src/server/public/components/issue-card.js +96 -0
- package/src/server/public/components/layout.js +88 -0
- package/src/server/public/components/message-bubble.js +120 -0
- package/src/server/public/components/stat-card.js +26 -0
- package/src/server/public/components/terminal-panel.js +140 -0
- package/src/server/public/favicon-16.png +0 -0
- package/src/server/public/favicon-32.png +0 -0
- package/src/server/public/favicon.ico +0 -0
- package/src/server/public/favicon.png +0 -0
- package/src/server/public/index.html +64 -0
- package/src/server/public/lib/api.js +35 -0
- package/src/server/public/lib/markdown.js +8 -0
- package/src/server/public/lib/preact-setup.js +8 -0
- package/src/server/public/lib/state.js +99 -0
- package/src/server/public/lib/utils.js +309 -0
- package/src/server/public/lib/ws.js +79 -0
- package/src/server/public/views/chat.js +983 -0
- package/src/server/public/views/costs.js +692 -0
- package/src/server/public/views/dashboard.js +781 -0
- package/src/server/public/views/gateway-chat.js +622 -0
- package/src/server/public/views/inspect.js +399 -0
- package/src/server/public/views/issues.js +470 -0
- package/src/server/public/views/setup.js +94 -0
- package/src/server/public/views/task-detail.js +422 -0
- package/src/server/routes.test.ts +3816 -0
- package/src/server/routes.ts +1964 -0
- package/src/server/websocket.test.ts +288 -0
- package/src/server/websocket.ts +196 -0
- package/src/sessions/compat.test.ts +109 -0
- package/src/sessions/compat.ts +17 -0
- package/src/sessions/store.test.ts +969 -0
- package/src/sessions/store.ts +480 -0
- package/src/test-helpers.test.ts +97 -0
- package/src/test-helpers.ts +143 -0
- package/src/types.ts +708 -0
- package/src/watchdog/daemon.test.ts +1233 -0
- package/src/watchdog/daemon.ts +533 -0
- package/src/watchdog/health.test.ts +371 -0
- package/src/watchdog/health.ts +248 -0
- package/src/watchdog/triage.test.ts +162 -0
- package/src/watchdog/triage.ts +193 -0
- package/src/worktree/manager.test.ts +444 -0
- package/src/worktree/manager.ts +224 -0
- package/src/worktree/tmux.test.ts +1238 -0
- package/src/worktree/tmux.ts +644 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +132 -0
- package/templates/overlay.md.tmpl +79 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Tier 1 AI-assisted triage.
|
|
3
|
+
* classifyResponse and buildTriagePrompt are pure functions — tested directly.
|
|
4
|
+
* triageAgent uses real filesystem (temp dirs). Claude spawn is expected to
|
|
5
|
+
* fail in test environments, exercising the fallback-to-extend path.
|
|
6
|
+
* spawnClaude is NOT mocked — we rely on it failing naturally in tests.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
13
|
+
import { buildTriagePrompt, classifyResponse, triageAgent } from "./triage.ts";
|
|
14
|
+
|
|
15
|
+
describe("classifyResponse", () => {
|
|
16
|
+
test("returns 'retry' when response contains 'retry'", () => {
|
|
17
|
+
const result = classifyResponse("The operation should retry.");
|
|
18
|
+
expect(result).toBe("retry");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("returns 'retry' when response contains 'recoverable'", () => {
|
|
22
|
+
const result = classifyResponse("This error is recoverable.");
|
|
23
|
+
expect(result).toBe("retry");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("returns 'terminate' when response contains 'terminate'", () => {
|
|
27
|
+
const result = classifyResponse("You should terminate the agent.");
|
|
28
|
+
expect(result).toBe("terminate");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("returns 'terminate' when response contains 'fatal'", () => {
|
|
32
|
+
const result = classifyResponse("This is a fatal error.");
|
|
33
|
+
expect(result).toBe("terminate");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("returns 'terminate' when response contains 'failed'", () => {
|
|
37
|
+
const result = classifyResponse("The operation has failed.");
|
|
38
|
+
expect(result).toBe("terminate");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("handles mixed case (e.g., 'RETRY', 'Fatal')", () => {
|
|
42
|
+
expect(classifyResponse("RETRY this operation")).toBe("retry");
|
|
43
|
+
expect(classifyResponse("Fatal error occurred")).toBe("terminate");
|
|
44
|
+
expect(classifyResponse("RecOverAble issue")).toBe("retry");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("returns 'extend' when response contains none of the keywords", () => {
|
|
48
|
+
const result = classifyResponse("The agent is processing data.");
|
|
49
|
+
expect(result).toBe("extend");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("returns 'extend' for empty string", () => {
|
|
53
|
+
const result = classifyResponse("");
|
|
54
|
+
expect(result).toBe("extend");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("first match wins when response has multiple keywords", () => {
|
|
58
|
+
// 'retry' is checked before 'terminate'
|
|
59
|
+
const result = classifyResponse("retry this but it may terminate later");
|
|
60
|
+
expect(result).toBe("retry");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("buildTriagePrompt", () => {
|
|
65
|
+
test("contains agent name in output", () => {
|
|
66
|
+
const prompt = buildTriagePrompt("test-agent", "2026-02-13T10:00:00Z", "log content");
|
|
67
|
+
expect(prompt).toContain("test-agent");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("contains lastActivity timestamp in output", () => {
|
|
71
|
+
const timestamp = "2026-02-13T10:00:00Z";
|
|
72
|
+
const prompt = buildTriagePrompt("test-agent", timestamp, "log content");
|
|
73
|
+
expect(prompt).toContain(timestamp);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("contains log content wrapped in code fences", () => {
|
|
77
|
+
const logContent = "Error: something went wrong\nat line 42";
|
|
78
|
+
const prompt = buildTriagePrompt("test-agent", "2026-02-13T10:00:00Z", logContent);
|
|
79
|
+
expect(prompt).toContain("```");
|
|
80
|
+
expect(prompt).toContain(logContent);
|
|
81
|
+
expect(prompt.split("```").length).toBeGreaterThanOrEqual(3); // Opening and closing fences
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("contains classification instructions (retry/terminate/extend)", () => {
|
|
85
|
+
const prompt = buildTriagePrompt("test-agent", "2026-02-13T10:00:00Z", "log content");
|
|
86
|
+
expect(prompt).toContain("retry");
|
|
87
|
+
expect(prompt).toContain("terminate");
|
|
88
|
+
expect(prompt).toContain("extend");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("triageAgent", () => {
|
|
93
|
+
let tempRoot: string;
|
|
94
|
+
|
|
95
|
+
beforeEach(async () => {
|
|
96
|
+
tempRoot = await mkdtemp(join(tmpdir(), "triage-test-"));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(async () => {
|
|
100
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("returns 'extend' when no logs directory exists", async () => {
|
|
104
|
+
const result = await triageAgent({
|
|
105
|
+
agentName: "test-agent",
|
|
106
|
+
root: tempRoot,
|
|
107
|
+
lastActivity: "2026-02-13T10:00:00Z",
|
|
108
|
+
});
|
|
109
|
+
expect(result).toBe("extend");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("returns 'extend' when logs directory exists but is empty", async () => {
|
|
113
|
+
const logsDir = join(tempRoot, ".legio", "logs", "test-agent");
|
|
114
|
+
await mkdir(logsDir, { recursive: true });
|
|
115
|
+
|
|
116
|
+
const result = await triageAgent({
|
|
117
|
+
agentName: "test-agent",
|
|
118
|
+
root: tempRoot,
|
|
119
|
+
lastActivity: "2026-02-13T10:00:00Z",
|
|
120
|
+
});
|
|
121
|
+
expect(result).toBe("extend");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("returns 'extend' when logs directory has session dir but no session.log", async () => {
|
|
125
|
+
const logsDir = join(tempRoot, ".legio", "logs", "test-agent", "2026-02-13T10-00-00");
|
|
126
|
+
await mkdir(logsDir, { recursive: true });
|
|
127
|
+
await writeFile(join(logsDir, ".gitkeep"), "", "utf-8");
|
|
128
|
+
|
|
129
|
+
const result = await triageAgent({
|
|
130
|
+
agentName: "test-agent",
|
|
131
|
+
root: tempRoot,
|
|
132
|
+
lastActivity: "2026-02-13T10:00:00Z",
|
|
133
|
+
});
|
|
134
|
+
expect(result).toBe("extend");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("returns 'extend' when session.log exists but claude binary fails", async () => {
|
|
138
|
+
const timestamp = "2026-02-13T10-00-00";
|
|
139
|
+
const sessionLogDir = join(tempRoot, ".legio", "logs", "test-agent", timestamp);
|
|
140
|
+
const sessionLogPath = join(sessionLogDir, "session.log");
|
|
141
|
+
|
|
142
|
+
await mkdir(sessionLogDir, { recursive: true });
|
|
143
|
+
|
|
144
|
+
// Create session.log with some content
|
|
145
|
+
await writeFile(
|
|
146
|
+
sessionLogPath,
|
|
147
|
+
"Agent started\nProcessing data\nError: something went wrong\n",
|
|
148
|
+
"utf-8",
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// triageAgent will try to spawn claude which should fail or be killed by timeout.
|
|
152
|
+
// Short timeout ensures the test doesn't hang even if the claude binary
|
|
153
|
+
// exists on the system (e.g., inside a Claude Code session).
|
|
154
|
+
const result = await triageAgent({
|
|
155
|
+
agentName: "test-agent",
|
|
156
|
+
root: tempRoot,
|
|
157
|
+
lastActivity: "2026-02-13T10:00:00Z",
|
|
158
|
+
timeoutMs: 500,
|
|
159
|
+
});
|
|
160
|
+
expect(result).toBe("extend");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 1 AI-assisted failure classification for stalled agents.
|
|
3
|
+
*
|
|
4
|
+
* When an agent is detected as stalled, triage reads recent log entries and
|
|
5
|
+
* uses Claude to classify the situation as recoverable, fatal, or long-running.
|
|
6
|
+
* Falls back to "extend" if Claude is unavailable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { AgentError } from "../errors.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Triage a stalled agent by analyzing its recent log output with Claude.
|
|
16
|
+
*
|
|
17
|
+
* Steps:
|
|
18
|
+
* 1. Find the most recent session log directory for the agent
|
|
19
|
+
* 2. Read the last 50 lines of session.log
|
|
20
|
+
* 3. Ask Claude to classify the situation
|
|
21
|
+
* 4. Parse the response to determine action
|
|
22
|
+
*
|
|
23
|
+
* @param options.agentName - Name of the agent to triage
|
|
24
|
+
* @param options.root - Project root directory (contains .legio/)
|
|
25
|
+
* @param options.lastActivity - ISO timestamp of the agent's last recorded activity
|
|
26
|
+
* @returns "retry" if recoverable, "terminate" if fatal, "extend" if likely long-running
|
|
27
|
+
*/
|
|
28
|
+
export async function triageAgent(options: {
|
|
29
|
+
agentName: string;
|
|
30
|
+
root: string;
|
|
31
|
+
lastActivity: string;
|
|
32
|
+
/** Timeout in ms for the Claude subprocess. Defaults to 30_000 (30s). */
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
}): Promise<"retry" | "terminate" | "extend"> {
|
|
35
|
+
const { agentName, root, lastActivity, timeoutMs } = options;
|
|
36
|
+
const logsDir = join(root, ".legio", "logs", agentName);
|
|
37
|
+
|
|
38
|
+
let logContent: string;
|
|
39
|
+
try {
|
|
40
|
+
logContent = await readRecentLog(logsDir);
|
|
41
|
+
} catch {
|
|
42
|
+
// No logs available — assume long-running operation
|
|
43
|
+
return "extend";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const prompt = buildTriagePrompt(agentName, lastActivity, logContent);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const response = await spawnClaude(prompt, timeoutMs);
|
|
50
|
+
return classifyResponse(response);
|
|
51
|
+
} catch {
|
|
52
|
+
// Claude not available — default to extend (safe fallback)
|
|
53
|
+
return "extend";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read the last 50 lines of the most recent session.log for an agent.
|
|
59
|
+
*
|
|
60
|
+
* @param logsDir - Path to the agent's logs directory (e.g., .legio/logs/{agent}/)
|
|
61
|
+
* @returns The last 50 lines of the session log as a string
|
|
62
|
+
* @throws AgentError if no log directories or session.log are found
|
|
63
|
+
*/
|
|
64
|
+
async function readRecentLog(logsDir: string): Promise<string> {
|
|
65
|
+
let entries: string[];
|
|
66
|
+
try {
|
|
67
|
+
entries = await readdir(logsDir);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new AgentError(`No log directory found at ${logsDir}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (entries.length === 0) {
|
|
73
|
+
throw new AgentError(`No session directories in ${logsDir}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Session directories are named with timestamps — sort descending to get most recent
|
|
77
|
+
const sorted = entries.sort().reverse();
|
|
78
|
+
const mostRecent = sorted[0];
|
|
79
|
+
if (mostRecent === undefined) {
|
|
80
|
+
throw new AgentError(`No session directories in ${logsDir}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const logPath = join(logsDir, mostRecent, "session.log");
|
|
84
|
+
let content: string;
|
|
85
|
+
try {
|
|
86
|
+
content = await readFile(logPath, "utf-8");
|
|
87
|
+
} catch {
|
|
88
|
+
throw new AgentError(`No session.log found at ${logPath}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const lines = content.split("\n");
|
|
92
|
+
|
|
93
|
+
// Take the last 50 non-empty lines
|
|
94
|
+
const tail = lines.slice(-50).join("\n");
|
|
95
|
+
return tail;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build the triage prompt for Claude analysis.
|
|
100
|
+
*/
|
|
101
|
+
export function buildTriagePrompt(
|
|
102
|
+
agentName: string,
|
|
103
|
+
lastActivity: string,
|
|
104
|
+
logContent: string,
|
|
105
|
+
): string {
|
|
106
|
+
return [
|
|
107
|
+
"Analyze this agent log and classify the situation.",
|
|
108
|
+
`Agent: ${agentName}`,
|
|
109
|
+
`Last activity: ${lastActivity}`,
|
|
110
|
+
"",
|
|
111
|
+
"Respond with exactly one word: 'retry' if the error is recoverable,",
|
|
112
|
+
"'terminate' if the error is fatal or the agent has failed,",
|
|
113
|
+
"or 'extend' if this looks like a long-running operation.",
|
|
114
|
+
"",
|
|
115
|
+
"Log content:",
|
|
116
|
+
"```",
|
|
117
|
+
logContent,
|
|
118
|
+
"```",
|
|
119
|
+
].join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Default timeout for Claude subprocess: 30 seconds */
|
|
123
|
+
const DEFAULT_TRIAGE_TIMEOUT_MS = 30_000;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Spawn Claude in non-interactive mode to analyze the log.
|
|
127
|
+
*
|
|
128
|
+
* @param prompt - The analysis prompt
|
|
129
|
+
* @param timeoutMs - Timeout in ms for the subprocess (default 30s)
|
|
130
|
+
* @returns Claude's response text
|
|
131
|
+
* @throws Error if claude is not installed, the process fails, or the timeout is reached
|
|
132
|
+
*/
|
|
133
|
+
async function spawnClaude(prompt: string, timeoutMs?: number): Promise<string> {
|
|
134
|
+
const timeout = timeoutMs ?? DEFAULT_TRIAGE_TIMEOUT_MS;
|
|
135
|
+
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const proc = spawn("claude", ["--print", "-p", prompt], {
|
|
138
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (proc.stdout === null || proc.stderr === null) {
|
|
142
|
+
reject(new AgentError("spawn failed to create stdio pipes"));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const timer = globalThis.setTimeout(() => {
|
|
147
|
+
proc.kill();
|
|
148
|
+
}, timeout);
|
|
149
|
+
|
|
150
|
+
const stdoutChunks: Buffer[] = [];
|
|
151
|
+
const stderrChunks: Buffer[] = [];
|
|
152
|
+
|
|
153
|
+
proc.stdout.on("data", (data: Buffer) => stdoutChunks.push(data));
|
|
154
|
+
proc.stderr.on("data", (data: Buffer) => stderrChunks.push(data));
|
|
155
|
+
|
|
156
|
+
proc.on("close", (code) => {
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
const stdout = Buffer.concat(stdoutChunks).toString().trim();
|
|
159
|
+
if (code !== 0) {
|
|
160
|
+
const stderr = Buffer.concat(stderrChunks).toString().trim();
|
|
161
|
+
reject(new AgentError(`Claude triage failed (exit ${code}): ${stderr}`));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
resolve(stdout);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
proc.on("error", (err) => {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
reject(err);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Classify Claude's response into a triage action.
|
|
176
|
+
*
|
|
177
|
+
* @param response - Claude's raw response text
|
|
178
|
+
* @returns "retry" | "terminate" | "extend"
|
|
179
|
+
*/
|
|
180
|
+
export function classifyResponse(response: string): "retry" | "terminate" | "extend" {
|
|
181
|
+
const lower = response.toLowerCase();
|
|
182
|
+
|
|
183
|
+
if (lower.includes("retry") || lower.includes("recoverable")) {
|
|
184
|
+
return "retry";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (lower.includes("terminate") || lower.includes("fatal") || lower.includes("failed")) {
|
|
188
|
+
return "terminate";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Default: assume long-running operation
|
|
192
|
+
return "extend";
|
|
193
|
+
}
|