openhermes 4.11.2 → 4.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CONTEXT.md +1 -1
  2. package/ETHOS.md +1 -1
  3. package/README.md +12 -18
  4. package/bootstrap.ts +73 -148
  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 +30 -23
  13. package/harness/codex/CHARTER.md +3 -3
  14. package/harness/lib/composer/compose.test.ts +11 -0
  15. package/harness/lib/composer/fragments/02-delegation.md +2 -1
  16. package/harness/lib/composer/fragments/04-task-flow.md +42 -2
  17. package/harness/lib/composer/fragments/08-routing.md +1 -1
  18. package/harness/lib/composer/fragments/09-guardrails.md +17 -4
  19. package/harness/lib/composer/index.ts +1 -1
  20. package/harness/lib/guards/guard-config.ts +72 -0
  21. package/harness/lib/hooks/builtins/confidence-gate-hook.ts +2 -4
  22. package/harness/lib/hooks/builtins/delegation-depth-hook.ts +23 -4
  23. package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -0
  24. package/harness/lib/hooks/builtins/next-route-hook.ts +24 -0
  25. package/harness/lib/hooks/builtins/plan-check-hook.ts +2 -2
  26. package/harness/lib/hooks/builtins/route-tracking-hook.ts +79 -25
  27. package/harness/lib/hooks/hooks.test.ts +117 -205
  28. package/harness/lib/hooks/index.ts +38 -30
  29. package/harness/lib/hooks/registry.ts +309 -416
  30. package/harness/lib/hooks/types.ts +116 -71
  31. package/harness/lib/plans/plan-location.ts +134 -0
  32. package/harness/lib/routing/index.ts +21 -0
  33. package/harness/lib/routing/route-guidance.ts +147 -0
  34. package/harness/lib/routing/route-resolver.ts +58 -0
  35. package/harness/lib/routing/routing.test.ts +195 -0
  36. package/harness/lib/routing/skill-frontmatter.ts +125 -0
  37. package/harness/lib/routing/types.ts +52 -0
  38. package/harness/skills/oh-ascii/SKILL.md +1 -1
  39. package/harness/skills/oh-fusion/DEEP.md +56 -33
  40. package/harness/skills/oh-fusion/SKILL.md +30 -16
  41. package/harness/skills/oh-init/DEEP.md +2 -2
  42. package/harness/skills/oh-manifest/SKILL.md +1 -0
  43. package/harness/skills/oh-plan-review/DEEP.md +1 -1
  44. package/harness/skills/oh-planner/DEEP.md +3 -3
  45. package/harness/skills/oh-review/DEEP.md +2 -0
  46. package/harness/skills/oh-review/SKILL.md +1 -0
  47. package/package.json +56 -55
  48. package/harness/lib/background/background.test.ts +0 -197
  49. package/harness/lib/background/index.ts +0 -7
  50. package/harness/lib/background/interfaces.ts +0 -31
  51. package/harness/lib/background/manager.ts +0 -320
  52. package/harness/lib/hooks/builtins/error-recovery-hook.ts +0 -107
  53. package/harness/lib/hooks/builtins/memory-sync-hook.ts +0 -73
  54. package/harness/lib/hooks/builtins/sanity-check-hook.ts +0 -52
  55. package/harness/lib/memory/index.ts +0 -18
  56. package/harness/lib/memory/interfaces.ts +0 -53
  57. package/harness/lib/memory/memory-manager.ts +0 -205
  58. package/harness/lib/memory/memory.test.ts +0 -491
  59. package/harness/lib/memory/plan-store.ts +0 -366
  60. package/harness/lib/recovery/handler.ts +0 -243
  61. package/harness/lib/recovery/index.ts +0 -14
  62. package/harness/lib/recovery/interfaces.ts +0 -48
  63. package/harness/lib/recovery/patterns.ts +0 -149
  64. package/harness/lib/recovery/recovery.test.ts +0 -312
  65. package/harness/lib/sanity/anomaly-tracker.ts +0 -127
  66. package/harness/lib/sanity/checker.ts +0 -178
  67. package/harness/lib/sanity/index.ts +0 -13
  68. package/harness/lib/sanity/interfaces.ts +0 -24
  69. package/harness/lib/sanity/sanity.test.ts +0 -472
  70. package/harness/lib/sync/file-watcher.ts +0 -174
  71. package/harness/lib/sync/index.ts +0 -11
  72. package/harness/lib/sync/interfaces.ts +0 -27
  73. package/harness/lib/sync/plan-sync.ts +0 -536
  74. package/harness/lib/sync/sync.test.ts +0 -832
package/CONTEXT.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ## Terms
4
4
  **OpenHermes** — OpenCode-native orchestration layer for this package.
5
5
  **Skill** — A `SKILL.md` loaded on demand through OpenCode's skill tool.
6
- **Command** — A slash command backed by package-local markdown in `harness/commands/`.
6
+ **Command** — A slash command backed by package-local command markdown; legacy compatibility loaders remain only where runtime-backed.
7
7
  **Agent** — A primary or subagent definition loaded through OpenCode config.
8
8
  **Instruction** — Markdown loaded through `AGENTS.md` or `opencode.json` instructions.
9
9
  **Bootstrap** — The first-message context injected by the OpenHermes plugin.
package/ETHOS.md CHANGED
@@ -9,7 +9,7 @@ OpenCode-native loading over manual copying or hidden state.
9
9
  Every file earns its keep. Prefer markdown when behavior is declarative.
10
10
 
11
11
  ## Skills Over Glue
12
- Behavior lives in `SKILL.md`, `commands/*.md`, and `agents/*.md`.
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.1"></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,18 +33,13 @@ 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. |
47
- | **Test Harness** | Disposable temp dirs (Symbol.asyncDispose), factory builders, restore-capable mocks. Professional-grade test utilities. |
41
+ | **Hook Registry** | Pluggable pre-tool, post-tool, route, and session hooks with priority-sort ordering. 7 built-in hooks, zero routing boilerplate. |
42
+ | **Plan Location** | Resolves plan file paths per project with directory-per-project layout in `~/.local/share/openhermes/plans/`. |
48
43
 
49
44
  ---
50
45
 
@@ -72,14 +67,14 @@ 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/`. 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** | Compositor, hooks, memory, recovery, sync, sanity checks, background commands, test harness — 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
- ## 30 skills — three tiers
77
+ ## 30 skills — four tiers
83
78
 
84
79
  ### Tier 4 — Pipeline orchestrators
85
80
  Full multi-phase workflows:
@@ -138,21 +133,20 @@ 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
- │ ├── commands/ # Slash commands
147
144
  │ ├── instructions/ # SHELL.md
148
145
  │ ├── lib/ # Internal subsystems
149
146
  │ │ ├── composer/ # Prompt fragment composition
150
- │ │ ├── recovery/ # Auto-recovery with error patterns
151
- │ │ ├── memory/ # 4-tier hierarchical memory
152
- │ │ ├── sync/ # MVCC plan synchronization
153
147
  │ │ ├── hooks/ # Pluggable hook registry
154
- │ │ ├── sanity/ # Output degeneration detection
155
- │ │ └── background/ # Fire-and-forget command system
148
+ │ │ ├── plans/ # Plan file path resolution
149
+ │ │ └── ... # guards/ (guard config)
156
150
  │ └── skills/ # 30 skill SKILL.md files (CORE/DEEP format)
157
151
  ├── lib/ # harness-resolver.ts
158
152
  └── test/
package/bootstrap.ts CHANGED
@@ -4,21 +4,24 @@ 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
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"
7
9
 
8
10
  // Hook system — pluggable lifecycle hooks with topological sort
9
11
  import {
10
12
  HookRegistry,
11
13
  HookResult,
14
+ nextRouteHook,
12
15
  planCheckHook,
13
16
  shellDetectHook,
14
17
  confidenceGateHook,
15
18
  delegationDepthHook,
16
19
  resetDepthTracker,
17
- errorRecoveryHook,
18
- memorySyncHook,
19
- sanityCheckHook,
20
+ dynamicRouteHook,
20
21
  routeTrackingHook,
22
+ DEFAULT_GUARD_CONFIG,
21
23
  } from "./harness/lib/hooks/index.ts"
24
+ import type { HookContext } from "./harness/lib/hooks/index.ts"
22
25
 
23
26
  const OPENHERMES_AGENT = "OpenHermes"
24
27
 
@@ -29,18 +32,7 @@ const USER_SKILL_DIRS: ReadonlyArray<string> = [
29
32
  path.join(os.homedir(), ".claude", "skills"), // Claude Code backward compat
30
33
  ]
31
34
 
32
- // Canonical storage under OpenCode's data directory survives npm updates
33
- let _planStorageOverride: string | undefined
34
- export function setPlanStorageDirForTest(dir: string | undefined): void { _planStorageOverride = dir }
35
- function planStorageDir(): string {
36
- return _planStorageOverride ?? path.join(os.homedir(), ".local", "share", "openhermes", "plans")
37
- }
38
-
39
- function getProjectName(projectDir: string): string {
40
- return path.basename(projectDir)
41
- }
42
-
43
- export { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir, ensurePlanFile, findLatestPlanFile }
35
+ export { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir, ensurePlanFile, findLatestPlanFile, setPlanStorageDirForTest }
44
36
 
45
37
  function parseFrontmatter(raw: string | undefined): Record<string, string> {
46
38
  const frontmatter: Record<string, string> = {}
@@ -140,46 +132,6 @@ function uniqueStrings(existing: string[] = [], additions: string[] = []): strin
140
132
  }
141
133
 
142
134
 
143
- function findLatestPlanFile(projectDir: string): string | null {
144
- const projectName = getProjectName(projectDir)
145
- const storage = planStorageDir()
146
- const projectDirPath = path.join(storage, projectName)
147
- if (!fs.existsSync(projectDirPath)) return null
148
- let latest: string | null = null
149
- let highest = -1
150
- try {
151
- for (const entry of fs.readdirSync(projectDirPath)) {
152
- const m = entry.match(/^plan-(\d{3})\.md$/)
153
- if (m) {
154
- const n = parseInt(m[1], 10)
155
- if (n > highest) {
156
- highest = n
157
- latest = path.join(projectDirPath, entry)
158
- }
159
- }
160
- }
161
- } catch {
162
- return null
163
- }
164
- return latest
165
- }
166
-
167
- function readPlanFromFile(filePath: string): string | null {
168
- if (!fs.existsSync(filePath)) return null
169
- const source = fs.readFileSync(filePath, "utf8")
170
- const status = source.match(/^Status:\s*(.+)$/m)?.[1]?.trim()
171
- const objective = source.match(/^Objective:\s*(.+)$/m)?.[1]?.trim()
172
- if (!status && !objective) return null
173
- const parts = [status ? `status=${status}` : null, objective ? `objective=${objective}` : null].filter(Boolean)
174
- return `Active plan: ${parts.join(" | ")}`
175
- }
176
-
177
- function readPlanSummary(projectDir: string): string | null {
178
- const planFile = findLatestPlanFile(projectDir)
179
- if (!planFile) return null
180
- return readPlanFromFile(planFile)
181
- }
182
-
183
135
  function ensureDir(dir: string): void {
184
136
  try {
185
137
  if (!fs.existsSync(dir)) {
@@ -192,68 +144,6 @@ function ensureDir(dir: string): void {
192
144
  }
193
145
  }
194
146
 
195
- /**
196
- * Ensure a plan file exists for the project.
197
- * Creates a skeleton plan if none exists or if the latest is complete/abandoned.
198
- * Reuses an existing active or in-progress plan.
199
- * Returns the path to the plan file.
200
- */
201
- function ensurePlanFile(projectDir: string): string {
202
- const projectName = getProjectName(projectDir)
203
- const storage = planStorageDir()
204
- const projectDirPath = path.join(storage, projectName)
205
- ensureDir(projectDirPath)
206
-
207
- // Reuse active or in-progress plan
208
- const latest = findLatestPlanFile(projectDir)
209
- if (latest) {
210
- const content = fs.readFileSync(latest, "utf8")
211
- const status = content.match(/^Status:\s*(.+)$/m)?.[1]?.trim()
212
- if (status === "active" || status === "in-progress") {
213
- return latest
214
- }
215
- }
216
-
217
- // Determine next sequence number
218
- let nextSeq = 1
219
- if (latest) {
220
- const m = path.basename(latest).match(/^plan-(\d{3})\.md$/)
221
- if (m) nextSeq = parseInt(m[1], 10) + 1
222
- }
223
-
224
- const seq = String(nextSeq).padStart(3, "0")
225
- const planId = `${projectName}/plan-${seq}.md`
226
- const planPath = path.join(projectDirPath, `plan-${seq}.md`)
227
- const now = new Date().toISOString().replace("T", " ").slice(0, 16)
228
-
229
- const content = [
230
- `# PLAN: ${projectName}`,
231
- "",
232
- `Plan ID: ${planId}`,
233
- `Project: ${projectName}`,
234
- `Status: active`,
235
- `Created: ${now}`,
236
- `Updated: ${now}`,
237
- `Project Path: ${projectDir}`,
238
- `Plan Path: ${planPath}`,
239
- `Objective: (pending classification)`,
240
- "",
241
- "## Tasks",
242
- "",
243
- "- [ ] (discoverable — pending classification)",
244
- "",
245
- ].join("\n")
246
-
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
- }
254
- return planPath
255
- }
256
-
257
147
  export function buildCompactionContext(projectDir: string): string[] {
258
148
  const context = [
259
149
  "OpenHermes: native-first, verify before claim, always delegate, concise over verbose.",
@@ -261,7 +151,7 @@ export function buildCompactionContext(projectDir: string): string[] {
261
151
  "Preserve blockers, current task, and next steps; do not invent durable state.",
262
152
  ]
263
153
 
264
- const planSummary = readPlanSummary(projectDir)
154
+ const planSummary = resolvePlanAccess(projectDir)?.summary
265
155
  if (planSummary) context.push(planSummary)
266
156
 
267
157
  return context
@@ -333,13 +223,17 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
333
223
  // Ensure plan storage exists
334
224
  try { ensureDir(planStorageDir()) } catch {}
335
225
 
226
+
227
+
336
228
  return {
337
229
  config: async (config: OpenHermesConfig) => {
230
+
338
231
  // ── 1. Hooks System ─────────────────────────────────────────────────
339
232
  // Read experimental.hooks config from the raw config object
340
- const hooksConfig = (config.experimental as Record<string, unknown> | undefined)?.hooks as
233
+ const experimental = config.experimental as Record<string, unknown> | undefined;
234
+ const hooksConfig = (experimental?.hooks as
341
235
  | Record<string, boolean>
342
- | undefined
236
+ | undefined)
343
237
  const hooksEnabled = (hooksConfig?.enabled ?? true) as boolean
344
238
 
345
239
  if (hooksEnabled) {
@@ -349,12 +243,14 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
349
243
  if (hooksConfig?.plan_check ?? true) reg.registerPreTool(planCheckHook)
350
244
  if (hooksConfig?.shell_detect ?? true) reg.registerPreTool(shellDetectHook)
351
245
  if (hooksConfig?.delegation_depth ?? true) reg.registerPreTool(delegationDepthHook)
246
+ reg.registerRoute(nextRouteHook)
352
247
  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
-
248
+ if (hooksConfig?.dynamic_route ?? true) reg.registerPostTool(dynamicRouteHook)
249
+ if (hooksConfig?.route_tracking ?? true) {
250
+ reg.registerRoute(routeTrackingHook)
251
+ } else {
252
+ reg.unregister("route-tracking")
253
+ }
358
254
  await logToOC("info", `hooks: ${reg.getPreToolHooks().length + reg.getPostToolHooks().length + reg.getRouteHooks().length} registered`)
359
255
  } else {
360
256
  await logToOC("info", "hooks: disabled via config")
@@ -379,10 +275,20 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
379
275
  const loadedAgents = agentDefinitions(agentsDir)
380
276
  // Use composer for the OpenHermes agent prompt — assemble from fragments
381
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
+ }
382
287
  try {
383
- openHermesPrompt = compose()
288
+ openHermesPrompt = injectSelfKnowledge(compose())
384
289
  } catch {
385
290
  openHermesPrompt = loadedAgents[OPENHERMES_AGENT]?.prompt ?? "You are OpenHermes."
291
+ openHermesPrompt = injectSelfKnowledge(openHermesPrompt)
386
292
  }
387
293
  const openHermesAgent = {
388
294
  description: loadedAgents[OPENHERMES_AGENT]?.description ?? "OpenHermes primary orchestrator",
@@ -445,7 +351,7 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
445
351
  question: "allow", // CAN ask user questions
446
352
  websearch: "allow", // CAN search web for research context
447
353
  external_directory: { // CAN read/write plan files outside worktree
448
- "~/.local/share/opencode/openhermes/**": "allow",
354
+ "~/.local/share/openhermes/plans/**": "allow",
449
355
  },
450
356
  },
451
357
  },
@@ -482,24 +388,23 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
482
388
  // Access optional fields from input (SDK may include these at runtime)
483
389
  const inputAny = input as Record<string, unknown>
484
390
  const agentName = typeof inputAny.agent === "string" ? inputAny.agent : "unknown"
391
+ const pendingNextRoute = getRuntimeRouteDecision(ctx.directory) ?? undefined
485
392
 
486
393
  // Build hook context from input and current session state
487
- const hookContext = {
394
+ const hookContext: HookContext = {
488
395
  sessionId: ctx.directory, // project directory as session key
489
396
  agent: agentName,
490
397
  directory: ctx.directory,
491
398
  sessions: new Map(),
492
399
  _confidenceLevel: typeof inputAny.confidence === "string" ? inputAny.confidence : undefined,
493
400
  _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
- },
401
+ _guardConfig: DEFAULT_GUARD_CONFIG,
402
+ _nextRoute: pendingNextRoute,
403
+ _routingSkillsDir: skillsDir,
499
404
  }
500
405
 
501
406
  // Run all registered PreToolUse hooks (plan check, shell detect, delegation depth)
502
- let preToolResult: any
407
+ let preToolResult: { result: HookResult; modifiedContext?: HookContext }
503
408
  try {
504
409
  preToolResult = await reg.executePreTool(hookContext)
505
410
  } catch (err) {
@@ -515,7 +420,7 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
515
420
  const errOutput = output as { args: unknown; isError?: boolean; content?: unknown[] }
516
421
  errOutput.isError = true
517
422
  const message = (preToolResult.modifiedContext?._depthError as string)
518
- ?? "LOOP GUARD: Delegation depth exceeded (max 25). " +
423
+ ?? `LOOP GUARD: Delegation depth exceeded (max ${DEFAULT_GUARD_CONFIG.maxDelegationDepth}). ` +
519
424
  "Surface to orchestrator with findings and stop delegating."
520
425
  errOutput.content = [{ type: "text", text: message }]
521
426
  return
@@ -546,7 +451,7 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
546
451
  // Run all registered RouteHooks — the agent/skill being delegated to IS the route
547
452
  // This fires confidence-gate (inject confirm/question on MEDIUM/LOW confidence)
548
453
  // and route-tracking (guard against infinite routing loops)
549
- let routeResult: any
454
+ let routeResult: { result: HookResult; modifiedRoute?: string }
550
455
  try {
551
456
  routeResult = await reg.executeRoute(hookContext, agentName)
552
457
  } catch (err) {
@@ -560,13 +465,22 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
560
465
  // Loop guard triggered by route-tracking hook
561
466
  const errOutput = output as { args: unknown; isError?: boolean; content?: unknown[] }
562
467
  errOutput.isError = true
563
- const ctxAny = hookContext as Record<string, unknown>
564
- const optiReport = ctxAny._optiRoute
565
- ? JSON.stringify(ctxAny._optiRoute, null, 2)
468
+ const optiReport = hookContext._optiRoute
469
+ ? JSON.stringify(hookContext._optiRoute, null, 2)
566
470
  : "Route guard: Excessive or unproductive routing detected."
567
471
  errOutput.content = [{ type: "text", text: `ROUTE GUARD: ${optiReport}\n\nSurface to orchestrator with findings and stop delegating.` }]
568
472
  }
569
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
+
570
484
  if (routeResult.result === HookResult.INJECT && routeResult.modifiedRoute) {
571
485
  // Confidence gate wants to inject a confirmation/pause into routing.
572
486
  // Parse the modifiedRoute for markers and inject into task description/prompt.
@@ -605,19 +519,21 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
605
519
  const agentName = typeof inputAny.agent === "string" ? inputAny.agent : "unknown"
606
520
 
607
521
  // Build hook context from input and current session state
608
- const hookContext = {
522
+ const hookContext: HookContext = {
609
523
  sessionId: ctx.directory,
610
524
  agent: agentName,
611
525
  directory: ctx.directory,
612
526
  sessions: new Map(),
613
527
  _confidenceLevel: typeof inputAny.confidence === "string" ? inputAny.confidence : undefined,
614
528
  _confidenceExchanges: 0,
615
- _maxDelegationDepth: 25,
529
+ _guardConfig: DEFAULT_GUARD_CONFIG,
530
+ _routingSkillsDir: skillsDir,
616
531
  }
617
532
 
618
533
  // Extract output text from tool result
619
534
  // output.content may be array of content blocks, or a string, or undefined
620
535
  const outputAny = output as Record<string, unknown> | undefined
536
+ const mutableOutput = (output ?? {}) as Record<string, unknown>
621
537
  let outputText = ""
622
538
  if (outputAny?.content) {
623
539
  const content = outputAny.content
@@ -634,18 +550,27 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
634
550
  try {
635
551
  const postToolResult = await reg.executePostTool(hookContext, outputText)
636
552
 
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
553
  // Log when hooks signal issues (INJECT = anomaly/error detected by a hook)
643
554
  if (postToolResult.result === HookResult.INJECT) {
644
555
  await logToOC("warn", "PostTool INJECT: hooks detected issues in tool output")
645
556
  }
646
557
 
647
- // memorySyncHook catches its own errors (best-effort sync),
648
- // so memory sync failures are already handled gracefully inside the hook
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
572
+ }
573
+
649
574
  } catch (err) {
650
575
  const msg = err instanceof Error ? err.message : String(err)
651
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` |
@@ -0,0 +1,30 @@
1
+ # ADR-0001: Rebuild v3→v4
2
+
3
+ **Status**: Accepted
4
+ **Date**: 2026-05-19
5
+
6
+ ## Context
7
+
8
+ The project started as a memory-tools plugin (v1–v3) with OHC compression. After three major versions, the architecture had accumulated significant complexity from incremental additions. The codebase mixed memory-tool concerns with nascent orchestration logic. Two paths existed: continue patching the existing architecture with incremental fixes, or clean-sheet rebuild as a full skill-harness platform.
9
+
10
+ Key constraints:
11
+ - The platform vision demanded 30+ skills and 17+ agent types — far beyond the original scope
12
+ - Existing users depended on v3 stability
13
+ - Team bandwidth allowed only one major direction
14
+
15
+ ## Decision
16
+
17
+ Clean-sheet rebuild into a 30-skill, 17-agent harness platform with:
18
+ - Routing engine for skill dispatch
19
+ - Hooks system for plugin extensibility
20
+ - Canonical plan storage with sequential naming
21
+ - Fragment-based prompt composition
22
+
23
+ No backward compatibility with v3 memory-tools internals.
24
+
25
+ ## Consequences
26
+
27
+ - **Positive**: Fundamentally better foundation for the platform vision. Clean separation between routing, hooks, plans, and composition. Easier to test each subsystem independently.
28
+ - **Positive**: Ability to onboard new skill types and agent roles without fighting legacy constraints.
29
+ - **Negative**: Temporary disruption of ongoing work — existing v3 features had to be re-implemented.
30
+ - **Negative**: Migration cost for any v3 users adopting the new platform.