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
@@ -4,45 +4,41 @@
4
4
 
5
5
  import { describe, it, before, after, beforeEach } from "node:test";
6
6
  import assert from "node:assert/strict";
7
- import {
8
- HookPhase,
9
- HookResult,
10
- HookRegistry,
11
- planCheckHook,
7
+ import {
8
+ HookPhase,
9
+ HookResult,
10
+ HookRegistry,
11
+ planCheckHook,
12
12
  shellDetectHook,
13
13
  confidenceGateHook,
14
14
  delegationDepthHook,
15
15
  resetDepthTracker,
16
- errorRecoveryHook,
17
- memorySyncHook,
18
- routeTrackingHook,
19
- resetRouteTracker,
20
- getHopHistory,
21
- sanityCheckHook,
22
- dynamicRouteHook,
23
- } from "./index.ts";
24
- import { AnomalyTracker } from "../sanity/anomaly-tracker.ts";
25
- import type {
26
- HookContext,
27
- HookContextPatch,
28
- HookMetadata,
29
- PreToolUseHook,
30
- PostToolUseHook,
31
- RouteHook,
16
+ routeTrackingHook,
17
+ resetRouteTracker,
18
+ getHopHistory,
19
+ dynamicRouteHook,
20
+ } from "./index.ts";
21
+ import type {
22
+ HookContext,
23
+ HookContextPatch,
24
+ HookMetadata,
25
+ PreToolUseHook,
26
+ PostToolUseHook,
27
+ RouteHook,
32
28
  SessionHook,
33
- } from "./types.ts";
34
- import fs from "node:fs";
35
- import os from "node:os";
36
- import path from "node:path";
29
+ } from "./types.ts";
30
+ import fs from "node:fs";
31
+ import os from "node:os";
32
+ import path from "node:path";
37
33
 
38
34
  // ---------------------------------------------------------------------------
39
35
  // Helpers
40
36
  // ---------------------------------------------------------------------------
41
37
 
42
- function makeContext(overrides?: HookContextPatch): HookContext {
43
- return {
44
- sessionId: "test-session",
45
- agent: "oh-builder",
38
+ function makeContext(overrides?: HookContextPatch): HookContext {
39
+ return {
40
+ sessionId: "test-session",
41
+ agent: "oh-builder",
46
42
  directory: "/tmp/test-project",
47
43
  sessions: new Map(),
48
44
  ...overrides,
@@ -52,13 +48,13 @@ function makeContext(overrides?: HookContextPatch): HookContext {
52
48
  function makePreToolHook(
53
49
  name: string,
54
50
  overrides?: Partial<HookMetadata>,
55
- impl?: (
56
- ctx: HookContext,
57
- ) => Promise<{
58
- result: HookResult;
59
- modifiedContext?: HookContextPatch;
60
- }>,
61
- ): PreToolUseHook {
51
+ impl?: (
52
+ ctx: HookContext,
53
+ ) => Promise<{
54
+ result: HookResult;
55
+ modifiedContext?: HookContextPatch;
56
+ }>,
57
+ ): PreToolUseHook {
62
58
  return {
63
59
  metadata: {
64
60
  name,
@@ -81,7 +77,6 @@ function makePostToolHook(
81
77
  ) => Promise<{
82
78
  result: HookResult;
83
79
  modifiedOutput?: string;
84
- injectRecovery?: string;
85
80
  }>,
86
81
  ): PostToolUseHook {
87
82
  return {
@@ -140,18 +135,18 @@ function makeSessionHook(
140
135
  // Tests
141
136
  // ---------------------------------------------------------------------------
142
137
 
143
- describe("HookRegistry", () => {
144
- const tmpDirs: string[] = [];
145
-
146
- after(() => {
147
- for (const dir of tmpDirs) {
148
- fs.rmSync(dir, { recursive: true, force: true });
149
- }
150
- });
151
-
152
- beforeEach(() => {
153
- HookRegistry.resetInstance();
154
- resetDepthTracker();
138
+ describe("HookRegistry", () => {
139
+ const tmpDirs: string[] = [];
140
+
141
+ after(() => {
142
+ for (const dir of tmpDirs) {
143
+ fs.rmSync(dir, { recursive: true, force: true });
144
+ }
145
+ });
146
+
147
+ beforeEach(() => {
148
+ HookRegistry.resetInstance();
149
+ resetDepthTracker();
155
150
  resetRouteTracker();
156
151
  });
157
152
 
@@ -264,7 +259,7 @@ describe("HookRegistry", () => {
264
259
  assert.equal(sorted[2].metadata.name, "late-hook");
265
260
  });
266
261
 
267
- it("handles simple linear dependencies", () => {
262
+ it("sorts by priority within same phase, ignoring dependencies", () => {
268
263
  const reg = HookRegistry.getInstance();
269
264
  const a = makePreToolHook("a", {
270
265
  phase: HookPhase.EARLY,
@@ -273,89 +268,27 @@ describe("HookRegistry", () => {
273
268
  });
274
269
  const b = makePreToolHook("b", {
275
270
  phase: HookPhase.EARLY,
276
- priority: 50,
271
+ priority: 70,
277
272
  dependencies: ["a"],
278
273
  });
279
- const c = makePreToolHook("c", {
280
- phase: HookPhase.EARLY,
281
- priority: 50,
282
- dependencies: ["b"],
283
- });
284
274
 
285
- const sorted = reg.topologicalSort([c, a, b]);
286
- assert.equal(sorted[0].metadata.name, "a");
287
- assert.equal(sorted[1].metadata.name, "b");
288
- assert.equal(sorted[2].metadata.name, "c");
289
- });
290
-
291
- it("handles diamond dependencies (A→B→D and A→C→D)", () => {
292
- const reg = HookRegistry.getInstance();
293
- const a = makePreToolHook("a", {
294
- phase: HookPhase.EARLY,
295
- priority: 50,
296
- dependencies: [],
297
- });
298
- const b = makePreToolHook("b", {
299
- phase: HookPhase.EARLY,
300
- priority: 50,
301
- dependencies: ["a"],
302
- });
303
- const c = makePreToolHook("c", {
304
- phase: HookPhase.EARLY,
305
- priority: 50,
306
- dependencies: ["a"],
307
- });
308
- const d = makePreToolHook("d", {
309
- phase: HookPhase.EARLY,
310
- priority: 50,
311
- dependencies: ["b", "c"],
312
- });
313
-
314
- const sorted = reg.topologicalSort([d, c, b, a]);
315
- // A must come first, D must come last
316
- assert.equal(sorted[0].metadata.name, "a");
317
- assert.equal(sorted[3].metadata.name, "d");
318
- // B and C can be in any order but must be before D and after A
319
- const bIdx = sorted.findIndex((h) => h.metadata.name === "b");
320
- const cIdx = sorted.findIndex((h) => h.metadata.name === "c");
321
- assert.ok(bIdx > 0);
322
- assert.ok(cIdx > 0);
323
- assert.ok(bIdx < 3);
324
- assert.ok(cIdx < 3);
325
- });
326
-
327
- it("throws on circular dependency (A→B→C→A)", () => {
328
- const reg = HookRegistry.getInstance();
329
- const a = makePreToolHook("a", {
330
- phase: HookPhase.EARLY,
331
- dependencies: ["c"],
332
- });
333
- const b = makePreToolHook("b", {
334
- phase: HookPhase.EARLY,
335
- dependencies: ["a"],
336
- });
337
- const c = makePreToolHook("c", {
338
- phase: HookPhase.EARLY,
339
- dependencies: ["b"],
340
- });
341
-
342
- assert.throws(
343
- () => reg.topologicalSort([a, b, c]),
344
- /Circular dependency detected/,
345
- );
275
+ // Higher priority first (70 > 50), regardless of dependency declaration
276
+ const sorted = reg.topologicalSort([a, b]);
277
+ assert.equal(sorted[0].metadata.name, "b");
278
+ assert.equal(sorted[1].metadata.name, "a");
346
279
  });
347
280
 
348
- it("self-dependency throws", () => {
281
+ it("preserves original order for equal phase and priority", () => {
349
282
  const reg = HookRegistry.getInstance();
350
- const a = makePreToolHook("a", {
351
- phase: HookPhase.EARLY,
352
- dependencies: ["a"],
353
- });
283
+ const c = makePreToolHook("c", { phase: HookPhase.EARLY, priority: 50 });
284
+ const a = makePreToolHook("a", { phase: HookPhase.EARLY, priority: 50 });
285
+ const b = makePreToolHook("b", { phase: HookPhase.EARLY, priority: 50 });
354
286
 
355
- assert.throws(
356
- () => reg.topologicalSort([a]),
357
- /Circular dependency detected/,
358
- );
287
+ // Stable sort: same phase + priority means original order is preserved
288
+ const sorted = reg.topologicalSort([c, a, b]);
289
+ assert.equal(sorted[0].metadata.name, "c");
290
+ assert.equal(sorted[1].metadata.name, "a");
291
+ assert.equal(sorted[2].metadata.name, "b");
359
292
  });
360
293
 
361
294
  it("cross-phase dependencies are ignored (not within same phase)", () => {
@@ -392,13 +325,13 @@ describe("HookRegistry", () => {
392
325
  }),
393
326
  );
394
327
 
395
- const result = await reg.executePreTool(makeContext());
396
- assert.equal(result.result, HookResult.CONTINUE);
397
- assert.equal(result.modifiedContext?.sessionId, "test-session");
398
- assert.equal(result.modifiedContext?.agent, "oh-builder");
399
- assert.equal(result.modifiedContext?.directory, "/tmp/test-project");
400
- assert.equal(result.modifiedContext?._track, "ran");
401
- });
328
+ const result = await reg.executePreTool(makeContext());
329
+ assert.equal(result.result, HookResult.CONTINUE);
330
+ assert.equal(result.modifiedContext?.sessionId, "test-session");
331
+ assert.equal(result.modifiedContext?.agent, "oh-builder");
332
+ assert.equal(result.modifiedContext?.directory, "/tmp/test-project");
333
+ assert.equal(result.modifiedContext?._track, "ran");
334
+ });
402
335
 
403
336
  it("stops execution on STOP result", async () => {
404
337
  const reg = HookRegistry.getInstance();
@@ -443,7 +376,7 @@ describe("HookRegistry", () => {
443
376
  });
444
377
  });
445
378
 
446
- describe("executePostTool", () => {
379
+ describe("executePostTool", () => {
447
380
  it("passes through output without modification", async () => {
448
381
  const reg = HookRegistry.getInstance();
449
382
  reg.registerPostTool(makePostToolHook("pass"));
@@ -478,98 +411,98 @@ describe("HookRegistry", () => {
478
411
  assert.equal(result.modifiedOutput, "[[[HELLO]]]");
479
412
  });
480
413
 
481
- it("injects recovery action", async () => {
482
- const reg = HookRegistry.getInstance();
483
- reg.registerPostTool(
414
+ it("injects recovery action (stub)", async () => {
415
+ // Recovery field removed in cleanup — test kept as placeholder
416
+ const reg = HookRegistry.getInstance();
417
+ reg.registerPostTool(
484
418
  makePostToolHook("recovery-test", {}, async () => ({
485
419
  result: HookResult.INJECT,
486
- injectRecovery: "retry with backoff",
487
420
  })),
488
421
  );
489
422
 
490
423
  const result = await reg.executePostTool(
491
424
  makeContext(),
492
425
  "output",
493
- );
494
- assert.equal(result.recovery, "retry with backoff");
495
- });
496
-
497
- it("appends structured route guidance from output evidence", async () => {
498
- const reg = HookRegistry.getInstance();
499
- reg.registerPostTool(dynamicRouteHook);
500
-
501
- const skillsDir = fs.mkdtempSync(path.join(os.tmpdir(), "oh-routing-hook-"));
502
- tmpDirs.push(skillsDir);
503
- const skillDir = path.join(skillsDir, "oh-review");
504
- fs.mkdirSync(skillDir, { recursive: true });
505
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
506
- name: oh-review
507
- route:
508
- pass:
509
- - oh-gauntlet
510
- - oh-ship
511
- fail: oh-builder
512
- blocker: surface
513
- ---\n`);
514
-
515
- const result = await reg.executePostTool(
516
- makeContext({ agent: "oh-review", _routingSkillsDir: skillsDir }),
517
- 'Review complete\nROUTE_EVIDENCE: {"outcome":"pass","target":"oh-ship"}',
518
- );
519
-
520
- assert.equal(result.result, HookResult.INJECT);
521
- assert.ok(result.modifiedOutput?.includes("Review complete"));
522
- assert.ok(result.modifiedOutput?.includes("ROUTE_GUIDANCE:"));
523
-
524
- const guidanceLine = result.modifiedOutput
525
- ?.split(/\r?\n/)
526
- .find((line) => line.startsWith("ROUTE_GUIDANCE:"));
527
- assert.ok(guidanceLine);
528
- assert.deepEqual(JSON.parse(guidanceLine!.slice("ROUTE_GUIDANCE:".length).trim()), {
529
- outcome: "pass",
530
- candidates: ["oh-gauntlet", "oh-ship"],
531
- selected: "oh-ship",
532
- reason: 'Selected "oh-ship" from output evidence.',
533
- });
534
- });
535
-
536
- it("ignores malformed structured route evidence safely", async () => {
537
- const reg = HookRegistry.getInstance();
538
- reg.registerPostTool(dynamicRouteHook);
539
-
540
- const skillsDir = fs.mkdtempSync(path.join(os.tmpdir(), "oh-routing-hook-"));
541
- tmpDirs.push(skillsDir);
542
- const skillDir = path.join(skillsDir, "oh-review");
543
- fs.mkdirSync(skillDir, { recursive: true });
544
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
545
- name: oh-review
546
- route:
547
- pass:
548
- - oh-gauntlet
549
- - oh-ship
550
- fail: oh-builder
551
- blocker: surface
552
- ---\n`);
553
-
554
- const output = 'Review complete\nROUTE_EVIDENCE: {"outcome":"pass","verification":"maybe"}';
555
- const result = await reg.executePostTool(
556
- makeContext({ agent: "oh-review", _routingSkillsDir: skillsDir }),
557
- output,
558
- );
559
-
560
- assert.equal(result.result, HookResult.CONTINUE);
561
- assert.equal(result.modifiedOutput, output);
562
- });
563
-
564
- it("leaves output unchanged when no route evidence is present", async () => {
565
- const reg = HookRegistry.getInstance();
566
- reg.registerPostTool(dynamicRouteHook);
567
-
568
- const result = await reg.executePostTool(makeContext({ agent: "oh-review" }), "plain output");
569
- assert.equal(result.result, HookResult.CONTINUE);
570
- assert.equal(result.modifiedOutput, "plain output");
571
- });
572
- });
426
+ );
427
+ assert.equal(result.result, HookResult.INJECT);
428
+ });
429
+
430
+ it("appends structured route guidance from output evidence", async () => {
431
+ const reg = HookRegistry.getInstance();
432
+ reg.registerPostTool(dynamicRouteHook);
433
+
434
+ const skillsDir = fs.mkdtempSync(path.join(os.tmpdir(), "oh-routing-hook-"));
435
+ tmpDirs.push(skillsDir);
436
+ const skillDir = path.join(skillsDir, "oh-review");
437
+ fs.mkdirSync(skillDir, { recursive: true });
438
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
439
+ name: oh-review
440
+ route:
441
+ pass:
442
+ - oh-gauntlet
443
+ - oh-ship
444
+ fail: oh-builder
445
+ blocker: surface
446
+ ---\n`);
447
+
448
+ const result = await reg.executePostTool(
449
+ makeContext({ agent: "oh-review", _routingSkillsDir: skillsDir }),
450
+ 'Review complete\nROUTE_EVIDENCE: {"outcome":"pass","target":"oh-ship"}',
451
+ );
452
+
453
+ assert.equal(result.result, HookResult.INJECT);
454
+ assert.ok(result.modifiedOutput?.includes("Review complete"));
455
+ assert.ok(result.modifiedOutput?.includes("ROUTE_GUIDANCE:"));
456
+
457
+ const guidanceLine = result.modifiedOutput
458
+ ?.split(/\r?\n/)
459
+ .find((line) => line.startsWith("ROUTE_GUIDANCE:"));
460
+ assert.ok(guidanceLine);
461
+ assert.deepEqual(JSON.parse(guidanceLine!.slice("ROUTE_GUIDANCE:".length).trim()), {
462
+ outcome: "pass",
463
+ candidates: ["oh-gauntlet", "oh-ship"],
464
+ selected: "oh-ship",
465
+ reason: 'Selected "oh-ship" from output evidence.',
466
+ });
467
+ });
468
+
469
+ it("ignores malformed structured route evidence safely", async () => {
470
+ const reg = HookRegistry.getInstance();
471
+ reg.registerPostTool(dynamicRouteHook);
472
+
473
+ const skillsDir = fs.mkdtempSync(path.join(os.tmpdir(), "oh-routing-hook-"));
474
+ tmpDirs.push(skillsDir);
475
+ const skillDir = path.join(skillsDir, "oh-review");
476
+ fs.mkdirSync(skillDir, { recursive: true });
477
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
478
+ name: oh-review
479
+ route:
480
+ pass:
481
+ - oh-gauntlet
482
+ - oh-ship
483
+ fail: oh-builder
484
+ blocker: surface
485
+ ---\n`);
486
+
487
+ const output = 'Review complete\nROUTE_EVIDENCE: {"outcome":"pass","verification":"maybe"}';
488
+ const result = await reg.executePostTool(
489
+ makeContext({ agent: "oh-review", _routingSkillsDir: skillsDir }),
490
+ output,
491
+ );
492
+
493
+ assert.equal(result.result, HookResult.CONTINUE);
494
+ assert.equal(result.modifiedOutput, output);
495
+ });
496
+
497
+ it("leaves output unchanged when no route evidence is present", async () => {
498
+ const reg = HookRegistry.getInstance();
499
+ reg.registerPostTool(dynamicRouteHook);
500
+
501
+ const result = await reg.executePostTool(makeContext({ agent: "oh-review" }), "plain output");
502
+ assert.equal(result.result, HookResult.CONTINUE);
503
+ assert.equal(result.modifiedOutput, "plain output");
504
+ });
505
+ });
573
506
 
574
507
  describe("executeRoute", () => {
575
508
  it("passes route unchanged", async () => {
@@ -678,18 +611,6 @@ route:
678
611
  assert.equal(delegationDepthHook.metadata.phase, HookPhase.NORMAL);
679
612
  });
680
613
 
681
- it("errorRecoveryHook has correct metadata", () => {
682
- assert.equal(errorRecoveryHook.metadata.name, "error-recovery");
683
- assert.equal(errorRecoveryHook.metadata.priority, 50);
684
- assert.equal(errorRecoveryHook.metadata.phase, HookPhase.LATE);
685
- });
686
-
687
- it("memorySyncHook has correct metadata", () => {
688
- assert.equal(memorySyncHook.metadata.name, "memory-sync");
689
- assert.equal(memorySyncHook.metadata.priority, 40);
690
- assert.equal(memorySyncHook.metadata.phase, HookPhase.LATE);
691
- });
692
-
693
614
  it("shellDetectHook returns shell context", async () => {
694
615
  const result = await shellDetectHook.execute(makeContext());
695
616
  assert.equal(result.result, HookResult.CONTINUE);
@@ -716,24 +637,6 @@ route:
716
637
  assert.equal(result.modifiedContext?._depthExceeded, true);
717
638
  });
718
639
 
719
- it("errorRecoveryHook returns CONTINUE for normal output", async () => {
720
- const result = await errorRecoveryHook.execute(
721
- makeContext(),
722
- "Everything completed successfully.",
723
- );
724
- assert.equal(result.result, HookResult.CONTINUE);
725
- });
726
-
727
- it("errorRecoveryHook detects error output", async () => {
728
- const result = await errorRecoveryHook.execute(
729
- makeContext(),
730
- "Error: Failed to connect to server",
731
- );
732
- assert.equal(result.result, HookResult.INJECT);
733
- assert.ok(result.injectRecovery);
734
- assert.ok(result.injectRecovery!.includes("Error Recovery"));
735
- });
736
-
737
640
  it("confidenceGateHook passes through without confidence info", async () => {
738
641
  const result = await confidenceGateHook.execute(
739
642
  makeContext(),
@@ -758,12 +661,9 @@ route:
758
661
  reg.registerPreTool(shellDetectHook);
759
662
  reg.registerPreTool(delegationDepthHook);
760
663
  reg.registerRoute(confidenceGateHook);
761
- reg.registerPostTool(errorRecoveryHook);
762
- reg.registerPostTool(memorySyncHook);
763
664
 
764
665
  assert.equal(reg.getPreToolHooks().length, 3);
765
666
  assert.equal(reg.getRouteHooks().length, 1);
766
- assert.equal(reg.getPostToolHooks().length, 2);
767
667
  });
768
668
 
769
669
  it("routeTrackingHook has correct metadata", () => {
@@ -784,9 +684,6 @@ route:
784
684
  // Route hook
785
685
  await confidenceGateHook.execute(ctx, "oh-builder");
786
686
 
787
- // Post-tool hooks
788
- await errorRecoveryHook.execute(ctx, "normal output");
789
- await memorySyncHook.execute(ctx, "some output");
790
687
  // If we got here without throwing, success
791
688
  assert.ok(true);
792
689
  });
@@ -815,10 +712,10 @@ route:
815
712
  // 5th — should STOP (>= maxSkillRepeats=5)
816
713
  const result = await routeTrackingHook.execute(ctx, "oh-builder");
817
714
  assert.equal(result.result, HookResult.STOP);
818
- assert.ok(ctx._optiRoute);
819
- assert.ok(ctx._optiRoute.reason);
820
- assert.ok(ctx._optiRoute.reason.includes("oh-builder"));
821
- assert.ok(ctx._optiRoute.chain.length === 5);
715
+ assert.ok(ctx._optiRoute);
716
+ assert.ok(ctx._optiRoute.reason);
717
+ assert.ok(ctx._optiRoute.reason.includes("oh-builder"));
718
+ assert.ok(ctx._optiRoute.chain.length === 5);
822
719
  });
823
720
 
824
721
  it("stops on 8th unproductive hop (default max 8)", async () => {
@@ -840,8 +737,8 @@ route:
840
737
  // 8th — should STOP
841
738
  const result = await routeTrackingHook.execute(ctx, "oh-builder");
842
739
  assert.equal(result.result, HookResult.STOP);
843
- assert.ok(ctx._optiRoute);
844
- assert.ok(ctx._optiRoute.reason.includes("unproductive"));
740
+ assert.ok(ctx._optiRoute);
741
+ assert.ok(ctx._optiRoute.reason.includes("unproductive"));
845
742
  });
846
743
 
847
744
  it("productive hop resets unproductive counter", async () => {
@@ -954,9 +851,9 @@ route:
954
851
  // 3rd unproductive should STOP (>=3)
955
852
  const result = await routeTrackingHook.execute(ctx, "oh-gauntlet");
956
853
  assert.equal(result.result, HookResult.STOP);
957
- assert.ok(ctx._optiRoute);
958
- assert.ok(ctx._optiRoute.reason.includes("unproductive"));
959
- });
854
+ assert.ok(ctx._optiRoute);
855
+ assert.ok(ctx._optiRoute.reason.includes("unproductive"));
856
+ });
960
857
  });
961
858
  });
962
859
 
@@ -1028,65 +925,4 @@ route:
1028
925
  });
1029
926
  });
1030
927
 
1031
- // ---------------------------------------------------------------------------
1032
- // sanityCheckHook
1033
- // ---------------------------------------------------------------------------
1034
-
1035
- describe("sanityCheckHook", () => {
1036
- beforeEach(() => {
1037
- AnomalyTracker.getInstance().resetAll();
1038
- });
1039
-
1040
- it("passes clean output through unchanged", async () => {
1041
- const ctx = makeContext();
1042
- const result = await sanityCheckHook.execute(
1043
- ctx,
1044
- "Everything is working fine. The system completed the task successfully.",
1045
- );
1046
- assert.equal(result.result, HookResult.CONTINUE);
1047
- assert.equal(result.modifiedOutput, undefined);
1048
- });
1049
-
1050
- it("detects repetitive output", async () => {
1051
- const ctx = makeContext();
1052
- const line =
1053
- "Sphinx of black quartz, judge my vow! The five boxing wizards jump quickly. 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1054
- const repetitiveOutput = Array.from({ length: 20 }, () => line).join("\n");
1055
- const result = await sanityCheckHook.execute(ctx, repetitiveOutput);
1056
- // First anomaly — not yet escalated
1057
- assert.equal(result.result, HookResult.CONTINUE);
1058
- });
1059
-
1060
- it("detects box-drawing character flooding", async () => {
1061
- const ctx = makeContext();
1062
- const boxArt = "─│┌┐└┘├┤┬┴┼".repeat(50);
1063
- const result = await sanityCheckHook.execute(ctx, boxArt);
1064
- assert.equal(result.result, HookResult.CONTINUE);
1065
- });
1066
-
1067
- it("detects placeholder patterns", async () => {
1068
- const ctx = makeContext();
1069
- // 50× [PLACEHOLDER] = 650 chars, 11 unique → triggers low_diversity check
1070
- const placeholderText = "[PLACEHOLDER]".repeat(50);
1071
- const result = await sanityCheckHook.execute(ctx, placeholderText);
1072
- assert.equal(result.result, HookResult.CONTINUE);
1073
- });
1074
-
1075
- it("tracks anomalies across calls", async () => {
1076
- const ctx = makeContext({ sessionId: "anomaly-escalation-test" });
1077
- const line =
1078
- "Sphinx of black quartz, judge my vow! The five boxing wizards jump quickly. 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1079
- const repetitiveOutput = Array.from({ length: 20 }, () => line).join("\n");
1080
-
1081
- // First call: anomaly detected but below threshold → CONTINUE
1082
- const firstResult = await sanityCheckHook.execute(ctx, repetitiveOutput);
1083
- assert.equal(firstResult.result, HookResult.CONTINUE);
1084
-
1085
- // Second call: threshold reached → INJECT with recovery
1086
- const secondResult = await sanityCheckHook.execute(ctx, repetitiveOutput);
1087
- assert.equal(secondResult.result, HookResult.INJECT);
1088
- assert.ok(secondResult.injectRecovery);
1089
- assert.equal(secondResult.modifiedOutput, repetitiveOutput);
1090
- });
1091
- });
1092
928
  });