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.
- package/CONTEXT.md +6 -6
- package/ETHOS.md +2 -2
- package/README.md +11 -17
- package/bootstrap.ts +118 -126
- 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 +35 -40
- package/harness/codex/CHARTER.md +3 -3
- package/harness/lib/composer/compose.test.ts +29 -29
- package/harness/lib/composer/fragments/02-delegation.md +5 -5
- package/harness/lib/composer/fragments/04-task-flow.md +13 -13
- package/harness/lib/composer/fragments/08-routing.md +1 -1
- package/harness/lib/composer/fragments/09-guardrails.md +25 -25
- package/harness/lib/composer/index.ts +1 -1
- package/harness/lib/guards/guard-config.ts +72 -72
- package/harness/lib/hooks/builtins/confidence-gate-hook.ts +9 -9
- package/harness/lib/hooks/builtins/delegation-depth-hook.ts +1 -1
- package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -99
- package/harness/lib/hooks/builtins/next-route-hook.ts +24 -24
- package/harness/lib/hooks/builtins/plan-check-hook.ts +5 -5
- package/harness/lib/hooks/builtins/route-tracking-hook.ts +1 -1
- package/harness/lib/hooks/hooks.test.ts +160 -324
- package/harness/lib/hooks/index.ts +38 -42
- package/harness/lib/hooks/registry.ts +309 -416
- package/harness/lib/hooks/types.ts +116 -119
- package/harness/lib/plans/plan-location.ts +134 -134
- package/harness/lib/routing/index.ts +21 -21
- package/harness/lib/routing/route-guidance.ts +147 -147
- package/harness/lib/routing/route-resolver.ts +58 -58
- package/harness/lib/routing/routing.test.ts +195 -195
- package/harness/lib/routing/skill-frontmatter.ts +125 -125
- package/harness/lib/routing/types.ts +52 -52
- package/harness/skills/oh-ascii/SKILL.md +1 -1
- package/harness/skills/oh-fusion/DEEP.md +109 -109
- package/harness/skills/oh-fusion/SKILL.md +47 -47
- package/harness/skills/oh-init/DEEP.md +2 -2
- 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 +5 -5
- package/package.json +56 -53
- package/harness/lib/background/background.test.ts +0 -216
- 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/hooks/builtins/subagent-failure-hook.ts +0 -93
- 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 -485
- package/harness/lib/memory/plan-store.ts +0 -346
- 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 -189
- 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 -175
- package/harness/lib/sync/index.ts +0 -11
- package/harness/lib/sync/interfaces.ts +0 -27
- package/harness/lib/sync/plan-sync.ts +0 -533
- 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="
|
|
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
|
|
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
|
-
| **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
|
-
| **
|
|
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
|
-
│ │
|
|
155
|
-
│
|
|
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
|
-
|
|
21
|
-
|
|
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?.
|
|
254
|
-
if (hooksConfig?.
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
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
|
-
//
|
|
555
|
-
if (postToolResult.
|
|
556
|
-
await logToOC("warn",
|
|
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` |
|