openhermes 4.9.2 → 4.12.1

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 (85) hide show
  1. package/CONTEXT.md +7 -7
  2. package/ETHOS.md +2 -2
  3. package/README.md +34 -33
  4. package/bootstrap.ts +310 -160
  5. package/harness/agents/oh-planner.md +1 -1
  6. package/harness/agents/openhermes.md +27 -126
  7. package/harness/codex/AUTOPILOT.md +131 -23
  8. package/harness/codex/CHARTER.md +4 -5
  9. package/harness/lib/background/background.test.ts +216 -0
  10. package/harness/lib/background/index.ts +7 -0
  11. package/harness/lib/background/interfaces.ts +31 -0
  12. package/harness/lib/background/manager.ts +320 -0
  13. package/harness/lib/composer/compose.test.ts +179 -0
  14. package/harness/lib/composer/compose.ts +65 -0
  15. package/harness/lib/composer/fragments/01-identity.md +1 -0
  16. package/harness/lib/composer/fragments/02-delegation.md +7 -0
  17. package/harness/lib/composer/fragments/03-permissions.md +13 -0
  18. package/harness/lib/composer/fragments/04-task-flow.md +55 -0
  19. package/harness/lib/composer/fragments/05-confidence.md +5 -0
  20. package/harness/lib/composer/fragments/06-parallelization.md +17 -0
  21. package/harness/lib/composer/fragments/07-shell.md +41 -0
  22. package/harness/lib/composer/fragments/08-routing.md +8 -0
  23. package/harness/lib/composer/fragments/09-guardrails.md +25 -0
  24. package/harness/lib/composer/index.ts +1 -0
  25. package/harness/lib/guards/guard-config.ts +72 -0
  26. package/harness/lib/hooks/builtins/confidence-gate-hook.ts +68 -0
  27. package/harness/lib/hooks/builtins/delegation-depth-hook.ts +78 -0
  28. package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -0
  29. package/harness/lib/hooks/builtins/error-recovery-hook.ts +107 -0
  30. package/harness/lib/hooks/builtins/memory-sync-hook.ts +73 -0
  31. package/harness/lib/hooks/builtins/next-route-hook.ts +24 -0
  32. package/harness/lib/hooks/builtins/plan-check-hook.ts +43 -0
  33. package/harness/lib/hooks/builtins/route-tracking-hook.ts +201 -0
  34. package/harness/lib/hooks/builtins/sanity-check-hook.ts +52 -0
  35. package/harness/lib/hooks/builtins/shell-detect-hook.ts +96 -0
  36. package/harness/lib/hooks/builtins/subagent-failure-hook.ts +93 -0
  37. package/harness/lib/hooks/hooks.test.ts +1092 -0
  38. package/harness/lib/hooks/index.ts +42 -0
  39. package/harness/lib/hooks/registry.ts +416 -0
  40. package/harness/lib/hooks/types.ts +119 -0
  41. package/harness/lib/memory/index.ts +18 -0
  42. package/harness/lib/memory/interfaces.ts +53 -0
  43. package/harness/lib/memory/memory-manager.ts +205 -0
  44. package/harness/lib/memory/memory.test.ts +485 -0
  45. package/harness/lib/memory/plan-store.ts +346 -0
  46. package/harness/lib/plans/plan-location.ts +134 -0
  47. package/harness/lib/recovery/handler.ts +243 -0
  48. package/harness/lib/recovery/index.ts +14 -0
  49. package/harness/lib/recovery/interfaces.ts +48 -0
  50. package/harness/lib/recovery/patterns.ts +149 -0
  51. package/harness/lib/recovery/recovery.test.ts +312 -0
  52. package/harness/lib/routing/index.ts +21 -0
  53. package/harness/lib/routing/route-guidance.ts +147 -0
  54. package/harness/lib/routing/route-resolver.ts +58 -0
  55. package/harness/lib/routing/routing.test.ts +195 -0
  56. package/harness/lib/routing/skill-frontmatter.ts +125 -0
  57. package/harness/lib/routing/types.ts +52 -0
  58. package/harness/lib/sanity/anomaly-tracker.ts +127 -0
  59. package/harness/lib/sanity/checker.ts +189 -0
  60. package/harness/lib/sanity/index.ts +13 -0
  61. package/harness/lib/sanity/interfaces.ts +24 -0
  62. package/harness/lib/sanity/sanity.test.ts +472 -0
  63. package/harness/lib/sync/file-watcher.ts +175 -0
  64. package/harness/lib/sync/index.ts +11 -0
  65. package/harness/lib/sync/interfaces.ts +27 -0
  66. package/harness/lib/sync/plan-sync.ts +533 -0
  67. package/harness/lib/sync/sync.test.ts +858 -0
  68. package/harness/skills/oh-fusion/DEEP.md +109 -86
  69. package/harness/skills/oh-fusion/SKILL.md +47 -33
  70. package/harness/skills/oh-init/DEEP.md +2 -2
  71. package/harness/skills/oh-manifest/SKILL.md +2 -1
  72. package/harness/skills/oh-plan-review/DEEP.md +1 -1
  73. package/harness/skills/oh-planner/DEEP.md +3 -3
  74. package/harness/skills/oh-review/DEEP.md +5 -3
  75. package/harness/skills/oh-review/SKILL.md +1 -0
  76. package/harness/skills/oh-ship/SKILL.md +1 -1
  77. package/harness/skills/oh-skill-craft/SKILL.md +1 -4
  78. package/package.json +53 -55
  79. package/tsconfig.json +1 -1
  80. package/harness/commands/oh-doctor.md +0 -205
  81. package/harness/commands/oh-log.md +0 -18
  82. package/harness/skills/oh-learn/DEEP.md +0 -44
  83. package/harness/skills/oh-learn/SKILL.md +0 -30
  84. package/scripts/count-tokens.mjs +0 -158
  85. package/scripts/oh-doctor.ps1 +0 -342
@@ -0,0 +1,41 @@
1
+ ## Confidence Gate Examples
2
+
3
+ **HIGH (transparent):**
4
+ > User: "There's a bug in the login flow"
5
+ > Orchestrator: (no conversation) → Classifies as INVESTIGATION → Loads oh-investigate
6
+
7
+ **MEDIUM (echo):**
8
+ > User: "Clean up the codebase and make it faster"
9
+ > Orchestrator: "I hear performance + cleanup work. Routing to oh-planner for a plan — does that match?"
10
+ > User: "Yes" → Classifies → Delegates
11
+ > (If "No, just run lint" → Re-analyzes → Classifies as HEALTH → Loads oh-health)
12
+
13
+ **LOW (question):**
14
+ > User: "I have an idea for the app"
15
+ > Orchestrator: "Quick one — is this about a new feature, a redesign, or something else?"
16
+ > User: "A new feature" → Classifies as PLANNING → Loads oh-planner
17
+ > (No answer → Default to oh-planner)
18
+
19
+ ## Shell Awareness (Windows)
20
+
21
+ You run on Windows. Three possible shells: CMD, PowerShell, Git Bash. Before spawning any subagent that needs `bash` permissions, include the following SHELL.md preamble in the subagent's task prompt. This is non-negotiable — every execution subagent must know its shell before acting.
22
+
23
+ Subagent task preamble — prepend to every execution subagent prompt:
24
+ ~~~markdown
25
+ ## Shell Pre-flight
26
+ Detect your shell before any command:
27
+ - `$PSVersionTable` exists → PowerShell
28
+ - `%CMDCMDLINE%` is set → CMD
29
+ - `$0` or `$BASH` → Git Bash
30
+
31
+ Required shell by operation:
32
+ - file ops, scoop, ps1 scripts, env vars → PowerShell
33
+ - git, bun, npm, node → any shell (all work)
34
+ - rm -rf, make, unix scripts → Git Bash
35
+ - .bat/.cmd → CMD
36
+
37
+ If wrong shell:
38
+ - → PowerShell: `powershell.exe -NoProfile -Command "..."`
39
+ - → Git Bash: `& "C:\Program Files\Git\bin\bash.exe" -c "..."`
40
+ - → CMD: `cmd.exe /c "..."`
41
+ ~~~
@@ -0,0 +1,8 @@
1
+ ## Plan Storage
2
+
3
+ Canonical path: `~/.local/share/openhermes/plans/<project-name>/plan-<nnn>.md`
4
+
5
+ - Plan files use `<project-name>/plan-<nnn>.md` naming — one directory per project, sequence zero-padded to 3 digits
6
+ - Status lifecycle: keep `active`/`in-progress`/`blocked`, delete `complete`/`abandoned`
7
+ - Entries are direct filesystem operations — no tracking DB
8
+ - The bootstrap plugin's `ensurePlanFile()` handles creation and reuse; delegate to sub-agents when possible
@@ -0,0 +1,25 @@
1
+ ## Guardrails
2
+
3
+ - All loop and safety limits are mechanically enforced by hooks (route-tracking, delegation-depth, subagent-failure). See AUTOPILOT.md §Safety Valves for limits and configuration.
4
+ - Before routing: if next skill's required input is missing and cannot be discovered → surface
5
+ - Concrete, low-risk findings from review or investigation are implementation candidates, not report-only endpoints; dispatch to oh-builder immediately.
6
+ - Confidence is evaluated once per session, not per routing hop — only re-evaluate when new user input arrives
7
+ - User skills at `~/.agents/skills/` and `~/.config/opencode/skills/` load on demand via skill tool
8
+ - Do not ask the user to resolve something the codebase or prior conversation already resolves. Ask only for true blockers.
9
+ - For fusion or protocol work, stop at an explicit approval gate before changing the harness. Approved plan in context counts as approval.
10
+ - If a proposed protocol makes OH weaker, slower, noisier, or less native, call that out, revise it, and prefer the stronger path before routing onward.
11
+
12
+ ## Routing
13
+
14
+ After every skill (in priority order):
15
+ 1. `NEXT_ROUTE: <skill>` from output — explicit override, highest priority
16
+ 2. `ROUTE_GUIDANCE.selected` from output — evidence-driven route, including richer routing signals
17
+ 3. Skill's `route:` frontmatter (pass / fail / blocker) — static fallback
18
+
19
+ For multi-candidate routes (e.g., pass: [oh-gauntlet, oh-ship]), the orchestrator should emit `ROUTE_EVIDENCE:` JSON with the richer schema. The runtime resolver applies these rules:
20
+ - verified + done + ship → prefers `oh-ship`
21
+ - unverified → prefers `oh-gauntlet`
22
+ - fixable / implement → prefers `oh-builder`
23
+ - explicit target in evidence → preferred when valid
24
+
25
+ Route immediately. Do not ask. Route values: `oh-<name>` (another skill), `surface`, `done` (terminal), `[a, b]` (choose with evidence). Internal switch: `mode`. If the result is a concrete, low-risk fix, do not end in a report: hand it to oh-builder.
@@ -0,0 +1 @@
1
+ export { compose, composeFragment, listFragments } from "./compose.ts"
@@ -0,0 +1,72 @@
1
+ // ---------------------------------------------------------------------------
2
+ // GuardConfig — centralized configuration for all loop/safety guards
3
+ // ---------------------------------------------------------------------------
4
+
5
+ export interface GuardConfig {
6
+ /** Max times the same skill can repeat in one chain before STOP */
7
+ maxSkillRepeats: number
8
+ /** Max consecutive unproductive hops before STOP (0 = disabled) */
9
+ maxUnproductiveHops: number
10
+ /** Max delegation (sub-agent) depth before STOP */
11
+ maxDelegationDepth: number
12
+ /** Consecutive anomalies before recovery escalation */
13
+ maxConsecutiveAnomalies: number
14
+ /** Max subagent failures on same task before BLOCKER */
15
+ maxSubagentFailures: number
16
+ /** Enable progressive warning at thresholds before hard stop */
17
+ progressiveGuards: boolean
18
+ /** Ratio of limit at which to warn (e.g. 0.6 = 60%) */
19
+ progressiveWarnThreshold: number
20
+ /** Ratio of limit at which to escalate (e.g. 0.8 = 80%) */
21
+ progressiveEscalateThreshold: number
22
+ }
23
+
24
+ export const DEFAULT_GUARD_CONFIG: GuardConfig = {
25
+ maxSkillRepeats: 5,
26
+ maxUnproductiveHops: 8,
27
+ maxDelegationDepth: 25,
28
+ maxConsecutiveAnomalies: 2,
29
+ maxSubagentFailures: 5,
30
+ progressiveGuards: true,
31
+ progressiveWarnThreshold: 0.6,
32
+ progressiveEscalateThreshold: 0.8,
33
+ }
34
+
35
+ export type GuardLevel = "ok" | "warn" | "escalate" | "stop"
36
+
37
+ export interface GuardProgression {
38
+ level: GuardLevel
39
+ current: number
40
+ limit: number
41
+ /**
42
+ * If progressive guards are disabled: stop at limit, ok otherwise.
43
+ * If enabled: ok < warn% < escalate% < stop.
44
+ */
45
+ }
46
+
47
+ export function checkGuardProgression(
48
+ current: number,
49
+ limit: number,
50
+ config: GuardConfig,
51
+ ): GuardProgression {
52
+ if (!config.progressiveGuards || limit <= 0) {
53
+ return {
54
+ level: current >= limit ? "stop" as GuardLevel : "ok" as GuardLevel,
55
+ current,
56
+ limit,
57
+ }
58
+ }
59
+ if (current >= limit) return { level: "stop", current, limit }
60
+ if (current / limit >= config.progressiveEscalateThreshold) return { level: "escalate" as GuardLevel, current, limit }
61
+ if (current / limit >= config.progressiveWarnThreshold) return { level: "warn" as GuardLevel, current, limit }
62
+ return { level: "ok" as GuardLevel, current, limit }
63
+ }
64
+
65
+ /**
66
+ * Merge partial user config(s) with defaults.
67
+ * Priority: defaults → earlier args → later args (last wins).
68
+ * Supports single-arg calls and multi-override chains.
69
+ */
70
+ export function mergeGuardConfig(...overrides: Array<Partial<GuardConfig> | undefined>): GuardConfig {
71
+ return Object.assign({}, DEFAULT_GUARD_CONFIG, ...overrides.filter(Boolean));
72
+ }
@@ -0,0 +1,68 @@
1
+ // ---------------------------------------------------------------------------
2
+ // ConfidenceGateHook — RouteHook, priority=70, phase=NORMAL
3
+ //
4
+ // Before routing, check if confidence gate needs to pause.
5
+ // Adjust route based on confidence level.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ import { HookPhase, HookResult } from "../types.ts";
9
+ import type { HookContext, RouteHook } from "../types.ts";
10
+
11
+ export interface ConfidenceGateState {
12
+ level: "HIGH" | "MEDIUM" | "LOW";
13
+ exchanges: number;
14
+ lastAction: string;
15
+ }
16
+
17
+ export const confidenceGateHook: RouteHook = {
18
+ metadata: {
19
+ name: "confidence-gate",
20
+ priority: 70,
21
+ phase: HookPhase.NORMAL,
22
+ dependencies: [],
23
+ errorHandling: "isolate",
24
+ },
25
+
26
+ async execute(context: HookContext, route: string) {
27
+ // Read confidence state from context if available
28
+ const confidenceLevel = context._confidenceLevel;
29
+
30
+ if (!confidenceLevel) {
31
+ // No confidence gate info — pass through unchanged
32
+ return { result: HookResult.CONTINUE, modifiedRoute: route };
33
+ }
34
+
35
+ // Store the confidence assessment for routing decisions
36
+ const state: ConfidenceGateState = {
37
+ level: confidenceLevel as ConfidenceGateState["level"],
38
+ exchanges: context._confidenceExchanges ?? 0,
39
+ lastAction: "assessed",
40
+ };
41
+
42
+ // HIGH confidence: proceed without modification
43
+ if (state.level === "HIGH") {
44
+ return {
45
+ result: HookResult.CONTINUE,
46
+ modifiedRoute: route,
47
+ };
48
+ }
49
+
50
+ // MEDIUM confidence: echo if first pass, otherwise proceed
51
+ if (state.level === "MEDIUM" && state.exchanges === 0) {
52
+ return {
53
+ result: HookResult.INJECT,
54
+ modifiedRoute: `${route}?echo=confirm`,
55
+ };
56
+ }
57
+
58
+ // LOW confidence: pause if first pass, otherwise proceed
59
+ if (state.level === "LOW" && state.exchanges === 0) {
60
+ return {
61
+ result: HookResult.INJECT,
62
+ modifiedRoute: `${route}?question=pause`,
63
+ };
64
+ }
65
+
66
+ return { result: HookResult.CONTINUE, modifiedRoute: route };
67
+ },
68
+ };
@@ -0,0 +1,78 @@
1
+ // ---------------------------------------------------------------------------
2
+ // DelegationDepthHook — PreToolUse, priority=60, phase=NORMAL
3
+ //
4
+ // Loop guard — track sub-agent call depth.
5
+ // If depth exceeds max, STOP and escalate.
6
+ // Progressive warning at thresholds before hard stop.
7
+ //
8
+ // Reads maxDelegationDepth from _guardConfig (centralized) with fallback
9
+ // to _maxDelegationDepth for backward compatibility.
10
+ // ---------------------------------------------------------------------------
11
+
12
+ import { HookPhase, HookResult } from "../types.ts";
13
+ import type { HookContext, PreToolUseHook } from "../types.ts";
14
+ import type { GuardConfig } from "../../guards/guard-config.ts";
15
+ import { checkGuardProgression, DEFAULT_GUARD_CONFIG } from "../../guards/guard-config.ts";
16
+
17
+ /** Module-level depth tracker — maps sessionId to current depth */
18
+ const depthTrackers = new Map<string, number>();
19
+
20
+ export function resetDepthTracker(): void {
21
+ depthTrackers.clear();
22
+ }
23
+
24
+ export function getDepth(sessionId: string): number {
25
+ return depthTrackers.get(sessionId) ?? 0;
26
+ }
27
+
28
+ export const delegationDepthHook: PreToolUseHook = {
29
+ metadata: {
30
+ name: "delegation-depth",
31
+ priority: 60,
32
+ phase: HookPhase.NORMAL,
33
+ dependencies: [],
34
+ errorHandling: "propagate",
35
+ },
36
+
37
+ async execute(context: HookContext) {
38
+ const sessionId = context.sessionId;
39
+
40
+ // Bump depth
41
+ const currentDepth = (depthTrackers.get(sessionId) ?? 0) + 1;
42
+ depthTrackers.set(sessionId, currentDepth);
43
+
44
+ // Resolve guard config for progression checks
45
+ const guardConfig: GuardConfig = context._guardConfig ?? DEFAULT_GUARD_CONFIG;
46
+
47
+ // Backward compat: if legacy _maxDelegationDepth is set, use it
48
+ // Otherwise use _guardConfig (centralized) with defaults
49
+ const legacyDepth = (context as any)._maxDelegationDepth as number | undefined;
50
+ const maxDepth = legacyDepth !== undefined ? legacyDepth : guardConfig.maxDelegationDepth;
51
+
52
+ // Progressive warning check
53
+ const progression = checkGuardProgression(currentDepth, maxDepth, guardConfig);
54
+
55
+ if (progression.level === "warn" || progression.level === "escalate") {
56
+ // Annotate context for the orchestrator but don't stop
57
+ context._guardProgression = progression;
58
+ }
59
+
60
+ if (progression.level === "stop") {
61
+ return {
62
+ result: HookResult.STOP,
63
+ modifiedContext: {
64
+ _depthExceeded: true,
65
+ _depthError: `LOOP GUARD: Delegation depth exceeded (max ${maxDepth}). Surface to orchestrator with findings and stop delegating.`,
66
+ _delegationDepth: currentDepth,
67
+ },
68
+ };
69
+ }
70
+
71
+ return {
72
+ result: HookResult.CONTINUE,
73
+ modifiedContext: {
74
+ _delegationDepth: currentDepth,
75
+ },
76
+ };
77
+ },
78
+ };
@@ -0,0 +1,99 @@
1
+ import path from "node:path";
2
+ import { HookPhase, HookResult } from "../types.ts";
3
+ import type { HookContext, PostToolUseHook } from "../types.ts";
4
+ import { readSkillFrontmatter, resolveRoute } from "../../routing/index.ts";
5
+ import type { RouteEvidence } from "../../routing/index.ts";
6
+ import { ROUTE_GUIDANCE_PREFIX } from "../../routing/index.ts";
7
+ import {
8
+ ROUTE_ACTIONS,
9
+ ROUTE_OUTCOMES,
10
+ ROUTE_VERIFICATIONS,
11
+ ROUTE_WORK_TYPES,
12
+ } from "../../routing/types.ts";
13
+
14
+ const ROUTE_EVIDENCE_PREFIX = "ROUTE_EVIDENCE:";
15
+
16
+ function isRouteOutcome(value: unknown): value is RouteEvidence["outcome"] {
17
+ return typeof value === "string" && ROUTE_OUTCOMES.includes(value as RouteEvidence["outcome"]);
18
+ }
19
+
20
+ function isRouteVerification(value: unknown): value is NonNullable<RouteEvidence["verification"]> {
21
+ return typeof value === "string" && ROUTE_VERIFICATIONS.includes(value as NonNullable<RouteEvidence["verification"]>);
22
+ }
23
+
24
+ function isRouteAction(value: unknown): value is NonNullable<RouteEvidence["action"]> {
25
+ return typeof value === "string" && ROUTE_ACTIONS.includes(value as NonNullable<RouteEvidence["action"]>);
26
+ }
27
+
28
+ function isRouteWork(value: unknown): value is NonNullable<RouteEvidence["work"]> {
29
+ return typeof value === "string" && ROUTE_WORK_TYPES.includes(value as NonNullable<RouteEvidence["work"]>);
30
+ }
31
+
32
+ function parseRouteEvidence(output: string): RouteEvidence | null {
33
+ const evidenceLine = output
34
+ .split(/\r?\n/)
35
+ .map((line) => line.trim())
36
+ .find((line) => line.startsWith(ROUTE_EVIDENCE_PREFIX));
37
+
38
+ if (!evidenceLine) return null;
39
+
40
+ const raw = evidenceLine.slice(ROUTE_EVIDENCE_PREFIX.length).trim();
41
+ if (!raw) return null;
42
+
43
+ try {
44
+ const parsed = JSON.parse(raw) as Partial<RouteEvidence>;
45
+ if (!isRouteOutcome(parsed.outcome)) return null;
46
+ if (parsed.verification !== undefined && !isRouteVerification(parsed.verification)) return null;
47
+ if (parsed.action !== undefined && !isRouteAction(parsed.action)) return null;
48
+ if (parsed.work !== undefined && !isRouteWork(parsed.work)) return null;
49
+ if (parsed.target !== undefined && typeof parsed.target !== "string") return null;
50
+ if (parsed.reason !== undefined && typeof parsed.reason !== "string") return null;
51
+
52
+ return {
53
+ outcome: parsed.outcome,
54
+ ...(parsed.verification ? { verification: parsed.verification } : {}),
55
+ ...(parsed.action ? { action: parsed.action } : {}),
56
+ ...(parsed.work ? { work: parsed.work } : {}),
57
+ ...(parsed.target ? { target: parsed.target } : {}),
58
+ ...(parsed.reason ? { reason: parsed.reason } : {}),
59
+ };
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ export const dynamicRouteHook: PostToolUseHook = {
66
+ metadata: {
67
+ name: "dynamic-route",
68
+ priority: 20,
69
+ phase: HookPhase.LATE,
70
+ dependencies: [],
71
+ errorHandling: "isolate",
72
+ },
73
+
74
+ async execute(context: HookContext, output: string) {
75
+ const evidence = parseRouteEvidence(output);
76
+ const skillsDir = typeof context._routingSkillsDir === "string" ? context._routingSkillsDir : undefined;
77
+
78
+ if (!evidence || !skillsDir || !context.agent) {
79
+ return { result: HookResult.CONTINUE };
80
+ }
81
+
82
+ const skillFilePath = path.join(skillsDir, context.agent, "SKILL.md");
83
+ const frontmatter = readSkillFrontmatter(skillFilePath);
84
+ if (!frontmatter) {
85
+ return { result: HookResult.CONTINUE };
86
+ }
87
+
88
+ const resolution = resolveRoute(frontmatter.route, evidence);
89
+ const guidance = `${ROUTE_GUIDANCE_PREFIX} ${JSON.stringify(resolution)}`;
90
+ const modifiedOutput = output.includes(ROUTE_GUIDANCE_PREFIX)
91
+ ? output
92
+ : `${output.trimEnd()}\n${guidance}`.trim();
93
+
94
+ return {
95
+ result: HookResult.INJECT,
96
+ modifiedOutput,
97
+ };
98
+ },
99
+ };
@@ -0,0 +1,107 @@
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
+ }
@@ -0,0 +1,73 @@
1
+ // ---------------------------------------------------------------------------
2
+ // MemorySyncHook — PostToolUse, priority=40, phase=LATE
3
+ //
4
+ // After each step, sync memory entries to plan file.
5
+ // Uses MemoryManager from harness/lib/memory/memory-manager.ts
6
+ // ---------------------------------------------------------------------------
7
+
8
+ import { HookPhase, HookResult } from "../types.ts";
9
+ import type { HookContext, PostToolUseHook } from "../types.ts";
10
+ import { MemoryManager } from "../../memory/memory-manager.ts";
11
+ import { PlanStore } from "../../memory/plan-store.ts";
12
+ import { resolvePlanAccess } from "../../plans/plan-location.ts";
13
+ import { MemoryLevel } from "../../memory/interfaces.ts";
14
+
15
+ export const memorySyncHook: PostToolUseHook = {
16
+ metadata: {
17
+ name: "memory-sync",
18
+ priority: 40,
19
+ phase: HookPhase.LATE,
20
+ dependencies: [],
21
+ errorHandling: "isolate",
22
+ },
23
+
24
+ async execute(context: HookContext, output: string) {
25
+ // Sync memory entries to plan file
26
+ const planFile = resolvePlanAccess(context.directory)?.path ?? null;
27
+ if (!planFile) {
28
+ // No plan file to sync to — skip silently
29
+ return { result: HookResult.CONTINUE };
30
+ }
31
+
32
+ const mem = MemoryManager.getInstance();
33
+
34
+ // Extract findings from the session memory (TASK level)
35
+ const taskEntries = mem.getEntries(MemoryLevel.TASK);
36
+
37
+ // Sync important task entries as plan findings
38
+ for (const entry of taskEntries) {
39
+ if (entry.importance >= 0.6) {
40
+ try {
41
+ await PlanStore.addFinding(planFile, context.sessionId, {
42
+ description: entry.content,
43
+ severity: entry.importance >= 0.8 ? "warning" : "info",
44
+ });
45
+ } catch {
46
+ // Plan sync is best-effort — don't break execution
47
+ }
48
+ }
49
+ }
50
+
51
+ // Sync any decisions
52
+ const missionEntries = mem.getEntries(MemoryLevel.MISSION);
53
+ for (const entry of missionEntries) {
54
+ if (entry.metadata?.type === "decision" && entry.importance >= 0.7) {
55
+ try {
56
+ await PlanStore.addDecision(planFile, context.sessionId, {
57
+ description: entry.content,
58
+ rationale: (entry.metadata.rationale as string) ?? "Auto-synced decision",
59
+ });
60
+ } catch {
61
+ // Best-effort
62
+ }
63
+ }
64
+ }
65
+
66
+ return {
67
+ result: HookResult.CONTINUE,
68
+ modifiedContext: {
69
+ _memorySyncCount: taskEntries.filter((e) => e.importance >= 0.6).length,
70
+ },
71
+ };
72
+ },
73
+ };
@@ -0,0 +1,24 @@
1
+ import { HookPhase, HookResult } from "../types.ts";
2
+ import type { HookContext, RouteHook } from "../types.ts";
3
+
4
+ export const nextRouteHook: RouteHook = {
5
+ metadata: {
6
+ name: "next-route",
7
+ priority: 90,
8
+ phase: HookPhase.EARLY,
9
+ dependencies: [],
10
+ errorHandling: "isolate",
11
+ },
12
+
13
+ async execute(context: HookContext, route: string) {
14
+ const nextRoute = context._nextRoute?.selected;
15
+ if (!nextRoute || nextRoute === route) {
16
+ return { result: HookResult.CONTINUE, modifiedRoute: route };
17
+ }
18
+
19
+ return {
20
+ result: HookResult.CONTINUE,
21
+ modifiedRoute: nextRoute,
22
+ };
23
+ },
24
+ };
@@ -0,0 +1,43 @@
1
+ // ---------------------------------------------------------------------------
2
+ // PlanCheckHook — PreToolUse, priority=90, phase=EARLY
3
+ //
4
+ // Before any sub-agent call, verify plan file exists at the expected path.
5
+ // If missing, inject "create plan first" instruction.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ import { HookPhase, HookResult } from "../types.ts";
9
+ import type { HookContext, PreToolUseHook } from "../types.ts";
10
+ import { resolvePlanAccess } from "../../plans/plan-location.ts";
11
+
12
+ export const planCheckHook: PreToolUseHook = {
13
+ metadata: {
14
+ name: "plan-check",
15
+ priority: 90,
16
+ phase: HookPhase.EARLY,
17
+ dependencies: [],
18
+ errorHandling: "propagate",
19
+ },
20
+
21
+ async execute(context: HookContext) {
22
+ const planFile = resolvePlanAccess(context.directory)?.path ?? null;
23
+
24
+ if (!planFile) {
25
+ return {
26
+ result: HookResult.INJECT,
27
+ modifiedContext: {
28
+ _planCheck: "missing",
29
+ _planCheckInstruction:
30
+ "No plan file found. Create a plan first before proceeding with any sub-agent tasks.",
31
+ },
32
+ };
33
+ }
34
+
35
+ return {
36
+ result: HookResult.CONTINUE,
37
+ modifiedContext: {
38
+ _planCheck: "found",
39
+ _planFilePath: planFile,
40
+ },
41
+ };
42
+ },
43
+ };