openhermes 4.11.2 → 4.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +1 -1
- package/ETHOS.md +1 -1
- package/README.md +12 -18
- package/bootstrap.ts +73 -148
- package/docs/HOW-IT-WORKS.md +162 -0
- package/docs/adr/ADR-0001-rebuild-vs-increment.md +30 -0
- package/docs/adr/ADR-0002-routing-graph-vs-linear-chain.md +36 -0
- package/docs/adr/ADR-0003-per-directory-plan-storage.md +34 -0
- package/docs/adr/ADR-0004-composer-fragment-architecture.md +42 -0
- package/docs/adr/ADR-0005-hook-system-design.md +42 -0
- package/docs/adr/README.md +9 -0
- package/harness/codex/AUTOPILOT.md +30 -23
- package/harness/codex/CHARTER.md +3 -3
- package/harness/lib/composer/compose.test.ts +11 -0
- package/harness/lib/composer/fragments/02-delegation.md +2 -1
- package/harness/lib/composer/fragments/04-task-flow.md +42 -2
- package/harness/lib/composer/fragments/08-routing.md +1 -1
- package/harness/lib/composer/fragments/09-guardrails.md +17 -4
- package/harness/lib/composer/index.ts +1 -1
- package/harness/lib/guards/guard-config.ts +72 -0
- package/harness/lib/hooks/builtins/confidence-gate-hook.ts +2 -4
- package/harness/lib/hooks/builtins/delegation-depth-hook.ts +23 -4
- package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -0
- package/harness/lib/hooks/builtins/next-route-hook.ts +24 -0
- package/harness/lib/hooks/builtins/plan-check-hook.ts +2 -2
- package/harness/lib/hooks/builtins/route-tracking-hook.ts +79 -25
- package/harness/lib/hooks/hooks.test.ts +117 -205
- package/harness/lib/hooks/index.ts +38 -30
- package/harness/lib/hooks/registry.ts +309 -416
- package/harness/lib/hooks/types.ts +116 -71
- package/harness/lib/plans/plan-location.ts +134 -0
- package/harness/lib/routing/index.ts +21 -0
- package/harness/lib/routing/route-guidance.ts +147 -0
- package/harness/lib/routing/route-resolver.ts +58 -0
- package/harness/lib/routing/routing.test.ts +195 -0
- package/harness/lib/routing/skill-frontmatter.ts +125 -0
- package/harness/lib/routing/types.ts +52 -0
- package/harness/skills/oh-ascii/SKILL.md +1 -1
- package/harness/skills/oh-fusion/DEEP.md +56 -33
- package/harness/skills/oh-fusion/SKILL.md +30 -16
- package/harness/skills/oh-init/DEEP.md +2 -2
- package/harness/skills/oh-manifest/SKILL.md +1 -0
- package/harness/skills/oh-plan-review/DEEP.md +1 -1
- package/harness/skills/oh-planner/DEEP.md +3 -3
- package/harness/skills/oh-review/DEEP.md +2 -0
- package/harness/skills/oh-review/SKILL.md +1 -0
- package/package.json +56 -55
- package/harness/lib/background/background.test.ts +0 -197
- package/harness/lib/background/index.ts +0 -7
- package/harness/lib/background/interfaces.ts +0 -31
- package/harness/lib/background/manager.ts +0 -320
- package/harness/lib/hooks/builtins/error-recovery-hook.ts +0 -107
- package/harness/lib/hooks/builtins/memory-sync-hook.ts +0 -73
- package/harness/lib/hooks/builtins/sanity-check-hook.ts +0 -52
- package/harness/lib/memory/index.ts +0 -18
- package/harness/lib/memory/interfaces.ts +0 -53
- package/harness/lib/memory/memory-manager.ts +0 -205
- package/harness/lib/memory/memory.test.ts +0 -491
- package/harness/lib/memory/plan-store.ts +0 -366
- package/harness/lib/recovery/handler.ts +0 -243
- package/harness/lib/recovery/index.ts +0 -14
- package/harness/lib/recovery/interfaces.ts +0 -48
- package/harness/lib/recovery/patterns.ts +0 -149
- package/harness/lib/recovery/recovery.test.ts +0 -312
- package/harness/lib/sanity/anomaly-tracker.ts +0 -127
- package/harness/lib/sanity/checker.ts +0 -178
- package/harness/lib/sanity/index.ts +0 -13
- package/harness/lib/sanity/interfaces.ts +0 -24
- package/harness/lib/sanity/sanity.test.ts +0 -472
- package/harness/lib/sync/file-watcher.ts +0 -174
- package/harness/lib/sync/index.ts +0 -11
- package/harness/lib/sync/interfaces.ts +0 -27
- package/harness/lib/sync/plan-sync.ts +0 -536
- package/harness/lib/sync/sync.test.ts +0 -832
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
|
|
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`,
|
|
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="
|
|
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
|
|
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
|
-
| **
|
|
42
|
-
| **
|
|
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
|
-
| **
|
|
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 —
|
|
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
|
-
│ │ ├──
|
|
155
|
-
│ │ └──
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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?.
|
|
354
|
-
if (hooksConfig?.
|
|
355
|
-
|
|
356
|
-
|
|
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/
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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:
|
|
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
|
-
??
|
|
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:
|
|
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
|
|
564
|
-
|
|
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
|
-
|
|
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
|
-
|
|
648
|
-
|
|
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.
|