openhermes 4.11.2 → 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.
- package/CONTEXT.md +1 -1
- package/ETHOS.md +1 -1
- package/README.md +12 -18
- package/bootstrap.ts +73 -148
- package/docs/HOW-IT-WORKS.md +162 -0
- package/docs/adr/ADR-0001-rebuild-vs-increment.md +30 -0
- package/docs/adr/ADR-0002-routing-graph-vs-linear-chain.md +36 -0
- package/docs/adr/ADR-0003-per-directory-plan-storage.md +34 -0
- package/docs/adr/ADR-0004-composer-fragment-architecture.md +42 -0
- package/docs/adr/ADR-0005-hook-system-design.md +42 -0
- package/docs/adr/README.md +9 -0
- package/harness/codex/AUTOPILOT.md +30 -23
- package/harness/codex/CHARTER.md +3 -3
- package/harness/lib/composer/compose.test.ts +11 -0
- package/harness/lib/composer/fragments/02-delegation.md +2 -1
- package/harness/lib/composer/fragments/04-task-flow.md +42 -2
- package/harness/lib/composer/fragments/08-routing.md +1 -1
- package/harness/lib/composer/fragments/09-guardrails.md +17 -4
- package/harness/lib/composer/index.ts +1 -1
- package/harness/lib/guards/guard-config.ts +72 -0
- package/harness/lib/hooks/builtins/confidence-gate-hook.ts +2 -4
- package/harness/lib/hooks/builtins/delegation-depth-hook.ts +23 -4
- package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -0
- package/harness/lib/hooks/builtins/next-route-hook.ts +24 -0
- package/harness/lib/hooks/builtins/plan-check-hook.ts +2 -2
- package/harness/lib/hooks/builtins/route-tracking-hook.ts +79 -25
- package/harness/lib/hooks/hooks.test.ts +117 -205
- package/harness/lib/hooks/index.ts +38 -30
- package/harness/lib/hooks/registry.ts +309 -416
- package/harness/lib/hooks/types.ts +116 -71
- package/harness/lib/plans/plan-location.ts +134 -0
- package/harness/lib/routing/index.ts +21 -0
- package/harness/lib/routing/route-guidance.ts +147 -0
- package/harness/lib/routing/route-resolver.ts +58 -0
- package/harness/lib/routing/routing.test.ts +195 -0
- package/harness/lib/routing/skill-frontmatter.ts +125 -0
- package/harness/lib/routing/types.ts +52 -0
- package/harness/skills/oh-ascii/SKILL.md +1 -1
- package/harness/skills/oh-fusion/DEEP.md +56 -33
- package/harness/skills/oh-fusion/SKILL.md +30 -16
- package/harness/skills/oh-init/DEEP.md +2 -2
- package/harness/skills/oh-manifest/SKILL.md +1 -0
- package/harness/skills/oh-plan-review/DEEP.md +1 -1
- package/harness/skills/oh-planner/DEEP.md +3 -3
- package/harness/skills/oh-review/DEEP.md +2 -0
- package/harness/skills/oh-review/SKILL.md +1 -0
- package/package.json +56 -55
- package/harness/lib/background/background.test.ts +0 -197
- package/harness/lib/background/index.ts +0 -7
- package/harness/lib/background/interfaces.ts +0 -31
- package/harness/lib/background/manager.ts +0 -320
- package/harness/lib/hooks/builtins/error-recovery-hook.ts +0 -107
- package/harness/lib/hooks/builtins/memory-sync-hook.ts +0 -73
- package/harness/lib/hooks/builtins/sanity-check-hook.ts +0 -52
- package/harness/lib/memory/index.ts +0 -18
- package/harness/lib/memory/interfaces.ts +0 -53
- package/harness/lib/memory/memory-manager.ts +0 -205
- package/harness/lib/memory/memory.test.ts +0 -491
- package/harness/lib/memory/plan-store.ts +0 -366
- package/harness/lib/recovery/handler.ts +0 -243
- package/harness/lib/recovery/index.ts +0 -14
- package/harness/lib/recovery/interfaces.ts +0 -48
- package/harness/lib/recovery/patterns.ts +0 -149
- package/harness/lib/recovery/recovery.test.ts +0 -312
- package/harness/lib/sanity/anomaly-tracker.ts +0 -127
- package/harness/lib/sanity/checker.ts +0 -178
- package/harness/lib/sanity/index.ts +0 -13
- package/harness/lib/sanity/interfaces.ts +0 -24
- package/harness/lib/sanity/sanity.test.ts +0 -472
- package/harness/lib/sync/file-watcher.ts +0 -174
- package/harness/lib/sync/index.ts +0 -11
- package/harness/lib/sync/interfaces.ts +0 -27
- package/harness/lib/sync/plan-sync.ts +0 -536
- package/harness/lib/sync/sync.test.ts +0 -832
|
@@ -13,28 +13,29 @@ import {
|
|
|
13
13
|
confidenceGateHook,
|
|
14
14
|
delegationDepthHook,
|
|
15
15
|
resetDepthTracker,
|
|
16
|
-
errorRecoveryHook,
|
|
17
|
-
memorySyncHook,
|
|
18
16
|
routeTrackingHook,
|
|
19
17
|
resetRouteTracker,
|
|
20
18
|
getHopHistory,
|
|
21
|
-
|
|
19
|
+
dynamicRouteHook,
|
|
22
20
|
} from "./index.ts";
|
|
23
|
-
import { AnomalyTracker } from "../sanity/anomaly-tracker.ts";
|
|
24
21
|
import type {
|
|
25
22
|
HookContext,
|
|
23
|
+
HookContextPatch,
|
|
26
24
|
HookMetadata,
|
|
27
25
|
PreToolUseHook,
|
|
28
26
|
PostToolUseHook,
|
|
29
27
|
RouteHook,
|
|
30
28
|
SessionHook,
|
|
31
29
|
} from "./types.ts";
|
|
30
|
+
import fs from "node:fs";
|
|
31
|
+
import os from "node:os";
|
|
32
|
+
import path from "node:path";
|
|
32
33
|
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
34
35
|
// Helpers
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
36
37
|
|
|
37
|
-
function makeContext(overrides?:
|
|
38
|
+
function makeContext(overrides?: HookContextPatch): HookContext {
|
|
38
39
|
return {
|
|
39
40
|
sessionId: "test-session",
|
|
40
41
|
agent: "oh-builder",
|
|
@@ -51,7 +52,7 @@ function makePreToolHook(
|
|
|
51
52
|
ctx: HookContext,
|
|
52
53
|
) => Promise<{
|
|
53
54
|
result: HookResult;
|
|
54
|
-
modifiedContext?:
|
|
55
|
+
modifiedContext?: HookContextPatch;
|
|
55
56
|
}>,
|
|
56
57
|
): PreToolUseHook {
|
|
57
58
|
return {
|
|
@@ -76,7 +77,6 @@ function makePostToolHook(
|
|
|
76
77
|
) => Promise<{
|
|
77
78
|
result: HookResult;
|
|
78
79
|
modifiedOutput?: string;
|
|
79
|
-
injectRecovery?: string;
|
|
80
80
|
}>,
|
|
81
81
|
): PostToolUseHook {
|
|
82
82
|
return {
|
|
@@ -136,6 +136,14 @@ function makeSessionHook(
|
|
|
136
136
|
// ---------------------------------------------------------------------------
|
|
137
137
|
|
|
138
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
|
+
|
|
139
147
|
beforeEach(() => {
|
|
140
148
|
HookRegistry.resetInstance();
|
|
141
149
|
resetDepthTracker();
|
|
@@ -251,7 +259,7 @@ describe("HookRegistry", () => {
|
|
|
251
259
|
assert.equal(sorted[2].metadata.name, "late-hook");
|
|
252
260
|
});
|
|
253
261
|
|
|
254
|
-
it("
|
|
262
|
+
it("sorts by priority within same phase, ignoring dependencies", () => {
|
|
255
263
|
const reg = HookRegistry.getInstance();
|
|
256
264
|
const a = makePreToolHook("a", {
|
|
257
265
|
phase: HookPhase.EARLY,
|
|
@@ -260,89 +268,27 @@ describe("HookRegistry", () => {
|
|
|
260
268
|
});
|
|
261
269
|
const b = makePreToolHook("b", {
|
|
262
270
|
phase: HookPhase.EARLY,
|
|
263
|
-
priority:
|
|
271
|
+
priority: 70,
|
|
264
272
|
dependencies: ["a"],
|
|
265
273
|
});
|
|
266
|
-
const c = makePreToolHook("c", {
|
|
267
|
-
phase: HookPhase.EARLY,
|
|
268
|
-
priority: 50,
|
|
269
|
-
dependencies: ["b"],
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const sorted = reg.topologicalSort([c, a, b]);
|
|
273
|
-
assert.equal(sorted[0].metadata.name, "a");
|
|
274
|
-
assert.equal(sorted[1].metadata.name, "b");
|
|
275
|
-
assert.equal(sorted[2].metadata.name, "c");
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it("handles diamond dependencies (A→B→D and A→C→D)", () => {
|
|
279
|
-
const reg = HookRegistry.getInstance();
|
|
280
|
-
const a = makePreToolHook("a", {
|
|
281
|
-
phase: HookPhase.EARLY,
|
|
282
|
-
priority: 50,
|
|
283
|
-
dependencies: [],
|
|
284
|
-
});
|
|
285
|
-
const b = makePreToolHook("b", {
|
|
286
|
-
phase: HookPhase.EARLY,
|
|
287
|
-
priority: 50,
|
|
288
|
-
dependencies: ["a"],
|
|
289
|
-
});
|
|
290
|
-
const c = makePreToolHook("c", {
|
|
291
|
-
phase: HookPhase.EARLY,
|
|
292
|
-
priority: 50,
|
|
293
|
-
dependencies: ["a"],
|
|
294
|
-
});
|
|
295
|
-
const d = makePreToolHook("d", {
|
|
296
|
-
phase: HookPhase.EARLY,
|
|
297
|
-
priority: 50,
|
|
298
|
-
dependencies: ["b", "c"],
|
|
299
|
-
});
|
|
300
274
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
assert.equal(sorted[0].metadata.name, "
|
|
304
|
-
assert.equal(sorted[
|
|
305
|
-
// B and C can be in any order but must be before D and after A
|
|
306
|
-
const bIdx = sorted.findIndex((h) => h.metadata.name === "b");
|
|
307
|
-
const cIdx = sorted.findIndex((h) => h.metadata.name === "c");
|
|
308
|
-
assert.ok(bIdx > 0);
|
|
309
|
-
assert.ok(cIdx > 0);
|
|
310
|
-
assert.ok(bIdx < 3);
|
|
311
|
-
assert.ok(cIdx < 3);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("throws on circular dependency (A→B→C→A)", () => {
|
|
315
|
-
const reg = HookRegistry.getInstance();
|
|
316
|
-
const a = makePreToolHook("a", {
|
|
317
|
-
phase: HookPhase.EARLY,
|
|
318
|
-
dependencies: ["c"],
|
|
319
|
-
});
|
|
320
|
-
const b = makePreToolHook("b", {
|
|
321
|
-
phase: HookPhase.EARLY,
|
|
322
|
-
dependencies: ["a"],
|
|
323
|
-
});
|
|
324
|
-
const c = makePreToolHook("c", {
|
|
325
|
-
phase: HookPhase.EARLY,
|
|
326
|
-
dependencies: ["b"],
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
assert.throws(
|
|
330
|
-
() => reg.topologicalSort([a, b, c]),
|
|
331
|
-
/Circular dependency detected/,
|
|
332
|
-
);
|
|
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");
|
|
333
279
|
});
|
|
334
280
|
|
|
335
|
-
it("
|
|
281
|
+
it("preserves original order for equal phase and priority", () => {
|
|
336
282
|
const reg = HookRegistry.getInstance();
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
});
|
|
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 });
|
|
341
286
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
);
|
|
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");
|
|
346
292
|
});
|
|
347
293
|
|
|
348
294
|
it("cross-phase dependencies are ignored (not within same phase)", () => {
|
|
@@ -381,6 +327,9 @@ describe("HookRegistry", () => {
|
|
|
381
327
|
|
|
382
328
|
const result = await reg.executePreTool(makeContext());
|
|
383
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");
|
|
384
333
|
assert.equal(result.modifiedContext?._track, "ran");
|
|
385
334
|
});
|
|
386
335
|
|
|
@@ -462,12 +411,12 @@ describe("HookRegistry", () => {
|
|
|
462
411
|
assert.equal(result.modifiedOutput, "[[[HELLO]]]");
|
|
463
412
|
});
|
|
464
413
|
|
|
465
|
-
it("injects recovery action", async () => {
|
|
414
|
+
it("injects recovery action (stub)", async () => {
|
|
415
|
+
// Recovery field removed in cleanup — test kept as placeholder
|
|
466
416
|
const reg = HookRegistry.getInstance();
|
|
467
417
|
reg.registerPostTool(
|
|
468
418
|
makePostToolHook("recovery-test", {}, async () => ({
|
|
469
419
|
result: HookResult.INJECT,
|
|
470
|
-
injectRecovery: "retry with backoff",
|
|
471
420
|
})),
|
|
472
421
|
);
|
|
473
422
|
|
|
@@ -475,7 +424,83 @@ describe("HookRegistry", () => {
|
|
|
475
424
|
makeContext(),
|
|
476
425
|
"output",
|
|
477
426
|
);
|
|
478
|
-
assert.equal(result.
|
|
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");
|
|
479
504
|
});
|
|
480
505
|
});
|
|
481
506
|
|
|
@@ -586,18 +611,6 @@ describe("HookRegistry", () => {
|
|
|
586
611
|
assert.equal(delegationDepthHook.metadata.phase, HookPhase.NORMAL);
|
|
587
612
|
});
|
|
588
613
|
|
|
589
|
-
it("errorRecoveryHook has correct metadata", () => {
|
|
590
|
-
assert.equal(errorRecoveryHook.metadata.name, "error-recovery");
|
|
591
|
-
assert.equal(errorRecoveryHook.metadata.priority, 50);
|
|
592
|
-
assert.equal(errorRecoveryHook.metadata.phase, HookPhase.LATE);
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
it("memorySyncHook has correct metadata", () => {
|
|
596
|
-
assert.equal(memorySyncHook.metadata.name, "memory-sync");
|
|
597
|
-
assert.equal(memorySyncHook.metadata.priority, 40);
|
|
598
|
-
assert.equal(memorySyncHook.metadata.phase, HookPhase.LATE);
|
|
599
|
-
});
|
|
600
|
-
|
|
601
614
|
it("shellDetectHook returns shell context", async () => {
|
|
602
615
|
const result = await shellDetectHook.execute(makeContext());
|
|
603
616
|
assert.equal(result.result, HookResult.CONTINUE);
|
|
@@ -624,24 +637,6 @@ describe("HookRegistry", () => {
|
|
|
624
637
|
assert.equal(result.modifiedContext?._depthExceeded, true);
|
|
625
638
|
});
|
|
626
639
|
|
|
627
|
-
it("errorRecoveryHook returns CONTINUE for normal output", async () => {
|
|
628
|
-
const result = await errorRecoveryHook.execute(
|
|
629
|
-
makeContext(),
|
|
630
|
-
"Everything completed successfully.",
|
|
631
|
-
);
|
|
632
|
-
assert.equal(result.result, HookResult.CONTINUE);
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
it("errorRecoveryHook detects error output", async () => {
|
|
636
|
-
const result = await errorRecoveryHook.execute(
|
|
637
|
-
makeContext(),
|
|
638
|
-
"Error: Failed to connect to server",
|
|
639
|
-
);
|
|
640
|
-
assert.equal(result.result, HookResult.INJECT);
|
|
641
|
-
assert.ok(result.injectRecovery);
|
|
642
|
-
assert.ok(result.injectRecovery!.includes("Error Recovery"));
|
|
643
|
-
});
|
|
644
|
-
|
|
645
640
|
it("confidenceGateHook passes through without confidence info", async () => {
|
|
646
641
|
const result = await confidenceGateHook.execute(
|
|
647
642
|
makeContext(),
|
|
@@ -666,12 +661,9 @@ describe("HookRegistry", () => {
|
|
|
666
661
|
reg.registerPreTool(shellDetectHook);
|
|
667
662
|
reg.registerPreTool(delegationDepthHook);
|
|
668
663
|
reg.registerRoute(confidenceGateHook);
|
|
669
|
-
reg.registerPostTool(errorRecoveryHook);
|
|
670
|
-
reg.registerPostTool(memorySyncHook);
|
|
671
664
|
|
|
672
665
|
assert.equal(reg.getPreToolHooks().length, 3);
|
|
673
666
|
assert.equal(reg.getRouteHooks().length, 1);
|
|
674
|
-
assert.equal(reg.getPostToolHooks().length, 2);
|
|
675
667
|
});
|
|
676
668
|
|
|
677
669
|
it("routeTrackingHook has correct metadata", () => {
|
|
@@ -692,9 +684,6 @@ describe("HookRegistry", () => {
|
|
|
692
684
|
// Route hook
|
|
693
685
|
await confidenceGateHook.execute(ctx, "oh-builder");
|
|
694
686
|
|
|
695
|
-
// Post-tool hooks
|
|
696
|
-
await errorRecoveryHook.execute(ctx, "normal output");
|
|
697
|
-
await memorySyncHook.execute(ctx, "some output");
|
|
698
687
|
// If we got here without throwing, success
|
|
699
688
|
assert.ok(true);
|
|
700
689
|
});
|
|
@@ -724,17 +713,9 @@ describe("HookRegistry", () => {
|
|
|
724
713
|
const result = await routeTrackingHook.execute(ctx, "oh-builder");
|
|
725
714
|
assert.equal(result.result, HookResult.STOP);
|
|
726
715
|
assert.ok(ctx._optiRoute);
|
|
727
|
-
assert.ok(
|
|
728
|
-
|
|
729
|
-
);
|
|
730
|
-
assert.ok(
|
|
731
|
-
((ctx._optiRoute as Record<string, unknown>).reason as string).includes(
|
|
732
|
-
"oh-builder",
|
|
733
|
-
),
|
|
734
|
-
);
|
|
735
|
-
assert.ok(
|
|
736
|
-
((ctx._optiRoute as Record<string, unknown>).chain as unknown[]).length === 5,
|
|
737
|
-
);
|
|
716
|
+
assert.ok(ctx._optiRoute.reason);
|
|
717
|
+
assert.ok(ctx._optiRoute.reason.includes("oh-builder"));
|
|
718
|
+
assert.ok(ctx._optiRoute.chain.length === 5);
|
|
738
719
|
});
|
|
739
720
|
|
|
740
721
|
it("stops on 8th unproductive hop (default max 8)", async () => {
|
|
@@ -757,11 +738,7 @@ describe("HookRegistry", () => {
|
|
|
757
738
|
const result = await routeTrackingHook.execute(ctx, "oh-builder");
|
|
758
739
|
assert.equal(result.result, HookResult.STOP);
|
|
759
740
|
assert.ok(ctx._optiRoute);
|
|
760
|
-
assert.ok(
|
|
761
|
-
((ctx._optiRoute as Record<string, unknown>).reason as string).includes(
|
|
762
|
-
"unproductive",
|
|
763
|
-
),
|
|
764
|
-
);
|
|
741
|
+
assert.ok(ctx._optiRoute.reason.includes("unproductive"));
|
|
765
742
|
});
|
|
766
743
|
|
|
767
744
|
it("productive hop resets unproductive counter", async () => {
|
|
@@ -875,11 +852,7 @@ describe("HookRegistry", () => {
|
|
|
875
852
|
const result = await routeTrackingHook.execute(ctx, "oh-gauntlet");
|
|
876
853
|
assert.equal(result.result, HookResult.STOP);
|
|
877
854
|
assert.ok(ctx._optiRoute);
|
|
878
|
-
assert.ok(
|
|
879
|
-
((ctx._optiRoute as Record<string, unknown>).reason as string).includes(
|
|
880
|
-
"unproductive",
|
|
881
|
-
),
|
|
882
|
-
);
|
|
855
|
+
assert.ok(ctx._optiRoute.reason.includes("unproductive"));
|
|
883
856
|
});
|
|
884
857
|
});
|
|
885
858
|
});
|
|
@@ -952,65 +925,4 @@ describe("HookRegistry", () => {
|
|
|
952
925
|
});
|
|
953
926
|
});
|
|
954
927
|
|
|
955
|
-
// ---------------------------------------------------------------------------
|
|
956
|
-
// sanityCheckHook
|
|
957
|
-
// ---------------------------------------------------------------------------
|
|
958
|
-
|
|
959
|
-
describe("sanityCheckHook", () => {
|
|
960
|
-
beforeEach(() => {
|
|
961
|
-
AnomalyTracker.getInstance().resetAll();
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
it("passes clean output through unchanged", async () => {
|
|
965
|
-
const ctx = makeContext();
|
|
966
|
-
const result = await sanityCheckHook.execute(
|
|
967
|
-
ctx,
|
|
968
|
-
"Everything is working fine. The system completed the task successfully.",
|
|
969
|
-
);
|
|
970
|
-
assert.equal(result.result, HookResult.CONTINUE);
|
|
971
|
-
assert.equal(result.modifiedOutput, undefined);
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
it("detects repetitive output", async () => {
|
|
975
|
-
const ctx = makeContext();
|
|
976
|
-
const line =
|
|
977
|
-
"Sphinx of black quartz, judge my vow! The five boxing wizards jump quickly. 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
978
|
-
const repetitiveOutput = Array.from({ length: 20 }, () => line).join("\n");
|
|
979
|
-
const result = await sanityCheckHook.execute(ctx, repetitiveOutput);
|
|
980
|
-
// First anomaly — not yet escalated
|
|
981
|
-
assert.equal(result.result, HookResult.CONTINUE);
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
it("detects box-drawing character flooding", async () => {
|
|
985
|
-
const ctx = makeContext();
|
|
986
|
-
const boxArt = "─│┌┐└┘├┤┬┴┼".repeat(50);
|
|
987
|
-
const result = await sanityCheckHook.execute(ctx, boxArt);
|
|
988
|
-
assert.equal(result.result, HookResult.CONTINUE);
|
|
989
|
-
});
|
|
990
|
-
|
|
991
|
-
it("detects placeholder patterns", async () => {
|
|
992
|
-
const ctx = makeContext();
|
|
993
|
-
// 50× [PLACEHOLDER] = 650 chars, 11 unique → triggers low_diversity check
|
|
994
|
-
const placeholderText = "[PLACEHOLDER]".repeat(50);
|
|
995
|
-
const result = await sanityCheckHook.execute(ctx, placeholderText);
|
|
996
|
-
assert.equal(result.result, HookResult.CONTINUE);
|
|
997
|
-
});
|
|
998
|
-
|
|
999
|
-
it("tracks anomalies across calls", async () => {
|
|
1000
|
-
const ctx = makeContext({ sessionId: "anomaly-escalation-test" });
|
|
1001
|
-
const line =
|
|
1002
|
-
"Sphinx of black quartz, judge my vow! The five boxing wizards jump quickly. 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
1003
|
-
const repetitiveOutput = Array.from({ length: 20 }, () => line).join("\n");
|
|
1004
|
-
|
|
1005
|
-
// First call: anomaly detected but below threshold → CONTINUE
|
|
1006
|
-
const firstResult = await sanityCheckHook.execute(ctx, repetitiveOutput);
|
|
1007
|
-
assert.equal(firstResult.result, HookResult.CONTINUE);
|
|
1008
|
-
|
|
1009
|
-
// Second call: threshold reached → INJECT with recovery
|
|
1010
|
-
const secondResult = await sanityCheckHook.execute(ctx, repetitiveOutput);
|
|
1011
|
-
assert.equal(secondResult.result, HookResult.INJECT);
|
|
1012
|
-
assert.ok(secondResult.injectRecovery);
|
|
1013
|
-
assert.equal(secondResult.modifiedOutput, repetitiveOutput);
|
|
1014
|
-
});
|
|
1015
|
-
});
|
|
1016
928
|
});
|
|
@@ -1,30 +1,38 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Hook System — barrel export
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
|
-
export {
|
|
6
|
-
HookPhase,
|
|
7
|
-
HookResult,
|
|
8
|
-
} from "./types.ts";
|
|
9
|
-
export type {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export {
|
|
26
|
-
export {
|
|
27
|
-
export {
|
|
28
|
-
export {
|
|
29
|
-
export {
|
|
30
|
-
export
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Hook System — barrel export
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
HookPhase,
|
|
7
|
+
HookResult,
|
|
8
|
+
} from "./types.ts";
|
|
9
|
+
export type {
|
|
10
|
+
HookContextBase,
|
|
11
|
+
HookContextExtras,
|
|
12
|
+
HookContext,
|
|
13
|
+
HookContextPatch,
|
|
14
|
+
HookMetadata,
|
|
15
|
+
PreToolUseHook,
|
|
16
|
+
PostToolUseHook,
|
|
17
|
+
RouteHook,
|
|
18
|
+
SessionHook,
|
|
19
|
+
AnyHook,
|
|
20
|
+
} from "./types.ts";
|
|
21
|
+
|
|
22
|
+
export { HookRegistry } from "./registry.ts";
|
|
23
|
+
|
|
24
|
+
// Built-in hooks
|
|
25
|
+
export { planCheckHook } from "./builtins/plan-check-hook.ts";
|
|
26
|
+
export { shellDetectHook } from "./builtins/shell-detect-hook.ts";
|
|
27
|
+
export { confidenceGateHook } from "./builtins/confidence-gate-hook.ts";
|
|
28
|
+
export { delegationDepthHook, resetDepthTracker } from "./builtins/delegation-depth-hook.ts";
|
|
29
|
+
export { dynamicRouteHook } from "./builtins/dynamic-route-hook.ts";
|
|
30
|
+
export { nextRouteHook } from "./builtins/next-route-hook.ts";
|
|
31
|
+
export { routeTrackingHook, resetRouteTracker, getHopHistory } from "./builtins/route-tracking-hook.ts";
|
|
32
|
+
export type { HopRecord, RouteTrackingConfig } from "./builtins/route-tracking-hook.ts";
|
|
33
|
+
|
|
34
|
+
// Guard configuration
|
|
35
|
+
export type { GuardConfig, GuardProgression, GuardLevel } from "../guards/guard-config.ts";
|
|
36
|
+
export { DEFAULT_GUARD_CONFIG, checkGuardProgression, mergeGuardConfig } from "../guards/guard-config.ts";
|
|
37
|
+
|
|
38
|
+
|