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