openhermes 4.12.1 → 4.13.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.
Files changed (73) hide show
  1. package/CONTEXT.md +6 -6
  2. package/ETHOS.md +2 -2
  3. package/README.md +11 -17
  4. package/bootstrap.ts +118 -126
  5. package/docs/HOW-IT-WORKS.md +162 -0
  6. package/docs/adr/ADR-0001-rebuild-vs-increment.md +30 -0
  7. package/docs/adr/ADR-0002-routing-graph-vs-linear-chain.md +36 -0
  8. package/docs/adr/ADR-0003-per-directory-plan-storage.md +34 -0
  9. package/docs/adr/ADR-0004-composer-fragment-architecture.md +42 -0
  10. package/docs/adr/ADR-0005-hook-system-design.md +42 -0
  11. package/docs/adr/README.md +9 -0
  12. package/harness/codex/AUTOPILOT.md +35 -40
  13. package/harness/codex/CHARTER.md +3 -3
  14. package/harness/lib/composer/compose.test.ts +29 -29
  15. package/harness/lib/composer/fragments/02-delegation.md +5 -5
  16. package/harness/lib/composer/fragments/04-task-flow.md +13 -13
  17. package/harness/lib/composer/fragments/08-routing.md +1 -1
  18. package/harness/lib/composer/fragments/09-guardrails.md +25 -25
  19. package/harness/lib/composer/index.ts +1 -1
  20. package/harness/lib/guards/guard-config.ts +72 -72
  21. package/harness/lib/hooks/builtins/confidence-gate-hook.ts +9 -9
  22. package/harness/lib/hooks/builtins/delegation-depth-hook.ts +1 -1
  23. package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -99
  24. package/harness/lib/hooks/builtins/next-route-hook.ts +24 -24
  25. package/harness/lib/hooks/builtins/plan-check-hook.ts +5 -5
  26. package/harness/lib/hooks/builtins/route-tracking-hook.ts +1 -1
  27. package/harness/lib/hooks/hooks.test.ts +160 -324
  28. package/harness/lib/hooks/index.ts +38 -42
  29. package/harness/lib/hooks/registry.ts +309 -416
  30. package/harness/lib/hooks/types.ts +116 -119
  31. package/harness/lib/plans/plan-location.ts +134 -134
  32. package/harness/lib/routing/index.ts +21 -21
  33. package/harness/lib/routing/route-guidance.ts +147 -147
  34. package/harness/lib/routing/route-resolver.ts +58 -58
  35. package/harness/lib/routing/routing.test.ts +195 -195
  36. package/harness/lib/routing/skill-frontmatter.ts +125 -125
  37. package/harness/lib/routing/types.ts +52 -52
  38. package/harness/skills/oh-ascii/SKILL.md +1 -1
  39. package/harness/skills/oh-fusion/DEEP.md +109 -109
  40. package/harness/skills/oh-fusion/SKILL.md +47 -47
  41. package/harness/skills/oh-init/DEEP.md +2 -2
  42. package/harness/skills/oh-plan-review/DEEP.md +1 -1
  43. package/harness/skills/oh-planner/DEEP.md +3 -3
  44. package/harness/skills/oh-review/DEEP.md +5 -5
  45. package/package.json +56 -53
  46. package/harness/lib/background/background.test.ts +0 -216
  47. package/harness/lib/background/index.ts +0 -7
  48. package/harness/lib/background/interfaces.ts +0 -31
  49. package/harness/lib/background/manager.ts +0 -320
  50. package/harness/lib/hooks/builtins/error-recovery-hook.ts +0 -107
  51. package/harness/lib/hooks/builtins/memory-sync-hook.ts +0 -73
  52. package/harness/lib/hooks/builtins/sanity-check-hook.ts +0 -52
  53. package/harness/lib/hooks/builtins/subagent-failure-hook.ts +0 -93
  54. package/harness/lib/memory/index.ts +0 -18
  55. package/harness/lib/memory/interfaces.ts +0 -53
  56. package/harness/lib/memory/memory-manager.ts +0 -205
  57. package/harness/lib/memory/memory.test.ts +0 -485
  58. package/harness/lib/memory/plan-store.ts +0 -346
  59. package/harness/lib/recovery/handler.ts +0 -243
  60. package/harness/lib/recovery/index.ts +0 -14
  61. package/harness/lib/recovery/interfaces.ts +0 -48
  62. package/harness/lib/recovery/patterns.ts +0 -149
  63. package/harness/lib/recovery/recovery.test.ts +0 -312
  64. package/harness/lib/sanity/anomaly-tracker.ts +0 -127
  65. package/harness/lib/sanity/checker.ts +0 -189
  66. package/harness/lib/sanity/index.ts +0 -13
  67. package/harness/lib/sanity/interfaces.ts +0 -24
  68. package/harness/lib/sanity/sanity.test.ts +0 -472
  69. package/harness/lib/sync/file-watcher.ts +0 -175
  70. package/harness/lib/sync/index.ts +0 -11
  71. package/harness/lib/sync/interfaces.ts +0 -27
  72. package/harness/lib/sync/plan-sync.ts +0 -533
  73. package/harness/lib/sync/sync.test.ts +0 -858
@@ -1,216 +0,0 @@
1
- import { describe, it, afterEach } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { BackgroundManager } from "./manager.ts";
4
- import type { BackgroundTaskStatus } from "./interfaces.ts";
5
-
6
- // ---------------------------------------------------------------------------
7
- // Helpers
8
- // ---------------------------------------------------------------------------
9
-
10
- function delay(ms: number): Promise<void> {
11
- return new Promise((resolve) => setTimeout(resolve, ms));
12
- }
13
-
14
- /**
15
- * Poll check() until the task reaches one of the given statuses, or until
16
- * a generous timeout elapses (2.5 s).
17
- */
18
- async function waitForStatus(
19
- manager: BackgroundManager,
20
- id: string,
21
- ...expected: BackgroundTaskStatus[]
22
- ): Promise<void> {
23
- for (let i = 0; i < 50; i++) {
24
- const task = manager.check(id);
25
- if (task && expected.includes(task.status)) return;
26
- await delay(50);
27
- }
28
- const task = manager.check(id);
29
- const actual = task?.status ?? "(not found)";
30
- throw new Error(
31
- `Timed out waiting for status [${expected.join("/")}], got "${actual}"`,
32
- );
33
- }
34
-
35
- // Windows detection — some assertions differ per platform
36
- const IS_WIN = process.platform === "win32";
37
-
38
- // ---------------------------------------------------------------------------
39
- // Tests
40
- // ---------------------------------------------------------------------------
41
-
42
- describe("BackgroundManager", () => {
43
- afterEach(() => {
44
- BackgroundManager.resetInstance();
45
- });
46
-
47
- // ---- 1: run() returns ID immediately ----------------------------------
48
-
49
- it("run() returns a task ID immediately", () => {
50
- const mgr = BackgroundManager.getInstance();
51
- const id = mgr.run({ command: IS_WIN ? "echo" : "echo", args: ["hello"] });
52
- assert.ok(typeof id === "string");
53
- assert.ok(id.length > 0, "id must not be empty");
54
- });
55
-
56
- // ---- 2: check() shows pending → running → completed -------------------
57
-
58
- it("check() transitions pending -> running -> completed", async () => {
59
- const mgr = BackgroundManager.getInstance();
60
- const id = mgr.run({ command: IS_WIN ? "echo" : "echo", args: ["hello"] });
61
-
62
- // Immediately after run() the task should be "pending"
63
- // (spawn is deferred via setImmediate)
64
- const initial = mgr.check(id);
65
- assert.ok(initial, "task must exist immediately");
66
- assert.equal(initial!.status, "pending");
67
-
68
- // Wait for it to complete
69
- await waitForStatus(mgr, id, "completed");
70
- const done = mgr.check(id);
71
- assert.equal(done!.exitCode, 0);
72
- });
73
-
74
- it("resetInstance returns a fresh manager with cleared state", async () => {
75
- const mgr = BackgroundManager.getInstance();
76
- const id = mgr.run({
77
- command: IS_WIN ? "powershell.exe" : "sleep",
78
- args: IS_WIN
79
- ? ["-NoProfile", "-Command", "Start-Sleep -Seconds 30"]
80
- : ["30"],
81
- timeout: 0,
82
- });
83
-
84
- await waitForStatus(mgr, id, "running");
85
-
86
- BackgroundManager.resetInstance();
87
-
88
- const fresh = BackgroundManager.getInstance();
89
- assert.notEqual(fresh, mgr);
90
- assert.equal(fresh.list().length, 0);
91
- });
92
-
93
- // ---- 3: capture stdout -------------------------------------------------
94
-
95
- it("captures stdout from a simple command", async () => {
96
- const mgr = BackgroundManager.getInstance();
97
- const id = mgr.run({
98
- command: IS_WIN ? "echo" : "echo",
99
- args: ["hello-background"],
100
- });
101
-
102
- await waitForStatus(mgr, id, "completed");
103
- const task = mgr.check(id);
104
- assert.ok(task, "task must exist");
105
- assert.match(task!.output, /hello-background/);
106
- });
107
-
108
- // ---- 4: failed command (non-zero exit) ---------------------------------
109
-
110
- it("detects a failed command (non-zero exit)", async () => {
111
- const mgr = BackgroundManager.getInstance();
112
- const id = mgr.run({
113
- command: IS_WIN ? "cmd.exe" : "bash",
114
- args: IS_WIN ? ["/c", "exit", "1"] : ["-c", "exit 1"],
115
- });
116
-
117
- await waitForStatus(mgr, id, "failed");
118
- const task = mgr.check(id);
119
- assert.ok(task);
120
- assert.equal(task!.exitCode, 1);
121
- assert.equal(task!.status, "failed");
122
- });
123
-
124
- // ---- 5: timeout enforcement --------------------------------------------
125
-
126
- it("enforces timeout and marks task as timed_out", async () => {
127
- const mgr = BackgroundManager.getInstance();
128
-
129
- // Use a long-running command with a very short timeout (100 ms)
130
- const id = mgr.run({
131
- command: IS_WIN ? "powershell.exe" : "sleep",
132
- args: IS_WIN
133
- ? ["-NoProfile", "-Command", "Start-Sleep -Seconds 30"]
134
- : ["30"],
135
- timeout: 100,
136
- });
137
-
138
- await waitForStatus(mgr, id, "timed_out");
139
- const task = mgr.check(id);
140
- assert.ok(task);
141
- assert.equal(task!.status, "timed_out");
142
- });
143
-
144
- // ---- 6: kill() marks as cancelled --------------------------------------
145
-
146
- it("kill() marks a running task as cancelled", async () => {
147
- const mgr = BackgroundManager.getInstance();
148
-
149
- const id = mgr.run({
150
- command: IS_WIN ? "powershell.exe" : "sleep",
151
- args: IS_WIN
152
- ? ["-NoProfile", "-Command", "Start-Sleep -Seconds 30"]
153
- : ["30"],
154
- timeout: 0, // no timeout
155
- });
156
-
157
- // Wait for the task to enter "running"
158
- await waitForStatus(mgr, id, "running");
159
-
160
- // Kill it
161
- const killed = mgr.kill(id);
162
- assert.ok(killed, "kill() must return true");
163
-
164
- const task = mgr.check(id);
165
- assert.ok(task);
166
- assert.equal(task!.status, "cancelled");
167
- });
168
-
169
- // ---- 7: list() returns all tasks ---------------------------------------
170
-
171
- it("list() returns all tracked tasks", async () => {
172
- const mgr = BackgroundManager.getInstance();
173
- const id1 = mgr.run({ command: IS_WIN ? "echo" : "echo", args: ["a"] });
174
- const id2 = mgr.run({ command: IS_WIN ? "echo" : "echo", args: ["b"] });
175
-
176
- const tasks = mgr.list();
177
- const ids = tasks.map((t) => t.id);
178
- assert.ok(ids.includes(id1), "list must contain first task");
179
- assert.ok(ids.includes(id2), "list must contain second task");
180
- });
181
-
182
- // ---- 8: kill() on already-terminal task returns false ------------------
183
-
184
- it("kill() returns false for already-completed task", async () => {
185
- const mgr = BackgroundManager.getInstance();
186
- const id = mgr.run({ command: IS_WIN ? "echo" : "echo", args: ["quick"] });
187
-
188
- await waitForStatus(mgr, id, "completed");
189
- const result = mgr.kill(id);
190
- assert.equal(result, false, "kill() must return false on complete task");
191
- });
192
-
193
- // ---- 9: check() returns undefined for unknown ID -----------------------
194
-
195
- it("check() returns undefined for unknown task ID", () => {
196
- const mgr = BackgroundManager.getInstance();
197
- const result = mgr.check("nonexistent-id");
198
- assert.equal(result, undefined);
199
- });
200
-
201
- // ---- 10: error output captured on command-not-found --------------------
202
-
203
- it("captures error output when command does not exist", async () => {
204
- const mgr = BackgroundManager.getInstance();
205
- const id = mgr.run({ command: "this-command-does-not-exist-hopefully" });
206
-
207
- await waitForStatus(mgr, id, "failed");
208
- const task = mgr.check(id);
209
- assert.ok(task);
210
- // On Windows cmd.exe will emit an error; on Unix spawn error will fire
211
- assert.ok(
212
- task!.errorOutput.length > 0 || task!.output.length > 0,
213
- "should have some error output",
214
- );
215
- });
216
- });
@@ -1,7 +0,0 @@
1
- export type {
2
- BackgroundTask,
3
- BackgroundTaskStatus,
4
- BackgroundRunOptions,
5
- } from "./interfaces.ts";
6
-
7
- export { BackgroundManager } from "./manager.ts";
@@ -1,31 +0,0 @@
1
- export interface BackgroundTask {
2
- id: string;
3
- command: string;
4
- args: string[];
5
- cwd: string;
6
- status: BackgroundTaskStatus;
7
- output: string;
8
- errorOutput: string;
9
- exitCode: number | null;
10
- startTime: number;
11
- endTime: number | null;
12
- timeout: number; // ms, 0 = no timeout
13
- label?: string;
14
- }
15
-
16
- export type BackgroundTaskStatus =
17
- | "pending"
18
- | "running"
19
- | "completed"
20
- | "failed"
21
- | "timed_out"
22
- | "cancelled";
23
-
24
- export interface BackgroundRunOptions {
25
- command: string;
26
- args?: string[];
27
- cwd?: string;
28
- timeout?: number; // ms, default 30000
29
- label?: string;
30
- env?: Record<string, string>;
31
- }
@@ -1,320 +0,0 @@
1
- import { spawn, exec, type ChildProcess } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
3
- import type {
4
- BackgroundTask,
5
- BackgroundTaskStatus,
6
- BackgroundRunOptions,
7
- } from "./interfaces.ts";
8
-
9
- // ---------------------------------------------------------------------------
10
- // Constants
11
- // ---------------------------------------------------------------------------
12
-
13
- const CLEANUP_INTERVAL_MS = 60_000; // Check for stale tasks every 60s
14
- const TASK_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
15
-
16
- // ---------------------------------------------------------------------------
17
- // Internal entry
18
- // ---------------------------------------------------------------------------
19
-
20
- interface TaskEntry {
21
- task: BackgroundTask;
22
- process: ChildProcess | null;
23
- timeoutId?: ReturnType<typeof setTimeout>;
24
- }
25
-
26
- // ---------------------------------------------------------------------------
27
- // Manager
28
- // ---------------------------------------------------------------------------
29
-
30
- export class BackgroundManager {
31
- private static instance: BackgroundManager | null = null;
32
- private tasks = new Map<string, TaskEntry>();
33
- private cleanupTimer: ReturnType<typeof setInterval> | null = null;
34
-
35
- private constructor() {
36
- this.startCleanup();
37
- }
38
-
39
- // -----------------------------------------------------------------------
40
- // Singleton
41
- // -----------------------------------------------------------------------
42
-
43
- static getInstance(): BackgroundManager {
44
- if (!BackgroundManager.instance) {
45
- BackgroundManager.instance = new BackgroundManager();
46
- }
47
- return BackgroundManager.instance;
48
- }
49
-
50
- /** Reset singleton — used in tests to get a clean slate. */
51
- static resetInstance(): void {
52
- const inst = BackgroundManager.instance;
53
- if (inst) {
54
- inst.destroy();
55
- BackgroundManager.instance = null;
56
- }
57
- }
58
-
59
- // -----------------------------------------------------------------------
60
- // Public API
61
- // -----------------------------------------------------------------------
62
-
63
- /**
64
- * Spawn a background process and return its task ID immediately.
65
- * The task status starts as "pending" and transitions to "running" on
66
- * the next event-loop tick when the process actually spawns.
67
- */
68
- run(options: BackgroundRunOptions): string {
69
- const id = randomUUID();
70
- const cwd = options.cwd ?? process.cwd();
71
- const args = options.args ?? [];
72
- const timeout = options.timeout ?? 30_000;
73
-
74
- const task: BackgroundTask = {
75
- id,
76
- command: options.command,
77
- args,
78
- cwd,
79
- status: "pending",
80
- output: "",
81
- errorOutput: "",
82
- exitCode: null,
83
- startTime: Date.now(),
84
- endTime: null,
85
- timeout,
86
- label: options.label,
87
- };
88
-
89
- const entry: TaskEntry = { task, process: null };
90
- this.tasks.set(id, entry);
91
-
92
- // Defer to next tick so callers can observe "pending" immediately
93
- setImmediate(() => {
94
- this.spawnTask(id, options, entry);
95
- });
96
-
97
- return id;
98
- }
99
-
100
- /** Return current state of a tracked task, or undefined if not found. */
101
- check(id: string): BackgroundTask | undefined {
102
- this.sweepStale();
103
- return this.tasks.get(id)?.task;
104
- }
105
-
106
- /** Return all tracked tasks (including completed ones). */
107
- list(): BackgroundTask[] {
108
- this.sweepStale();
109
- return Array.from(this.tasks.values()).map((e) => e.task);
110
- }
111
-
112
- /**
113
- * Kill a running / pending task.
114
- * Returns true if the task existed and was killable, false otherwise.
115
- */
116
- kill(id: string): boolean {
117
- const entry = this.tasks.get(id);
118
- if (!entry) return false;
119
-
120
- const { task } = entry;
121
- if (isTerminal(task.status)) return false;
122
-
123
- this.killProcess(entry);
124
- task.status = "cancelled";
125
- task.endTime = Date.now();
126
- this.clearTimeout(entry);
127
- return true;
128
- }
129
-
130
- // -----------------------------------------------------------------------
131
- // Lifecycle (test cleanup / singleton teardown)
132
- // -----------------------------------------------------------------------
133
-
134
- /** Shut down the manager — kill all processes, clear state, stop timers. */
135
- destroy(): void {
136
- if (this.cleanupTimer) {
137
- clearInterval(this.cleanupTimer);
138
- this.cleanupTimer = null;
139
- }
140
- for (const [, entry] of this.tasks) {
141
- if (!isTerminal(entry.task.status)) {
142
- this.killProcess(entry);
143
- }
144
- this.clearTimeout(entry);
145
- }
146
- this.tasks.clear();
147
- }
148
-
149
- // -----------------------------------------------------------------------
150
- // Internals
151
- // -----------------------------------------------------------------------
152
-
153
- private spawnTask(
154
- id: string,
155
- options: BackgroundRunOptions,
156
- entry: TaskEntry,
157
- ): void {
158
- const { task } = entry;
159
- const args = options.args ?? [];
160
- const env = options.env;
161
- const timeout = options.timeout ?? 30_000;
162
-
163
- try {
164
- const isWindows = process.platform === "win32";
165
-
166
- // On Windows, wrap everything in cmd.exe /c so PATH and .exe
167
- // resolution work the way users expect.
168
- // Sanitize: strip shell metacharacters from the command to prevent
169
- // command injection via LLM-generated command strings.
170
- const command = isWindows ? "cmd.exe" : options.command;
171
- const sanitizedCommand = isWindows
172
- ? options.command.replace(/[&|;<>^%!]/g, "")
173
- : options.command;
174
- const sanitizedArgs = args.map((a) =>
175
- a.replace(/[&|;<>^%!]/g, ""),
176
- );
177
- const commandArgs = isWindows
178
- ? ["/d", "/c", sanitizedCommand, ...sanitizedArgs]
179
- : args;
180
-
181
- const child = spawn(command, commandArgs, {
182
- cwd: task.cwd,
183
- env: env ? { ...process.env, ...env } : undefined,
184
- stdio: ["ignore", "pipe", "pipe"],
185
- });
186
-
187
- entry.process = child;
188
- task.status = "running";
189
-
190
- child.stdout?.on("data", (data: Buffer) => {
191
- task.output += data.toString();
192
- });
193
-
194
- child.stderr?.on("data", (data: Buffer) => {
195
- task.errorOutput += data.toString();
196
- });
197
-
198
- child.on("error", (err: Error) => {
199
- task.status = "failed";
200
- task.errorOutput += `\n[spawn error] ${err.message}`;
201
- task.endTime = Date.now();
202
- this.clearTimeout(entry);
203
- });
204
-
205
- child.on("close", (code: number | null) => {
206
- task.exitCode = code;
207
- task.endTime = Date.now();
208
- if (task.status === "running" || task.status === "pending") {
209
- task.status = code === 0 ? "completed" : "failed";
210
- }
211
- this.clearTimeout(entry);
212
- });
213
-
214
- // Timeout enforcement
215
- if (timeout > 0) {
216
- entry.timeoutId = setTimeout(() => {
217
- if (!isTerminal(task.status)) {
218
- this.killProcess(entry);
219
- task.status = "timed_out";
220
- task.endTime = Date.now();
221
- }
222
- }, timeout);
223
- }
224
- } catch (err) {
225
- task.status = "failed";
226
- task.errorOutput = `[exception] ${String(err)}`;
227
- task.endTime = Date.now();
228
- }
229
- }
230
-
231
- private async killProcess(entry: TaskEntry): Promise<void> {
232
- if (!entry.process) return;
233
-
234
- if (process.platform === "win32") {
235
- // Forceful tree-kill via taskkill (more reliable than SIGTERM on Windows)
236
- try {
237
- await new Promise<void>((resolve, reject) => {
238
- exec(`taskkill /pid ${entry.process!.pid} /f /t`, (err) => {
239
- if (err) reject(err);
240
- else resolve();
241
- });
242
- });
243
- } catch {
244
- // taskkill failed — process may already be dead
245
- }
246
- // Also try SIGTERM as a graceful fallback
247
- try {
248
- entry.process.kill("SIGTERM");
249
- } catch {
250
- /* already dead */
251
- }
252
- } else {
253
- try {
254
- entry.process.kill("SIGTERM");
255
- } catch {
256
- /* already dead */
257
- }
258
- }
259
- }
260
-
261
- private clearTimeout(entry: TaskEntry): void {
262
- if (entry.timeoutId !== undefined) {
263
- clearTimeout(entry.timeoutId);
264
- entry.timeoutId = undefined;
265
- }
266
- }
267
-
268
- private startCleanup(): void {
269
- this.cleanupTimer = setInterval(() => this.sweepStale(), CLEANUP_INTERVAL_MS);
270
- this.cleanupTimer?.unref();
271
- }
272
-
273
- /**
274
- * Remove stale tasks:
275
- * - Completed/failed tasks older than TASK_MAX_AGE_MS
276
- * - Zombie processes (status "running" but process handle is dead)
277
- */
278
- private sweepStale(): void {
279
- const now = Date.now();
280
- for (const [id, entry] of this.tasks) {
281
- const { task } = entry;
282
-
283
- // Completed/failed tasks older than threshold
284
- if (task.endTime && now - task.endTime > TASK_MAX_AGE_MS) {
285
- this.clearTimeout(entry);
286
- this.tasks.delete(id);
287
- continue;
288
- }
289
-
290
- // Check for zombie processes: status "running" but process has exited
291
- // Use exitCode !== null as the reliable cross-platform check
292
- if (task.status === "running" && entry.process) {
293
- if (entry.process.exitCode !== null) {
294
- // Process exited but the close event wasn't processed (zombie)
295
- task.status = "failed";
296
- task.endTime = Date.now();
297
- this.tasks.delete(id);
298
- }
299
- } else if (task.status === "running" && !entry.process) {
300
- // Process reference is gone but status not updated
301
- task.status = "failed";
302
- task.endTime = Date.now();
303
- this.tasks.delete(id);
304
- }
305
- }
306
- }
307
- }
308
-
309
- // ---------------------------------------------------------------------------
310
- // Helpers
311
- // ---------------------------------------------------------------------------
312
-
313
- function isTerminal(status: BackgroundTaskStatus): boolean {
314
- return (
315
- status === "completed" ||
316
- status === "failed" ||
317
- status === "timed_out" ||
318
- status === "cancelled"
319
- );
320
- }
@@ -1,107 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // ErrorRecoveryHook — PostToolUse, priority=50, phase=LATE
3
- //
4
- // After sub-agent call, check if output indicates error.
5
- // Use the RecoveryHandler to match patterns and inject recovery actions.
6
- // ---------------------------------------------------------------------------
7
-
8
- import { HookPhase, HookResult } from "../types.ts";
9
- import type { HookContext, PostToolUseHook } from "../types.ts";
10
- import { RecoveryHandler } from "../../recovery/handler.ts";
11
- import type { ErrorContext } from "../../recovery/interfaces.ts";
12
-
13
- export const errorRecoveryHook: PostToolUseHook = {
14
- metadata: {
15
- name: "error-recovery",
16
- priority: 50,
17
- phase: HookPhase.LATE,
18
- dependencies: [],
19
- errorHandling: "isolate",
20
- },
21
-
22
- async execute(context: HookContext, output: string) {
23
- // Check if the output looks like an error
24
- const isErrorOutput = looksLikeError(output);
25
- if (!isErrorOutput) {
26
- return { result: HookResult.CONTINUE };
27
- }
28
-
29
- // Classify the error and get a recovery action
30
- const handler = RecoveryHandler.getInstance();
31
- const errorContext: ErrorContext = {
32
- sessionId: context.sessionId,
33
- error: new Error(output.slice(0, 500)), // Truncate for classification
34
- attempt: context._recoveryAttempt ?? 0,
35
- timestamp: Date.now(),
36
- agent: context.agent,
37
- };
38
-
39
- const action = handler.handleError(errorContext);
40
-
41
- // Build a recovery instruction based on the action
42
- const recoveryInstruction = buildRecoveryInstruction(action);
43
-
44
- return {
45
- result: HookResult.INJECT,
46
- modifiedOutput: output,
47
- injectRecovery: recoveryInstruction,
48
- };
49
- },
50
- };
51
-
52
- /**
53
- * Heuristic check: does the output look like an error?
54
- * Looks for common error patterns in tool output.
55
- */
56
- function looksLikeError(output: string): boolean {
57
- if (!output || output.length === 0) return false;
58
-
59
- const errorPatterns = [
60
- /error/i,
61
- /exception/i,
62
- /failed/i,
63
- /failure/i,
64
- /unable to/i,
65
- /could not/i,
66
- /not found/i,
67
- /ECONNREFUSED/i,
68
- /ETIMEDOUT/i,
69
- /rate.?limited/i,
70
- /too many requests/i,
71
- /context.?length/i,
72
- /token.?limit/i,
73
- /parse.?error/i,
74
- /syntax.?error/i,
75
- /timeout/i,
76
- /execution.?timed.?out/i,
77
- ];
78
-
79
- // Check first 2000 chars to avoid false positives in long output
80
- const head = output.slice(0, 2000);
81
- return errorPatterns.some((p) => p.test(head));
82
- }
83
-
84
- /**
85
- * Build a recovery instruction string from a RecoveryAction.
86
- */
87
- function buildRecoveryInstruction(
88
- action: { type: string; delay?: number; maxAttempts?: number; reason: string; modifyPrompt?: string },
89
- ): string {
90
- const parts: string[] = [
91
- `[HOOK: Error Recovery]`,
92
- `Action: ${action.type}`,
93
- `Reason: ${action.reason}`,
94
- ];
95
-
96
- if (action.delay) {
97
- parts.push(`Delay: ${action.delay}ms before retry`);
98
- }
99
- if (action.maxAttempts) {
100
- parts.push(`Max attempts: ${action.maxAttempts}`);
101
- }
102
- if (action.modifyPrompt) {
103
- parts.push(`Modification: ${action.modifyPrompt}`);
104
- }
105
-
106
- return parts.join("\n");
107
- }