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
package/CONTEXT.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # OpenHermes — Shared Language
2
2
 
3
3
  ## Terms
4
- **OpenHermes** — OpenCode-native orchestration layer for this package.
5
- **Skill** — A `SKILL.md` loaded on demand through OpenCode's skill tool.
6
- **Command** — A slash command backed by package-local command markdown; legacy compatibility loaders remain only where runtime-backed.
7
- **Agent** — A primary or subagent definition loaded through OpenCode config.
8
- **Instruction** — Markdown loaded through `AGENTS.md` or `opencode.json` instructions.
9
- **Bootstrap** — The first-message context injected by the OpenHermes plugin.
4
+ **OpenHermes** — OpenCode-native orchestration layer for this package.
5
+ **Skill** — A `SKILL.md` loaded on demand through OpenCode's skill tool.
6
+ **Command** — A slash command backed by package-local command markdown; legacy compatibility loaders remain only where runtime-backed.
7
+ **Agent** — A primary or subagent definition loaded through OpenCode config.
8
+ **Instruction** — Markdown loaded through `AGENTS.md` or `opencode.json` instructions.
9
+ **Bootstrap** — The first-message context injected by the OpenHermes plugin.
10
10
 
11
11
  ### Confidence Gate Terms
12
12
  **Confidence Gate** — Phase 0.5 protocol in the autopilot loop that evaluates signal strength before routing. Bounded to 1 conversational exchange max.
package/ETHOS.md CHANGED
@@ -8,8 +8,8 @@ OpenCode-native loading over manual copying or hidden state.
8
8
  ## Small Surface
9
9
  Every file earns its keep. Prefer markdown when behavior is declarative.
10
10
 
11
- ## Skills Over Glue
12
- Behavior lives in `SKILL.md`, command markdown, and agent markdown. Legacy command-doc compatibility loaders remain supported only where runtime-backed.
11
+ ## Skills Over Glue
12
+ Behavior lives in `SKILL.md`, command markdown, and agent markdown. Legacy command-doc compatibility loaders remain supported only where runtime-backed.
13
13
 
14
14
  ## Always Delegate — Never Execute
15
15
  OpenHermes orchestrates and reports. Sub-agents execute. OpenHermes never writes code, runs tests, or touches files directly.
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="v4.11.3"></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="npm version"></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>
@@ -33,17 +33,12 @@ To install from `dev` (latest features, may be unstable):
33
33
 
34
34
  ## One sentence. One engine.
35
35
 
36
- OpenHermes v4.11 ships with a hardened internal architecture — 8 subsystems working together to make every session faster, more reliable, and fully autonomous:
36
+ OpenHermes ships with a focused internal architecture — 3 subsystems working together to make every session faster, more reliable, and fully autonomous:
37
37
 
38
38
  | Subsystem | What it does |
39
39
  |-----------|-------------|
40
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. |
41
+ | **Hook Registry** | Pluggable pre-tool, post-tool, route, and session hooks with priority-sort ordering. 7 built-in hooks, zero routing boilerplate. |
47
42
  | **Plan Location** | Resolves plan file paths per project with directory-per-project layout in `~/.local/share/openhermes/plans/`. |
48
43
 
49
44
  ---
@@ -72,11 +67,11 @@ The loop runs unsupervised because these never turn off:
72
67
  |---|---|
73
68
  | **Self-driving loop** | Type once. OpenHermes classifies, delegates, and routes — no pauses, no asking permission, no verbosity. |
74
69
  | **30 specialist skills** | Planning → building → testing → browser → security → review → shipping → retro. Every dev cycle phase. |
75
- | **Auto-detected user skills** | Drop a skill in `~/.agents/skills/` or `~/.config/opencode/skills/`. OpenHermes finds it. Same name as a built-in? Your version wins. Survives `npm update`. |
70
+ | **Auto-detected user skills** | Drop a skill in `~/.agents/skills/` or `~/.config/opencode/skills/`. OpenHermes finds it. Same name as a built-in? Your version wins. Survives `npm update`. |
76
71
  | **Shared operating model** | CHARTER + AUTOPILOT + CONTEXT + ETHOS injected every session. Every interaction grounded in the same rules. |
77
72
  | **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. |
78
73
  | **Plan file storage** | `~/.local/share/openhermes/plans/`. Survives `npm update`. |
79
- | **8 internal subsystems** | Composer, recovery, memory, sync, hooks, plans, sanity, background — all native Node.js / TypeScript. |
74
+ | **3 internal subsystems** | Composer, hooks, plans — all native Node.js / TypeScript. |
80
75
  | **Zero npm dependency additions** | All new subsystems use native Node.js and TypeScript only. No new packages. |
81
76
 
82
77
  ## 30 skills — four tiers
@@ -138,22 +133,21 @@ openhermes-pkg/
138
133
  ├── AGENTS.md # User-side routing overlay
139
134
  ├── CONTEXT.md # Shared domain language
140
135
  ├── ETHOS.md # Operating principles
136
+ ├── docs/
137
+ │ ├── HOW-IT-WORKS.md # Runtime data flow (routing, hooks, plans)
138
+ │ └── adr/ # Architecture Decision Records
141
139
  ├── bootstrap.ts # Plugin entry — registers everything
142
140
  ├── index.ts # Package entrypoint
143
141
  ├── harness/
144
142
  │ ├── agents/ # Agent manifests (OpenHermes primary)
145
143
  │ ├── codex/ # CHARTER, AUTOPILOT
146
- │ ├── instructions/ # SHELL.md
144
+ │ ├── instructions/ # SHELL.md
147
145
  │ ├── lib/ # Internal subsystems
148
146
  │ │ ├── composer/ # Prompt fragment composition
149
- │ │ ├── recovery/ # Auto-recovery with error patterns
150
- │ │ ├── memory/ # 4-tier hierarchical memory
151
- │ │ ├── sync/ # MVCC plan synchronization
152
147
  │ │ ├── hooks/ # Pluggable hook registry
153
148
  │ │ ├── plans/ # Plan file path resolution
154
- │ │ ├── sanity/ # Output degeneration detection
155
- └── background/ # Fire-and-forget command system
156
- │ └── skills/ # 30 skill SKILL.md files (CORE/DEEP format)
149
+ │ │ └── ... # guards/ (guard config)
150
+ │ └── skills/ # 30 skill SKILL.md files (CORE/DEEP format)
157
151
  ├── lib/ # harness-resolver.ts
158
152
  └── test/
159
153
  └── harness/ # Test utilities (fixture, builders, mocks)
package/bootstrap.ts CHANGED
@@ -1,32 +1,27 @@
1
- import path from "node:path"
2
- import fs from "node:fs"
3
- import os from "node:os"
4
- import type { Plugin } from "@opencode-ai/plugin"
5
- import { getHarnessDir, setHarnessRootForTest, resolveHarnessRoot } from "./lib/harness-resolver.ts"
6
- import { compose } from "./harness/lib/composer/index.ts"
7
- import { ensurePlanFile, findLatestPlanFile, planStorageDir, setPlanStorageDirForTest, resolvePlanAccess } from "./harness/lib/plans/plan-location.ts"
8
- import { clearRuntimeRouteDecision, consumeRouteGuidance, getRuntimeRouteDecision, rememberRuntimeRouteDecision } from "./harness/lib/routing/index.ts"
1
+ import path from "node:path"
2
+ import fs from "node:fs"
3
+ import os from "node:os"
4
+ import type { Plugin } from "@opencode-ai/plugin"
5
+ import { getHarnessDir, setHarnessRootForTest, resolveHarnessRoot } from "./lib/harness-resolver.ts"
6
+ import { compose } from "./harness/lib/composer/index.ts"
7
+ import { ensurePlanFile, findLatestPlanFile, planStorageDir, setPlanStorageDirForTest, resolvePlanAccess } from "./harness/lib/plans/plan-location.ts"
8
+ import { clearRuntimeRouteDecision, consumeRouteGuidance, getRuntimeRouteDecision, rememberRuntimeRouteDecision } from "./harness/lib/routing/index.ts"
9
9
 
10
10
  // Hook system — pluggable lifecycle hooks with topological sort
11
11
  import {
12
- HookRegistry,
13
- HookResult,
14
- nextRouteHook,
15
- planCheckHook,
12
+ HookRegistry,
13
+ HookResult,
14
+ nextRouteHook,
15
+ planCheckHook,
16
16
  shellDetectHook,
17
17
  confidenceGateHook,
18
18
  delegationDepthHook,
19
19
  resetDepthTracker,
20
- errorRecoveryHook,
21
- memorySyncHook,
22
- sanityCheckHook,
23
- dynamicRouteHook,
24
- routeTrackingHook,
25
- subagentFailureHook,
26
- resetSubagentFailures,
20
+ dynamicRouteHook,
21
+ routeTrackingHook,
27
22
  DEFAULT_GUARD_CONFIG,
28
23
  } from "./harness/lib/hooks/index.ts"
29
- import type { HookContext } from "./harness/lib/hooks/index.ts"
24
+ import type { HookContext } from "./harness/lib/hooks/index.ts"
30
25
 
31
26
  const OPENHERMES_AGENT = "OpenHermes"
32
27
 
@@ -37,7 +32,7 @@ const USER_SKILL_DIRS: ReadonlyArray<string> = [
37
32
  path.join(os.homedir(), ".claude", "skills"), // Claude Code backward compat
38
33
  ]
39
34
 
40
- export { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir, ensurePlanFile, findLatestPlanFile, setPlanStorageDirForTest }
35
+ export { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir, ensurePlanFile, findLatestPlanFile, setPlanStorageDirForTest }
41
36
 
42
37
  function parseFrontmatter(raw: string | undefined): Record<string, string> {
43
38
  const frontmatter: Record<string, string> = {}
@@ -52,12 +47,12 @@ function parseFrontmatter(raw: string | undefined): Record<string, string> {
52
47
  return frontmatter
53
48
  }
54
49
 
55
- interface MarkdownDocument {
56
- frontmatter: Record<string, string>
57
- body: string
58
- }
59
-
60
- function readMarkdownDocument(filePath: string): MarkdownDocument | null {
50
+ interface MarkdownDocument {
51
+ frontmatter: Record<string, string>
52
+ body: string
53
+ }
54
+
55
+ function readMarkdownDocument(filePath: string): MarkdownDocument | null {
61
56
  if (!fs.existsSync(filePath)) return null
62
57
  const source = fs.readFileSync(filePath, "utf8")
63
58
  const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/)
@@ -83,7 +78,7 @@ function readMarkdownDirectory(dir: string): DirEntry[] {
83
78
  .filter((e): e is DirEntry => e !== null)
84
79
  }
85
80
 
86
- interface CommandDef {
81
+ interface CommandDef {
87
82
  description: string
88
83
  template: string
89
84
  agent?: string
@@ -106,7 +101,7 @@ function commandDefinitions(dir: string): Record<string, CommandDef> {
106
101
  return commands
107
102
  }
108
103
 
109
- interface AgentDef {
104
+ interface AgentDef {
110
105
  description: string
111
106
  mode: string
112
107
  prompt: string
@@ -137,7 +132,7 @@ function uniqueStrings(existing: string[] = [], additions: string[] = []): strin
137
132
  }
138
133
 
139
134
 
140
- function ensureDir(dir: string): void {
135
+ function ensureDir(dir: string): void {
141
136
  try {
142
137
  if (!fs.existsSync(dir)) {
143
138
  fs.mkdirSync(dir, { recursive: true })
@@ -149,15 +144,15 @@ function ensureDir(dir: string): void {
149
144
  }
150
145
  }
151
146
 
152
- export function buildCompactionContext(projectDir: string): string[] {
147
+ export function buildCompactionContext(projectDir: string): string[] {
153
148
  const context = [
154
149
  "OpenHermes: native-first, verify before claim, always delegate, concise over verbose.",
155
150
  "Preserve domain terms: skill, command, agent, bootstrap, compaction.",
156
151
  "Preserve blockers, current task, and next steps; do not invent durable state.",
157
152
  ]
158
153
 
159
- const planSummary = resolvePlanAccess(projectDir)?.summary
160
- if (planSummary) context.push(planSummary)
154
+ const planSummary = resolvePlanAccess(projectDir)?.summary
155
+ if (planSummary) context.push(planSummary)
161
156
 
162
157
  return context
163
158
  }
@@ -232,7 +227,7 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
232
227
 
233
228
  return {
234
229
  config: async (config: OpenHermesConfig) => {
235
-
230
+
236
231
  // ── 1. Hooks System ─────────────────────────────────────────────────
237
232
  // Read experimental.hooks config from the raw config object
238
233
  const experimental = config.experimental as Record<string, unknown> | undefined;
@@ -244,23 +239,18 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
244
239
  if (hooksEnabled) {
245
240
  const reg = HookRegistry.getInstance()
246
241
 
247
- // Check individual hook flags (default: true if not specified)
248
- if (hooksConfig?.plan_check ?? true) reg.registerPreTool(planCheckHook)
249
- if (hooksConfig?.shell_detect ?? true) reg.registerPreTool(shellDetectHook)
250
- if (hooksConfig?.delegation_depth ?? true) reg.registerPreTool(delegationDepthHook)
251
- reg.registerRoute(nextRouteHook)
252
- if (hooksConfig?.confidence_gate ?? true) reg.registerRoute(confidenceGateHook)
253
- if (hooksConfig?.error_recovery ?? true) reg.registerPostTool(errorRecoveryHook)
254
- if (hooksConfig?.memory_sync ?? true) reg.registerPostTool(memorySyncHook)
255
- if (hooksConfig?.sanity_check ?? true) reg.registerPostTool(sanityCheckHook)
256
- if (hooksConfig?.dynamic_route ?? true) reg.registerPostTool(dynamicRouteHook)
257
- if (hooksConfig?.route_tracking ?? true) {
258
- reg.registerRoute(routeTrackingHook)
259
- } else {
242
+ // Check individual hook flags (default: true if not specified)
243
+ if (hooksConfig?.plan_check ?? true) reg.registerPreTool(planCheckHook)
244
+ if (hooksConfig?.shell_detect ?? true) reg.registerPreTool(shellDetectHook)
245
+ if (hooksConfig?.delegation_depth ?? true) reg.registerPreTool(delegationDepthHook)
246
+ reg.registerRoute(nextRouteHook)
247
+ if (hooksConfig?.confidence_gate ?? true) reg.registerRoute(confidenceGateHook)
248
+ if (hooksConfig?.dynamic_route ?? true) reg.registerPostTool(dynamicRouteHook)
249
+ if (hooksConfig?.route_tracking ?? true) {
250
+ reg.registerRoute(routeTrackingHook)
251
+ } else {
260
252
  reg.unregister("route-tracking")
261
253
  }
262
- if (hooksConfig?.subagent_failure ?? true) reg.registerPostTool(subagentFailureHook)
263
-
264
254
  await logToOC("info", `hooks: ${reg.getPreToolHooks().length + reg.getPostToolHooks().length + reg.getRouteHooks().length} registered`)
265
255
  } else {
266
256
  await logToOC("info", "hooks: disabled via config")
@@ -285,10 +275,20 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
285
275
  const loadedAgents = agentDefinitions(agentsDir)
286
276
  // Use composer for the OpenHermes agent prompt — assemble from fragments
287
277
  let openHermesPrompt: string
278
+ const injectSelfKnowledge = (prompt: string): string => {
279
+ try {
280
+ const pkgDir = import.meta.dirname
281
+ const pkg = JSON.parse(fs.readFileSync(path.resolve(pkgDir, "package.json"), "utf-8"))
282
+ return `OpenHermes v${pkg.version} | Install: ${pkgDir} | Harness: ${hDir}\n\n${prompt}`
283
+ } catch {
284
+ return prompt
285
+ }
286
+ }
288
287
  try {
289
- openHermesPrompt = compose()
288
+ openHermesPrompt = injectSelfKnowledge(compose())
290
289
  } catch {
291
290
  openHermesPrompt = loadedAgents[OPENHERMES_AGENT]?.prompt ?? "You are OpenHermes."
291
+ openHermesPrompt = injectSelfKnowledge(openHermesPrompt)
292
292
  }
293
293
  const openHermesAgent = {
294
294
  description: loadedAgents[OPENHERMES_AGENT]?.description ?? "OpenHermes primary orchestrator",
@@ -350,11 +350,11 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
350
350
  webfetch: "allow", // CAN fetch docs for context
351
351
  question: "allow", // CAN ask user questions
352
352
  websearch: "allow", // CAN search web for research context
353
- external_directory: { // CAN read/write plan files outside worktree
354
- "~/.local/share/openhermes/plans/**": "allow",
355
- },
356
- },
357
- },
353
+ external_directory: { // CAN read/write plan files outside worktree
354
+ "~/.local/share/openhermes/plans/**": "allow",
355
+ },
356
+ },
357
+ },
358
358
  }
359
359
 
360
360
  config.default_agent = OPENHERMES_AGENT
@@ -370,10 +370,9 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
370
370
  // creates plans on demand (see Task Flow step 1 in agent prompt).
371
371
  // Auto-creation produced ghost skeletons like plan-004.
372
372
 
373
- // Reset delegation depth and subagent failures on session start/error
373
+ // Reset delegation depth on session start/error
374
374
  if (typed.type === "session.created" || typed.type === "session.error") {
375
375
  resetDepthTracker()
376
- resetSubagentFailures()
377
376
  }
378
377
  },
379
378
 
@@ -386,10 +385,10 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
386
385
  if (input.tool === "task") {
387
386
  const reg = HookRegistry.getInstance()
388
387
 
389
- // Access optional fields from input (SDK may include these at runtime)
390
- const inputAny = input as Record<string, unknown>
391
- const agentName = typeof inputAny.agent === "string" ? inputAny.agent : "unknown"
392
- const pendingNextRoute = getRuntimeRouteDecision(ctx.directory) ?? undefined
388
+ // Access optional fields from input (SDK may include these at runtime)
389
+ const inputAny = input as Record<string, unknown>
390
+ const agentName = typeof inputAny.agent === "string" ? inputAny.agent : "unknown"
391
+ const pendingNextRoute = getRuntimeRouteDecision(ctx.directory) ?? undefined
393
392
 
394
393
  // Build hook context from input and current session state
395
394
  const hookContext: HookContext = {
@@ -397,12 +396,12 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
397
396
  agent: agentName,
398
397
  directory: ctx.directory,
399
398
  sessions: new Map(),
400
- _confidenceLevel: typeof inputAny.confidence === "string" ? inputAny.confidence : undefined,
401
- _confidenceExchanges: 0,
402
- _guardConfig: DEFAULT_GUARD_CONFIG,
403
- _nextRoute: pendingNextRoute,
404
- _routingSkillsDir: skillsDir,
405
- }
399
+ _confidenceLevel: typeof inputAny.confidence === "string" ? inputAny.confidence : undefined,
400
+ _confidenceExchanges: 0,
401
+ _guardConfig: DEFAULT_GUARD_CONFIG,
402
+ _nextRoute: pendingNextRoute,
403
+ _routingSkillsDir: skillsDir,
404
+ }
406
405
 
407
406
  // Run all registered PreToolUse hooks (plan check, shell detect, delegation depth)
408
407
  let preToolResult: { result: HookResult; modifiedContext?: HookContext }
@@ -462,27 +461,27 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
462
461
  routeResult = { result: HookResult.CONTINUE }
463
462
  }
464
463
 
465
- if (routeResult.result === HookResult.STOP) {
466
- // Loop guard triggered by route-tracking hook
467
- const errOutput = output as { args: unknown; isError?: boolean; content?: unknown[] }
468
- errOutput.isError = true
469
- const optiReport = hookContext._optiRoute
470
- ? JSON.stringify(hookContext._optiRoute, null, 2)
471
- : "Route guard: Excessive or unproductive routing detected."
472
- errOutput.content = [{ type: "text", text: `ROUTE GUARD: ${optiReport}\n\nSurface to orchestrator with findings and stop delegating.` }]
473
- }
474
-
475
- if (routeResult.modifiedRoute) {
476
- const concreteRoute = routeResult.modifiedRoute.split("?")[0] ?? routeResult.modifiedRoute
477
- if (concreteRoute && concreteRoute !== agentName) {
478
- inputAny.agent = concreteRoute
479
- }
480
- if (pendingNextRoute?.selected && concreteRoute === pendingNextRoute.selected) {
481
- clearRuntimeRouteDecision(ctx.directory)
482
- }
483
- }
484
-
485
- if (routeResult.result === HookResult.INJECT && routeResult.modifiedRoute) {
464
+ if (routeResult.result === HookResult.STOP) {
465
+ // Loop guard triggered by route-tracking hook
466
+ const errOutput = output as { args: unknown; isError?: boolean; content?: unknown[] }
467
+ errOutput.isError = true
468
+ const optiReport = hookContext._optiRoute
469
+ ? JSON.stringify(hookContext._optiRoute, null, 2)
470
+ : "Route guard: Excessive or unproductive routing detected."
471
+ errOutput.content = [{ type: "text", text: `ROUTE GUARD: ${optiReport}\n\nSurface to orchestrator with findings and stop delegating.` }]
472
+ }
473
+
474
+ if (routeResult.modifiedRoute) {
475
+ const concreteRoute = routeResult.modifiedRoute.split("?")[0] ?? routeResult.modifiedRoute
476
+ if (concreteRoute && concreteRoute !== agentName) {
477
+ inputAny.agent = concreteRoute
478
+ }
479
+ if (pendingNextRoute?.selected && concreteRoute === pendingNextRoute.selected) {
480
+ clearRuntimeRouteDecision(ctx.directory)
481
+ }
482
+ }
483
+
484
+ if (routeResult.result === HookResult.INJECT && routeResult.modifiedRoute) {
486
485
  // Confidence gate wants to inject a confirmation/pause into routing.
487
486
  // Parse the modifiedRoute for markers and inject into task description/prompt.
488
487
  const modifiedRoute: string = routeResult.modifiedRoute
@@ -520,21 +519,21 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
520
519
  const agentName = typeof inputAny.agent === "string" ? inputAny.agent : "unknown"
521
520
 
522
521
  // Build hook context from input and current session state
523
- const hookContext: HookContext = {
524
- sessionId: ctx.directory,
525
- agent: agentName,
526
- directory: ctx.directory,
527
- sessions: new Map(),
528
- _confidenceLevel: typeof inputAny.confidence === "string" ? inputAny.confidence : undefined,
529
- _confidenceExchanges: 0,
530
- _guardConfig: DEFAULT_GUARD_CONFIG,
531
- _routingSkillsDir: skillsDir,
532
- }
522
+ const hookContext: HookContext = {
523
+ sessionId: ctx.directory,
524
+ agent: agentName,
525
+ directory: ctx.directory,
526
+ sessions: new Map(),
527
+ _confidenceLevel: typeof inputAny.confidence === "string" ? inputAny.confidence : undefined,
528
+ _confidenceExchanges: 0,
529
+ _guardConfig: DEFAULT_GUARD_CONFIG,
530
+ _routingSkillsDir: skillsDir,
531
+ }
533
532
 
534
533
  // Extract output text from tool result
535
534
  // output.content may be array of content blocks, or a string, or undefined
536
- const outputAny = output as Record<string, unknown> | undefined
537
- const mutableOutput = (output ?? {}) as Record<string, unknown>
535
+ const outputAny = output as Record<string, unknown> | undefined
536
+ const mutableOutput = (output ?? {}) as Record<string, unknown>
538
537
  let outputText = ""
539
538
  if (outputAny?.content) {
540
539
  const content = outputAny.content
@@ -551,34 +550,27 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
551
550
  try {
552
551
  const postToolResult = await reg.executePostTool(hookContext, outputText)
553
552
 
554
- // Surface recovery instructions from errorRecoveryHook and/or sanityCheckHook
555
- if (postToolResult.recovery) {
556
- await logToOC("warn", `PostTool recovery instruction:\n${postToolResult.recovery}`)
553
+ // Log when hooks signal issues (INJECT = anomaly/error detected by a hook)
554
+ if (postToolResult.result === HookResult.INJECT) {
555
+ await logToOC("warn", "PostTool INJECT: hooks detected issues in tool output")
556
+ }
557
+
558
+ const routedOutput = consumeRouteGuidance(postToolResult.modifiedOutput ?? outputText)
559
+ const finalOutput = routedOutput.output
560
+ const runtimeNextRoute = rememberRuntimeRouteDecision(ctx.directory, finalOutput)
561
+
562
+ if (finalOutput !== outputText) {
563
+ if (typeof outputAny?.content === "string") {
564
+ mutableOutput.content = finalOutput
565
+ } else {
566
+ mutableOutput.content = [{ type: "text", text: finalOutput }]
567
+ }
568
+ }
569
+
570
+ if (runtimeNextRoute) {
571
+ mutableOutput._nextRoute = runtimeNextRoute
557
572
  }
558
573
 
559
- // Log when hooks signal issues (INJECT = anomaly/error detected by a hook)
560
- if (postToolResult.result === HookResult.INJECT) {
561
- await logToOC("warn", "PostTool INJECT: hooks detected issues in tool output")
562
- }
563
-
564
- const routedOutput = consumeRouteGuidance(postToolResult.modifiedOutput ?? outputText)
565
- const finalOutput = routedOutput.output
566
- const runtimeNextRoute = rememberRuntimeRouteDecision(ctx.directory, finalOutput)
567
-
568
- if (finalOutput !== outputText) {
569
- if (typeof outputAny?.content === "string") {
570
- mutableOutput.content = finalOutput
571
- } else {
572
- mutableOutput.content = [{ type: "text", text: finalOutput }]
573
- }
574
- }
575
-
576
- if (runtimeNextRoute) {
577
- mutableOutput._nextRoute = runtimeNextRoute
578
- }
579
-
580
- // memorySyncHook catches its own errors (best-effort sync),
581
- // so memory sync failures are already handled gracefully inside the hook
582
574
  } catch (err) {
583
575
  const msg = err instanceof Error ? err.message : String(err)
584
576
  await logToOC("error", `Hook error (PostTool): ${msg}`)
@@ -0,0 +1,162 @@
1
+ # How OpenHermes Works
2
+
3
+ This document traces the runtime data flow of the OpenHermes plugin package.
4
+ It is the primary bus-factor mitigation for the routing engine, hook system, plan storage, and composer.
5
+
6
+ ## 1. Skill Routing Flow
7
+
8
+ ```
9
+ User Request
10
+
11
+
12
+ Orchestrator (classifies task, loads skill)
13
+
14
+ ├── Skill SKILL.md frontmatter defines routes:
15
+ │ pass → [oh-ship, oh-gauntlet]
16
+ │ fail → oh-planner
17
+ │ blocker → surface
18
+
19
+
20
+ Route Resolver (harness/lib/routing/route-resolver.ts)
21
+
22
+ ├── Collects route candidates from skill frontmatter
23
+ ├── Applies NEXT_ROUTE overrides from subagent output
24
+ ├── Applies ROUTE_GUIDANCE evidence from subagent output
25
+ └── Selects target skill or terminal (surface/done)
26
+
27
+
28
+ Dispatch to target skill subagent
29
+ ```
30
+
31
+ ### Key files:
32
+ - `harness/lib/routing/route-resolver.ts` — resolves route candidates from skill frontmatter
33
+ - `harness/lib/routing/route-guidance.ts` — applies NEXT_ROUTE overrides and evidence
34
+ - `harness/lib/routing/skill-frontmatter.ts` — parses pass/fail/blocker from SKILL.md
35
+
36
+ ## 2. Hook Lifecycle
37
+
38
+ ```
39
+ BootstrapPlugin registers hooks during init
40
+
41
+
42
+ PreTool hooks fire before tool execution (EARLY → NORMAL → LATE)
43
+
44
+ ├── confidence-gate: checks user input for injection tokens
45
+ ├── delegation-depth: prevents runaway agent chains (max 5)
46
+ ├── plan-check: ensures plan files exist for multi-step work
47
+ └── shell-detect: identifies Windows shell type
48
+
49
+
50
+ PostTool hooks fire after tool execution
51
+
52
+ ├── route-tracking: logs which route was taken
53
+ ├── next-route: captures NEXT_ROUTE from output
54
+ └── dynamic-route: applies evidence-driven routing
55
+
56
+
57
+ Route hooks modify routing decisions
58
+
59
+ Session hooks fire on session boundaries
60
+ ```
61
+
62
+ ### Hook Types:
63
+ | Type | When | Count |
64
+ |------|------|-------|
65
+ | PreTool | Before each tool call | 4 |
66
+ | PostTool | After each tool call | 3 |
67
+ | Route | During route resolution | 0 (extensible) |
68
+ | Session | Session start/end | 0 (extensible) |
69
+
70
+ ### Phases (within each hook type):
71
+ - **EARLY** — high-priority, runs first
72
+ - **NORMAL** — standard priority
73
+ - **LATE** — low-priority, runs last
74
+
75
+ ### Key files:
76
+ - `harness/lib/hooks/builtins/` — 7 built-in hook implementations
77
+ - `harness/lib/hooks/hooks.test.ts` — hook system tests (928 lines)
78
+ - `bootstrap.ts` — hook registration in `plugin.tool.execute.before/after`
79
+
80
+ ## 3. Plan Storage
81
+
82
+ ```
83
+ Plans stored at: ~/.local/share/openhermes/plans/<project>/
84
+
85
+ ├── <project>/plan-001.md
86
+ ├── <project>/plan-002.md
87
+ └── <project>/plan-003.md
88
+
89
+
90
+ Sequential numbering (001, 002, 003...)
91
+
92
+
93
+ Status lifecycle:
94
+ active/in-progress → keep
95
+ complete/abandoned → delete
96
+ ```
97
+
98
+ ### Key files:
99
+ - `harness/lib/plans/plan-location.ts` — resolves canonical paths
100
+ - `bootstrap.ts` — exports `ensurePlanFile`, `findLatestPlanFile`, `resolveHarnessRoot`
101
+
102
+ ## 4. Composer (Agent Prompt Assembly)
103
+
104
+ ```
105
+ 9 numbered fragments in harness/lib/composer/fragments/
106
+
107
+ ├── 01-identity.md → "You are OpenHermes..."
108
+ ├── 02-delegation.md → Core Behaviors
109
+ ├── 03-permissions.md → Permission matrix
110
+ ├── 04-task-flow.md → Task flow steps
111
+ ├── 05-confidence.md → Stop Conditions
112
+ ├── 06-parallelization.md → Parallelization rules
113
+ ├── 07-shell.md → Shell Awareness
114
+ ├── 08-routing.md → Plan Storage
115
+ └── 09-guardrails.md → Guardrails + Routing
116
+
117
+
118
+ compose.ts assembles fragments with phase filtering (EARLY/NORMAL/LATE)
119
+
120
+
121
+ Path traversal sanitization prevents directory escape
122
+
123
+
124
+ Output: complete agent prompt consumed by the LLM
125
+ ```
126
+
127
+ ### Key files:
128
+ - `harness/lib/composer/compose.ts` — fragment assembly engine
129
+ - `harness/lib/composer/fragments/` — 9 content fragments
130
+ - `harness/agents/openhermes.md` — agent manifest declaring fragments
131
+
132
+ ## 5. Full Request Lifecycle (ASCII Overview)
133
+
134
+ ```
135
+ ┌─────────────────────────────────────────────────────┐
136
+ │ User sends request │
137
+ │ │ │
138
+ │ ▼ │
139
+ │ OpenHermes Orchestrator │
140
+ │ │ │
141
+ │ ├── 1. Classify task (investigate/build/...)│
142
+ │ ├── 2. Load skill (SKILL.md frontmatter) │
143
+ │ ├── 3. PreTool hooks fire (confidence, etc) │
144
+ │ ├── 4. Delegate to subagent │
145
+ │ ├── 5. Subagent returns output + evidence │
146
+ │ ├── 6. PostTool hooks fire (tracking, etc) │
147
+ │ ├── 7. ROUTE_EVIDENCE parsed │
148
+ │ └── 8. Route to next skill or surface │
149
+ │ │ │
150
+ │ ▼ │
151
+ │ Next skill (or surface/done) │
152
+ └─────────────────────────────────────────────────────┘
153
+ ```
154
+
155
+ ## Architecture Summary
156
+
157
+ | System | Purpose | Key File |
158
+ |--------|---------|----------|
159
+ | Routing | Resolves next skill from frontmatter + evidence | `harness/lib/routing/route-resolver.ts` |
160
+ | Hooks | Plugin extensibility points | `harness/lib/hooks/builtins/` |
161
+ | Plans | Canonical task tracking | `harness/lib/plans/plan-location.ts` |
162
+ | Composer | Agent prompt assembly | `harness/lib/composer/compose.ts` |