openhermes 4.9.2 → 4.11.2

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 (69) hide show
  1. package/CONTEXT.md +1 -1
  2. package/README.md +32 -31
  3. package/bootstrap.ts +262 -45
  4. package/harness/agents/oh-planner.md +1 -1
  5. package/harness/agents/openhermes.md +27 -126
  6. package/harness/codex/AUTOPILOT.md +99 -3
  7. package/harness/codex/CHARTER.md +3 -4
  8. package/harness/lib/background/background.test.ts +197 -0
  9. package/harness/lib/background/index.ts +7 -0
  10. package/harness/lib/background/interfaces.ts +31 -0
  11. package/harness/lib/background/manager.ts +320 -0
  12. package/harness/lib/composer/compose.test.ts +168 -0
  13. package/harness/lib/composer/compose.ts +65 -0
  14. package/harness/lib/composer/fragments/01-identity.md +1 -0
  15. package/harness/lib/composer/fragments/02-delegation.md +6 -0
  16. package/harness/lib/composer/fragments/03-permissions.md +13 -0
  17. package/harness/lib/composer/fragments/04-task-flow.md +15 -0
  18. package/harness/lib/composer/fragments/05-confidence.md +5 -0
  19. package/harness/lib/composer/fragments/06-parallelization.md +17 -0
  20. package/harness/lib/composer/fragments/07-shell.md +41 -0
  21. package/harness/lib/composer/fragments/08-routing.md +8 -0
  22. package/harness/lib/composer/fragments/09-guardrails.md +12 -0
  23. package/harness/lib/composer/index.ts +1 -0
  24. package/harness/lib/hooks/builtins/confidence-gate-hook.ts +70 -0
  25. package/harness/lib/hooks/builtins/delegation-depth-hook.ts +59 -0
  26. package/harness/lib/hooks/builtins/error-recovery-hook.ts +107 -0
  27. package/harness/lib/hooks/builtins/memory-sync-hook.ts +73 -0
  28. package/harness/lib/hooks/builtins/plan-check-hook.ts +43 -0
  29. package/harness/lib/hooks/builtins/route-tracking-hook.ts +147 -0
  30. package/harness/lib/hooks/builtins/sanity-check-hook.ts +52 -0
  31. package/harness/lib/hooks/builtins/shell-detect-hook.ts +96 -0
  32. package/harness/lib/hooks/hooks.test.ts +1016 -0
  33. package/harness/lib/hooks/index.ts +30 -0
  34. package/harness/lib/hooks/registry.ts +416 -0
  35. package/harness/lib/hooks/types.ts +71 -0
  36. package/harness/lib/memory/index.ts +18 -0
  37. package/harness/lib/memory/interfaces.ts +53 -0
  38. package/harness/lib/memory/memory-manager.ts +205 -0
  39. package/harness/lib/memory/memory.test.ts +491 -0
  40. package/harness/lib/memory/plan-store.ts +366 -0
  41. package/harness/lib/recovery/handler.ts +243 -0
  42. package/harness/lib/recovery/index.ts +14 -0
  43. package/harness/lib/recovery/interfaces.ts +48 -0
  44. package/harness/lib/recovery/patterns.ts +149 -0
  45. package/harness/lib/recovery/recovery.test.ts +312 -0
  46. package/harness/lib/sanity/anomaly-tracker.ts +127 -0
  47. package/harness/lib/sanity/checker.ts +178 -0
  48. package/harness/lib/sanity/index.ts +13 -0
  49. package/harness/lib/sanity/interfaces.ts +24 -0
  50. package/harness/lib/sanity/sanity.test.ts +472 -0
  51. package/harness/lib/sync/file-watcher.ts +174 -0
  52. package/harness/lib/sync/index.ts +11 -0
  53. package/harness/lib/sync/interfaces.ts +27 -0
  54. package/harness/lib/sync/plan-sync.ts +536 -0
  55. package/harness/lib/sync/sync.test.ts +832 -0
  56. package/harness/skills/oh-init/DEEP.md +2 -2
  57. package/harness/skills/oh-manifest/SKILL.md +1 -1
  58. package/harness/skills/oh-plan-review/DEEP.md +1 -1
  59. package/harness/skills/oh-planner/DEEP.md +3 -3
  60. package/harness/skills/oh-ship/SKILL.md +1 -1
  61. package/harness/skills/oh-skill-craft/SKILL.md +1 -4
  62. package/package.json +5 -5
  63. package/tsconfig.json +1 -1
  64. package/harness/commands/oh-doctor.md +0 -205
  65. package/harness/commands/oh-log.md +0 -18
  66. package/harness/skills/oh-learn/DEEP.md +0 -44
  67. package/harness/skills/oh-learn/SKILL.md +0 -30
  68. package/scripts/count-tokens.mjs +0 -158
  69. package/scripts/oh-doctor.ps1 +0 -342
package/CONTEXT.md CHANGED
@@ -24,4 +24,4 @@
24
24
  - OpenHermes is the default primary Agent.
25
25
 
26
26
  ## Flagged Ambiguities
27
- - Durable state is deferred for now and not a domain term for this pass.
27
+ - Durable state resolved 4-Tier Memory subsystem (System/Project/Mission/Task) now provides structured persistence with importance-driven retention.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  </p>
6
6
 
7
7
  <p align="center">
8
- <a href="https://www.npmjs.com/package/openhermes"><img src="https://img.shields.io/npm/v/openhermes?style=for-the-badge&label=version&color=FFD700" alt="npm version"></a>
8
+ <a href="https://www.npmjs.com/package/openhermes"><img src="https://img.shields.io/npm/v/openhermes?style=for-the-badge&label=version&color=FFD700" alt="v4.11.1"></a>
9
9
  <a href="https://github.com/nathwn12/openhermes/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="License: MIT"></a>
10
10
  <a href="https://opencode.ai"><img src="https://img.shields.io/badge/runs%20on-OpenCode-6366f1?style=for-the-badge" alt="Runs on OpenCode"></a>
11
11
  <a href="https://github.com/nathwn12/openhermes"><img src="https://img.shields.io/badge/⭐%20star%20on-GitHub-181717?style=for-the-badge" alt="Star on GitHub"></a>
@@ -31,27 +31,20 @@ To install from `dev` (latest features, may be unstable):
31
31
 
32
32
  ---
33
33
 
34
- ## One sentence. Nine steps.
34
+ ## One sentence. One engine.
35
35
 
36
- Add the plugin. Restart. Type:
36
+ OpenHermes v4.11 ships with a hardened internal architecture — 8 subsystems working together to make every session faster, more reliable, and fully autonomous:
37
37
 
38
- > *"Plan a CLI tool for managing dotfiles."*
39
-
40
- You see output. Behind the scenes, this runs:
41
-
42
- | # | What fires | What it does |
43
- |---|---|---|
44
- | **1** | `AUTOPILOT.md` decision matrix | Multi-step, vague `PLANNING NEEDED` |
45
- | **2** | `oh-planner` | Brainstorm mode: architecture, user flow, risks |
46
- | **3** | `oh-planner` `oh-grill` | Plan passes stress-test it |
47
- | **4** | `oh-grill` `oh-planner` (revise) | Gaps found planner revises |
48
- | **5** | `oh-planner` → `oh-manifest` | Plan solid → enter build loop |
49
- | **6** | `oh-planner` → `oh-builder` → `oh-gauntlet` | Implement → test → review → loop |
50
- | **7** | `oh-gauntlet` → `oh-ship` | Tests pass → PR pipeline |
51
- | **8** | `oh-ship` → `oh-retro` | Shipped → retrospective |
52
- | **9** | `oh-retro` → `oh-planner` | Ready for the next cycle |
53
-
54
- One sentence. Nine automated steps. Each skill loaded on demand, executed in isolation, routed to the next specialist. **Auto-classify, delegate, route, repeat.** That's the entire model.
38
+ | Subsystem | What it does |
39
+ |-----------|-------------|
40
+ | **Prompt Composer** | 9 modular fragments joined at runtime → byte-identical. Add a fragment, never edit the composition code. |
41
+ | **Auto-Recovery** | 9 error categories with typed actions — retry with backoff, compact on overflow, escalate on unknowns. Self-healing. |
42
+ | **4-Tier Memory** | System Project Mission Task. Importance-scored, budget-enforced, plan-file-persisted. Context that survives hops. |
43
+ | **Hook Registry** | Pluggable pre-tool, post-tool, route, and session hooks with topological sort. 8 built-in hooks, zero routing boilerplate. |
44
+ | **MVCC Sync** | Atomic writes, version counters, conflict detection. Multiple sub-agents writing the same plan file — no data loss. |
45
+ | **Sanity Checker** | 8 output degeneration detectors — repetition, gibberish, low diversity — with automatic escalation and recovery injection. |
46
+ | **Background Cmd** | Fire-and-forget process spawning with timeout, status polling, and auto-cleanup. Non-blocking long-running tasks. |
47
+ | **Test Harness** | Disposable temp dirs (Symbol.asyncDispose), factory builders, restore-capable mocks. Professional-grade test utilities. |
55
48
 
56
49
  ---
57
50
 
@@ -78,15 +71,15 @@ The loop runs unsupervised because these never turn off:
78
71
  | Capability | Why it matters |
79
72
  |---|---|
80
73
  | **Self-driving loop** | Type once. OpenHermes classifies, delegates, and routes — no pauses, no asking permission, no verbosity. |
81
- | **31 specialist skills** | Planning → building → testing → browser → security → review → shipping → retro. Every dev cycle phase. |
74
+ | **30 specialist skills** | Planning → building → testing → browser → security → review → shipping → retro. Every dev cycle phase. |
82
75
  | **Auto-detected user skills** | Drop a skill in `~/.agents/skills/`. OpenHermes finds it. Same name as a built-in? Your version wins. Survives `npm update`. |
83
- | **`/oh-doctor`** | Verify plugin load, skill discovery, command registration, config safety. |
84
- | **`/oh-log`** | Session log — routing hops, skill loads, compaction events. |
85
76
  | **Shared operating model** | CHARTER + AUTOPILOT + CONTEXT + ETHOS injected every session. Every interaction grounded in the same rules. |
86
77
  | **CORE/DEEP skill format** | Every skill is a two-file system: CORE (SKILL.md) handles 80% of passes in one read. DEEP.md loads on demand for hard cases. |
87
- | **Plan file storage** | `~/.local/share/opencode/openhermes/plans/`. Survives `npm update`. |
78
+ | **Plan file storage** | `~/.local/share/openhermes/plans/`. Survives `npm update`. |
79
+ | **8 internal subsystems** | Compositor, hooks, memory, recovery, sync, sanity checks, background commands, test harness — all native Node.js / TypeScript. |
80
+ | **Zero npm dependency additions** | All new subsystems use native Node.js and TypeScript only. No new packages. |
88
81
 
89
- ## 31 skills — three tiers
82
+ ## 30 skills — three tiers
90
83
 
91
84
  ### Tier 4 — Pipeline orchestrators
92
85
  Full multi-phase workflows:
@@ -132,7 +125,6 @@ Single-purpose, one thing well:
132
125
  | **oh-issue** | Break a plan/spec/PRD into independently-grabbable issues |
133
126
  | **oh-prd** | Conversation → PRD → GitHub issue |
134
127
  | **oh-freeze** | Restrict file edits to a specific directory |
135
- | **oh-learn** | Extract, evolve, promote session learnings as instincts |
136
128
  | **oh-guard** | Safety confirmation — warn before destructive operations |
137
129
  | **oh-skills-link** | Verify OpenCode discovers the skill directory |
138
130
  | **oh-skills-list** | List all available `oh-*` skills |
@@ -148,17 +140,26 @@ openhermes-pkg/
148
140
  ├── ETHOS.md # Operating principles
149
141
  ├── bootstrap.ts # Plugin entry — registers everything
150
142
  ├── index.ts # Package entrypoint
151
- ├── lib/ # harness-resolver.ts
152
143
  ├── harness/
153
144
  │ ├── agents/ # Agent manifests (OpenHermes primary)
154
145
  │ ├── codex/ # CHARTER, AUTOPILOT
155
- │ ├── commands/ # Slash commands (/oh-doctor, /oh-log)
146
+ │ ├── commands/ # Slash commands
156
147
  │ ├── instructions/ # SHELL.md
157
- └── skills/ # 31 skill SKILL.md files (CORE/DEEP format)
148
+ ├── lib/ # Internal subsystems
149
+ │ │ ├── composer/ # Prompt fragment composition
150
+ │ │ ├── recovery/ # Auto-recovery with error patterns
151
+ │ │ ├── memory/ # 4-tier hierarchical memory
152
+ │ │ ├── sync/ # MVCC plan synchronization
153
+ │ │ ├── hooks/ # Pluggable hook registry
154
+ │ │ ├── sanity/ # Output degeneration detection
155
+ │ │ └── background/ # Fire-and-forget command system
156
+ │ └── skills/ # 30 skill SKILL.md files (CORE/DEEP format)
157
+ ├── lib/ # harness-resolver.ts
158
158
  └── test/
159
+ └── harness/ # Test utilities (fixture, builders, mocks)
159
160
  ```
160
161
 
161
- Plan files: `~/.local/share/opencode/openhermes/plans/<project>-plan-<nnn>.md`
162
+ Plan files: `~/.local/share/openhermes/plans/<project>/plan-<nnn>.md`
162
163
 
163
164
  ---
164
165
 
@@ -166,7 +167,7 @@ Plan files: `~/.local/share/opencode/openhermes/plans/<project>-plan-<nnn>.md`
166
167
 
167
168
  1. Add the plugin line to `opencode.json`
168
169
  2. Restart or reload OpenCode
169
- 3. Run `/oh-doctor` to verify everything loaded
170
+ 3. Verify the plugin loaded in the startup log
170
171
  4. Type *any* prompt — "plan a feature", "investigate this bug", "refactor this module"
171
172
 
172
173
  The first time you see OpenHermes auto-route to a specialist skill without you asking — you'll feel the loop.
package/bootstrap.ts CHANGED
@@ -3,6 +3,22 @@ import fs from "node:fs"
3
3
  import os from "node:os"
4
4
  import type { Plugin } from "@opencode-ai/plugin"
5
5
  import { getHarnessDir, setHarnessRootForTest, resolveHarnessRoot } from "./lib/harness-resolver.ts"
6
+ import { compose } from "./harness/lib/composer/index.ts"
7
+
8
+ // Hook system — pluggable lifecycle hooks with topological sort
9
+ import {
10
+ HookRegistry,
11
+ HookResult,
12
+ planCheckHook,
13
+ shellDetectHook,
14
+ confidenceGateHook,
15
+ delegationDepthHook,
16
+ resetDepthTracker,
17
+ errorRecoveryHook,
18
+ memorySyncHook,
19
+ sanityCheckHook,
20
+ routeTrackingHook,
21
+ } from "./harness/lib/hooks/index.ts"
6
22
 
7
23
  const OPENHERMES_AGENT = "OpenHermes"
8
24
 
@@ -17,15 +33,14 @@ const USER_SKILL_DIRS: ReadonlyArray<string> = [
17
33
  let _planStorageOverride: string | undefined
18
34
  export function setPlanStorageDirForTest(dir: string | undefined): void { _planStorageOverride = dir }
19
35
  function planStorageDir(): string {
20
- return _planStorageOverride ?? path.join(os.homedir(), ".local", "share", "opencode", "openhermes", "plans")
36
+ return _planStorageOverride ?? path.join(os.homedir(), ".local", "share", "openhermes", "plans")
21
37
  }
22
38
 
23
39
  function getProjectName(projectDir: string): string {
24
40
  return path.basename(projectDir)
25
41
  }
26
42
 
27
-
28
- export { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir, ensurePlanFile }
43
+ export { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir, ensurePlanFile, findLatestPlanFile }
29
44
 
30
45
  function parseFrontmatter(raw: string | undefined): Record<string, string> {
31
46
  const frontmatter: Record<string, string> = {}
@@ -125,25 +140,21 @@ function uniqueStrings(existing: string[] = [], additions: string[] = []): strin
125
140
  }
126
141
 
127
142
 
128
- function regexEscape(s: string): string {
129
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
130
- }
131
-
132
143
  function findLatestPlanFile(projectDir: string): string | null {
133
144
  const projectName = getProjectName(projectDir)
134
145
  const storage = planStorageDir()
135
- if (!fs.existsSync(storage)) return null
136
- const pattern = new RegExp(`^${regexEscape(projectName)}-plan-(\\d{3})\\.md$`)
146
+ const projectDirPath = path.join(storage, projectName)
147
+ if (!fs.existsSync(projectDirPath)) return null
137
148
  let latest: string | null = null
138
149
  let highest = -1
139
150
  try {
140
- for (const entry of fs.readdirSync(storage)) {
141
- const m = entry.match(pattern)
151
+ for (const entry of fs.readdirSync(projectDirPath)) {
152
+ const m = entry.match(/^plan-(\d{3})\.md$/)
142
153
  if (m) {
143
154
  const n = parseInt(m[1], 10)
144
155
  if (n > highest) {
145
156
  highest = n
146
- latest = path.join(storage, entry)
157
+ latest = path.join(projectDirPath, entry)
147
158
  }
148
159
  }
149
160
  }
@@ -170,8 +181,14 @@ function readPlanSummary(projectDir: string): string | null {
170
181
  }
171
182
 
172
183
  function ensureDir(dir: string): void {
173
- if (!fs.existsSync(dir)) {
174
- fs.mkdirSync(dir, { recursive: true })
184
+ try {
185
+ if (!fs.existsSync(dir)) {
186
+ fs.mkdirSync(dir, { recursive: true })
187
+ }
188
+ } catch (err) {
189
+ const msg = err instanceof Error ? err.message : String(err)
190
+ console.error(`[openhermes] Failed to create directory ${dir}: ${msg}`)
191
+ // Don't throw — let the plan system degrade gracefully
175
192
  }
176
193
  }
177
194
 
@@ -184,7 +201,8 @@ function ensureDir(dir: string): void {
184
201
  function ensurePlanFile(projectDir: string): string {
185
202
  const projectName = getProjectName(projectDir)
186
203
  const storage = planStorageDir()
187
- ensureDir(storage)
204
+ const projectDirPath = path.join(storage, projectName)
205
+ ensureDir(projectDirPath)
188
206
 
189
207
  // Reuse active or in-progress plan
190
208
  const latest = findLatestPlanFile(projectDir)
@@ -199,12 +217,13 @@ function ensurePlanFile(projectDir: string): string {
199
217
  // Determine next sequence number
200
218
  let nextSeq = 1
201
219
  if (latest) {
202
- const m = path.basename(latest).match(/-plan-(\d{3})\.md$/)
220
+ const m = path.basename(latest).match(/^plan-(\d{3})\.md$/)
203
221
  if (m) nextSeq = parseInt(m[1], 10) + 1
204
222
  }
205
223
 
206
- const planId = `${projectName}-plan-${String(nextSeq).padStart(3, "0")}`
207
- const planPath = path.join(storage, `${planId}.md`)
224
+ const seq = String(nextSeq).padStart(3, "0")
225
+ const planId = `${projectName}/plan-${seq}.md`
226
+ const planPath = path.join(projectDirPath, `plan-${seq}.md`)
208
227
  const now = new Date().toISOString().replace("T", " ").slice(0, 16)
209
228
 
210
229
  const content = [
@@ -225,7 +244,13 @@ function ensurePlanFile(projectDir: string): string {
225
244
  "",
226
245
  ].join("\n")
227
246
 
228
- fs.writeFileSync(planPath, content, "utf8")
247
+ try {
248
+ fs.writeFileSync(planPath, content, "utf8")
249
+ } catch (err) {
250
+ const msg = err instanceof Error ? err.message : String(err)
251
+ console.error(`[openhermes] Failed to write plan file ${planPath}: ${msg}`)
252
+ // Don't throw — let the plan system degrade gracefully
253
+ }
229
254
  return planPath
230
255
  }
231
256
 
@@ -277,6 +302,7 @@ interface OpenHermesConfig {
277
302
  agent?: Record<string, unknown>
278
303
  instructions?: string[]
279
304
  default_agent?: string
305
+ [key: string]: unknown // allow additional SDK properties (experimental, etc.)
280
306
  }
281
307
 
282
308
  export const BootstrapPlugin: Plugin = async (ctx) => {
@@ -298,17 +324,43 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
298
324
  // Auto-detect and wire user skills from ~/.agents/skills and ~/.config/opencode/skills
299
325
  const userSkillPaths: string[] = []
300
326
  for (const userDir of USER_SKILL_DIRS) {
301
- ensureDir(userDir)
327
+ try { ensureDir(userDir) } catch {}
302
328
  userSkillPaths.push(userDir)
303
329
  await logToOC("info", `wired user skills from ${userDir}`)
304
330
  }
305
331
 
306
332
  const compactionContext = buildCompactionContext(ctx.directory)
307
333
  // Ensure plan storage exists
308
- ensureDir(planStorageDir())
334
+ try { ensureDir(planStorageDir()) } catch {}
309
335
 
310
336
  return {
311
337
  config: async (config: OpenHermesConfig) => {
338
+ // ── 1. Hooks System ─────────────────────────────────────────────────
339
+ // Read experimental.hooks config from the raw config object
340
+ const hooksConfig = (config.experimental as Record<string, unknown> | undefined)?.hooks as
341
+ | Record<string, boolean>
342
+ | undefined
343
+ const hooksEnabled = (hooksConfig?.enabled ?? true) as boolean
344
+
345
+ if (hooksEnabled) {
346
+ const reg = HookRegistry.getInstance()
347
+
348
+ // Check individual hook flags (default: true if not specified)
349
+ if (hooksConfig?.plan_check ?? true) reg.registerPreTool(planCheckHook)
350
+ if (hooksConfig?.shell_detect ?? true) reg.registerPreTool(shellDetectHook)
351
+ if (hooksConfig?.delegation_depth ?? true) reg.registerPreTool(delegationDepthHook)
352
+ if (hooksConfig?.confidence_gate ?? true) reg.registerRoute(confidenceGateHook)
353
+ if (hooksConfig?.error_recovery ?? true) reg.registerPostTool(errorRecoveryHook)
354
+ if (hooksConfig?.memory_sync ?? true) reg.registerPostTool(memorySyncHook)
355
+ if (hooksConfig?.sanity_check ?? true) reg.registerPostTool(sanityCheckHook)
356
+ if (hooksConfig?.route_tracking ?? true) reg.registerRoute(routeTrackingHook)
357
+
358
+ await logToOC("info", `hooks: ${reg.getPreToolHooks().length + reg.getPostToolHooks().length + reg.getRouteHooks().length} registered`)
359
+ } else {
360
+ await logToOC("info", "hooks: disabled via config")
361
+ }
362
+
363
+ // ── 2. Skills ──────────────────────────────────────────────────────
312
364
  config.skills = config.skills || {}
313
365
  // Built-in paths first, user paths last → user skills override built-in on name conflict
314
366
  const allPaths = [skillsDir, ...userSkillPaths]
@@ -325,10 +377,17 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
325
377
  config.command = { ...(config.command ?? {}), ...commandDefinitions(commandsDir) }
326
378
 
327
379
  const loadedAgents = agentDefinitions(agentsDir)
328
- const openHermesAgent = loadedAgents[OPENHERMES_AGENT] ?? {
329
- description: "OpenHermes primary orchestrator",
330
- mode: "primary",
331
- prompt: "You are OpenHermes.",
380
+ // Use composer for the OpenHermes agent prompt — assemble from fragments
381
+ let openHermesPrompt: string
382
+ try {
383
+ openHermesPrompt = compose()
384
+ } catch {
385
+ openHermesPrompt = loadedAgents[OPENHERMES_AGENT]?.prompt ?? "You are OpenHermes."
386
+ }
387
+ const openHermesAgent = {
388
+ description: loadedAgents[OPENHERMES_AGENT]?.description ?? "OpenHermes primary orchestrator",
389
+ mode: loadedAgents[OPENHERMES_AGENT]?.mode ?? "primary",
390
+ prompt: openHermesPrompt,
332
391
  }
333
392
 
334
393
  // Subagent permissions — tier-4 and tier-3 get execution access but cannot spawn orchestrators
@@ -336,18 +395,18 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
336
395
  "oh-builder": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
337
396
  "oh-browser": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
338
397
  "oh-facade": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
398
+ "oh-fusion": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
339
399
  "oh-gauntlet": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
340
- "oh-manifest": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
341
- "oh-ship": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
342
- "oh-planner": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
343
400
  "oh-grill": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
344
401
  "oh-investigate": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
402
+ "oh-manifest": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
345
403
  "oh-plan-review": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
346
- "oh-security": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
404
+ "oh-planner": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
347
405
  "oh-refactor": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
348
- "oh-review": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
349
- "oh-fusion": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
350
406
  "oh-retro": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
407
+ "oh-review": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
408
+ "oh-security": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
409
+ "oh-ship": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
351
410
  "oh-skill-craft": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
352
411
  }
353
412
 
@@ -407,7 +466,7 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
407
466
 
408
467
  // Reset delegation depth on session start/error
409
468
  if (typed.type === "session.created" || typed.type === "session.error") {
410
- delegationDepths.delete(`delegation:${ctx.directory}`)
469
+ resetDepthTracker()
411
470
  }
412
471
  },
413
472
 
@@ -415,22 +474,182 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
415
474
  output.context.push(...compactionContext)
416
475
  },
417
476
 
418
- // Mechanical delegation loop guard prevents runaway agent nesting
477
+ // Hook-enabled tool executiondelegates to HookRegistry for lifecycle hooks
419
478
  "tool.execute.before": async (input, output) => {
420
479
  if (input.tool === "task") {
421
- // Track delegation depth per project (one session per project at a time)
422
- const depthKey = `delegation:${ctx.directory}`
423
- const currentDepth = (delegationDepths.get(depthKey) ?? 0) + 1
424
- delegationDepths.set(depthKey, currentDepth)
480
+ const reg = HookRegistry.getInstance()
481
+
482
+ // Access optional fields from input (SDK may include these at runtime)
483
+ const inputAny = input as Record<string, unknown>
484
+ const agentName = typeof inputAny.agent === "string" ? inputAny.agent : "unknown"
485
+
486
+ // Build hook context from input and current session state
487
+ const hookContext = {
488
+ sessionId: ctx.directory, // project directory as session key
489
+ agent: agentName,
490
+ directory: ctx.directory,
491
+ sessions: new Map(),
492
+ _confidenceLevel: typeof inputAny.confidence === "string" ? inputAny.confidence : undefined,
493
+ _confidenceExchanges: 0,
494
+ _maxDelegationDepth: 25,
495
+ _routeTrackingConfig: {
496
+ maxSkillRepeats: 5,
497
+ maxUnproductiveHops: 30, // higher than max delegation depth (25) so depth guard fires first
498
+ },
499
+ }
500
+
501
+ // Run all registered PreToolUse hooks (plan check, shell detect, delegation depth)
502
+ let preToolResult: any
503
+ try {
504
+ preToolResult = await reg.executePreTool(hookContext)
505
+ } catch (err) {
506
+ const msg = err instanceof Error ? err.message : String(err)
507
+ const errOutput = output as { args: unknown; isError?: boolean; content?: unknown[] }
508
+ errOutput.isError = true
509
+ errOutput.content = [{ type: "text", text: `Hook error (PreTool): ${msg}` }]
510
+ return
511
+ }
425
512
 
426
- if (currentDepth >= 10) {
513
+ if (preToolResult.result === HookResult.STOP) {
514
+ // Depth exceeded or other stop condition
427
515
  const errOutput = output as { args: unknown; isError?: boolean; content?: unknown[] }
428
516
  errOutput.isError = true
429
- errOutput.content = [{
430
- type: "text",
431
- text: "LOOP GUARD: Delegation depth exceeded (max 10). " +
432
- "Surface to orchestrator with findings and stop delegating."
433
- }]
517
+ const message = (preToolResult.modifiedContext?._depthError as string)
518
+ ?? "LOOP GUARD: Delegation depth exceeded (max 25). " +
519
+ "Surface to orchestrator with findings and stop delegating."
520
+ errOutput.content = [{ type: "text", text: message }]
521
+ return
522
+ }
523
+
524
+ // Handle INJECT from PreTool hooks (e.g. plan check wants a plan first)
525
+ if (preToolResult.result === HookResult.INJECT) {
526
+ const planInstruction = preToolResult.modifiedContext?._planCheckInstruction as string | undefined
527
+ if (planInstruction) {
528
+ // Plan check hook returned INJECT — inject "create plan" instruction
529
+ const inputAny = input as Record<string, unknown>
530
+ const existingPrompt = (inputAny.description as string) || (inputAny.prompt as string) || ""
531
+ if (inputAny.description) {
532
+ inputAny.description = `${planInstruction}\n\n${existingPrompt}`
533
+ } else if (inputAny.prompt) {
534
+ inputAny.prompt = `${planInstruction}\n\n${existingPrompt}`
535
+ } else {
536
+ inputAny.description = planInstruction
537
+ }
538
+ await logToOC("info", `Plan check: injected plan creation instruction into task for "${agentName}"`)
539
+ } else {
540
+ // Generic INJECT — hooks modified task context
541
+ await logToOC("debug", `PreTool INJECT: hooks modified task context for "${agentName}"`)
542
+ }
543
+ // Continue execution — INJECT is not a stop signal
544
+ }
545
+
546
+ // Run all registered RouteHooks — the agent/skill being delegated to IS the route
547
+ // This fires confidence-gate (inject confirm/question on MEDIUM/LOW confidence)
548
+ // and route-tracking (guard against infinite routing loops)
549
+ let routeResult: any
550
+ try {
551
+ routeResult = await reg.executeRoute(hookContext, agentName)
552
+ } catch (err) {
553
+ const msg = err instanceof Error ? err.message : String(err)
554
+ await logToOC("error", `Hook error (Route): ${msg}`)
555
+ // Route failed — don't change routing decision, just log
556
+ routeResult = { result: HookResult.CONTINUE }
557
+ }
558
+
559
+ if (routeResult.result === HookResult.STOP) {
560
+ // Loop guard triggered by route-tracking hook
561
+ const errOutput = output as { args: unknown; isError?: boolean; content?: unknown[] }
562
+ errOutput.isError = true
563
+ const ctxAny = hookContext as Record<string, unknown>
564
+ const optiReport = ctxAny._optiRoute
565
+ ? JSON.stringify(ctxAny._optiRoute, null, 2)
566
+ : "Route guard: Excessive or unproductive routing detected."
567
+ errOutput.content = [{ type: "text", text: `ROUTE GUARD: ${optiReport}\n\nSurface to orchestrator with findings and stop delegating.` }]
568
+ }
569
+
570
+ if (routeResult.result === HookResult.INJECT && routeResult.modifiedRoute) {
571
+ // Confidence gate wants to inject a confirmation/pause into routing.
572
+ // Parse the modifiedRoute for markers and inject into task description/prompt.
573
+ const modifiedRoute: string = routeResult.modifiedRoute
574
+ const inputAny = input as Record<string, unknown>
575
+ const existingPrompt = (inputAny.description as string) || (inputAny.prompt as string) || ""
576
+ let gateMsg: string | undefined
577
+
578
+ if (modifiedRoute.includes("?echo=confirm")) {
579
+ gateMsg = "[CONFIDENCE: MEDIUM] Review your plan and confirm it before executing."
580
+ } else if (modifiedRoute.includes("?question=pause")) {
581
+ gateMsg = "[CONFIDENCE: LOW] Pause and ask the user for approval before proceeding."
582
+ }
583
+
584
+ if (gateMsg) {
585
+ if (inputAny.description) {
586
+ inputAny.description = `${gateMsg}\n${existingPrompt}`
587
+ } else if (inputAny.prompt) {
588
+ inputAny.prompt = `${gateMsg}\n${existingPrompt}`
589
+ } else {
590
+ inputAny.description = gateMsg
591
+ }
592
+ await logToOC("info", `Confidence gate: injected instruction into task to "${agentName}": ${gateMsg}`)
593
+ }
594
+ }
595
+ }
596
+ },
597
+
598
+ // Hook-enabled post-execution — runs PostToolUse hooks
599
+ "tool.execute.after": async (input, output) => {
600
+ if (input.tool === "task") {
601
+ const reg = HookRegistry.getInstance()
602
+
603
+ // Access optional fields from input
604
+ const inputAny = input as Record<string, unknown>
605
+ const agentName = typeof inputAny.agent === "string" ? inputAny.agent : "unknown"
606
+
607
+ // Build hook context from input and current session state
608
+ const hookContext = {
609
+ sessionId: ctx.directory,
610
+ agent: agentName,
611
+ directory: ctx.directory,
612
+ sessions: new Map(),
613
+ _confidenceLevel: typeof inputAny.confidence === "string" ? inputAny.confidence : undefined,
614
+ _confidenceExchanges: 0,
615
+ _maxDelegationDepth: 25,
616
+ }
617
+
618
+ // Extract output text from tool result
619
+ // output.content may be array of content blocks, or a string, or undefined
620
+ const outputAny = output as Record<string, unknown> | undefined
621
+ let outputText = ""
622
+ if (outputAny?.content) {
623
+ const content = outputAny.content
624
+ if (typeof content === "string") {
625
+ outputText = content
626
+ } else if (Array.isArray(content)) {
627
+ outputText = content
628
+ .map((c: Record<string, unknown>) => (typeof c.text === "string" ? c.text : ""))
629
+ .join("\n")
630
+ }
631
+ }
632
+
633
+ // Run all registered PostToolUse hooks
634
+ try {
635
+ const postToolResult = await reg.executePostTool(hookContext, outputText)
636
+
637
+ // Surface recovery instructions from errorRecoveryHook and/or sanityCheckHook
638
+ if (postToolResult.recovery) {
639
+ await logToOC("warn", `PostTool recovery instruction:\n${postToolResult.recovery}`)
640
+ }
641
+
642
+ // Log when hooks signal issues (INJECT = anomaly/error detected by a hook)
643
+ if (postToolResult.result === HookResult.INJECT) {
644
+ await logToOC("warn", "PostTool INJECT: hooks detected issues in tool output")
645
+ }
646
+
647
+ // memorySyncHook catches its own errors (best-effort sync),
648
+ // so memory sync failures are already handled gracefully inside the hook
649
+ } catch (err) {
650
+ const msg = err instanceof Error ? err.message : String(err)
651
+ await logToOC("error", `Hook error (PostTool): ${msg}`)
652
+ // Non-fatal — tool already executed
434
653
  }
435
654
  }
436
655
  },
@@ -438,5 +657,3 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
438
657
  }
439
658
  }
440
659
 
441
- // Module-level delegation depth tracker — reset per project session
442
- const delegationDepths = new Map<string, number>()
@@ -26,7 +26,7 @@ Always know before you go.
26
26
 
27
27
  # oh-planner
28
28
 
29
- ALL-arounder planner. Merges brainstorm, architecture analysis, strategy, and plan review into one skill. Produces plan files in canonical storage (`~/.local/share/opencode/openhermes/plans/`).
29
+ ALL-arounder planner. Merges brainstorm, architecture analysis, strategy, and plan review into one skill. Produces plan files in canonical storage (`~/.local/share/openhermes/plans/`).
30
30
 
31
31
  Load the relevant section based on entry mode:
32
32