openhermes 4.9.2 → 4.12.1
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 +7 -7
- package/ETHOS.md +2 -2
- package/README.md +34 -33
- package/bootstrap.ts +310 -160
- package/harness/agents/oh-planner.md +1 -1
- package/harness/agents/openhermes.md +27 -126
- package/harness/codex/AUTOPILOT.md +131 -23
- package/harness/codex/CHARTER.md +4 -5
- package/harness/lib/background/background.test.ts +216 -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 +179 -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 +7 -0
- package/harness/lib/composer/fragments/03-permissions.md +13 -0
- package/harness/lib/composer/fragments/04-task-flow.md +55 -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 +25 -0
- package/harness/lib/composer/index.ts +1 -0
- package/harness/lib/guards/guard-config.ts +72 -0
- package/harness/lib/hooks/builtins/confidence-gate-hook.ts +68 -0
- package/harness/lib/hooks/builtins/delegation-depth-hook.ts +78 -0
- package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -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/next-route-hook.ts +24 -0
- package/harness/lib/hooks/builtins/plan-check-hook.ts +43 -0
- package/harness/lib/hooks/builtins/route-tracking-hook.ts +201 -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/builtins/subagent-failure-hook.ts +93 -0
- package/harness/lib/hooks/hooks.test.ts +1092 -0
- package/harness/lib/hooks/index.ts +42 -0
- package/harness/lib/hooks/registry.ts +416 -0
- package/harness/lib/hooks/types.ts +119 -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 +485 -0
- package/harness/lib/memory/plan-store.ts +346 -0
- package/harness/lib/plans/plan-location.ts +134 -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/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/lib/sanity/anomaly-tracker.ts +127 -0
- package/harness/lib/sanity/checker.ts +189 -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 +175 -0
- package/harness/lib/sync/index.ts +11 -0
- package/harness/lib/sync/interfaces.ts +27 -0
- package/harness/lib/sync/plan-sync.ts +533 -0
- package/harness/lib/sync/sync.test.ts +858 -0
- package/harness/skills/oh-fusion/DEEP.md +109 -86
- package/harness/skills/oh-fusion/SKILL.md +47 -33
- package/harness/skills/oh-init/DEEP.md +2 -2
- package/harness/skills/oh-manifest/SKILL.md +2 -1
- 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 -3
- package/harness/skills/oh-review/SKILL.md +1 -0
- package/harness/skills/oh-ship/SKILL.md +1 -1
- package/harness/skills/oh-skill-craft/SKILL.md +1 -4
- package/package.json +53 -55
- 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/bootstrap.ts
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
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"
|
|
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
|
+
|
|
10
|
+
// Hook system — pluggable lifecycle hooks with topological sort
|
|
11
|
+
import {
|
|
12
|
+
HookRegistry,
|
|
13
|
+
HookResult,
|
|
14
|
+
nextRouteHook,
|
|
15
|
+
planCheckHook,
|
|
16
|
+
shellDetectHook,
|
|
17
|
+
confidenceGateHook,
|
|
18
|
+
delegationDepthHook,
|
|
19
|
+
resetDepthTracker,
|
|
20
|
+
errorRecoveryHook,
|
|
21
|
+
memorySyncHook,
|
|
22
|
+
sanityCheckHook,
|
|
23
|
+
dynamicRouteHook,
|
|
24
|
+
routeTrackingHook,
|
|
25
|
+
subagentFailureHook,
|
|
26
|
+
resetSubagentFailures,
|
|
27
|
+
DEFAULT_GUARD_CONFIG,
|
|
28
|
+
} from "./harness/lib/hooks/index.ts"
|
|
29
|
+
import type { HookContext } from "./harness/lib/hooks/index.ts"
|
|
6
30
|
|
|
7
31
|
const OPENHERMES_AGENT = "OpenHermes"
|
|
8
32
|
|
|
@@ -13,19 +37,7 @@ const USER_SKILL_DIRS: ReadonlyArray<string> = [
|
|
|
13
37
|
path.join(os.homedir(), ".claude", "skills"), // Claude Code backward compat
|
|
14
38
|
]
|
|
15
39
|
|
|
16
|
-
|
|
17
|
-
let _planStorageOverride: string | undefined
|
|
18
|
-
export function setPlanStorageDirForTest(dir: string | undefined): void { _planStorageOverride = dir }
|
|
19
|
-
function planStorageDir(): string {
|
|
20
|
-
return _planStorageOverride ?? path.join(os.homedir(), ".local", "share", "opencode", "openhermes", "plans")
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function getProjectName(projectDir: string): string {
|
|
24
|
-
return path.basename(projectDir)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir, ensurePlanFile }
|
|
40
|
+
export { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir, ensurePlanFile, findLatestPlanFile, setPlanStorageDirForTest }
|
|
29
41
|
|
|
30
42
|
function parseFrontmatter(raw: string | undefined): Record<string, string> {
|
|
31
43
|
const frontmatter: Record<string, string> = {}
|
|
@@ -40,12 +52,12 @@ function parseFrontmatter(raw: string | undefined): Record<string, string> {
|
|
|
40
52
|
return frontmatter
|
|
41
53
|
}
|
|
42
54
|
|
|
43
|
-
interface MarkdownDocument {
|
|
44
|
-
frontmatter: Record<string, string>
|
|
45
|
-
body: string
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function readMarkdownDocument(filePath: string): MarkdownDocument | null {
|
|
55
|
+
interface MarkdownDocument {
|
|
56
|
+
frontmatter: Record<string, string>
|
|
57
|
+
body: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readMarkdownDocument(filePath: string): MarkdownDocument | null {
|
|
49
61
|
if (!fs.existsSync(filePath)) return null
|
|
50
62
|
const source = fs.readFileSync(filePath, "utf8")
|
|
51
63
|
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/)
|
|
@@ -71,7 +83,7 @@ function readMarkdownDirectory(dir: string): DirEntry[] {
|
|
|
71
83
|
.filter((e): e is DirEntry => e !== null)
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
interface CommandDef {
|
|
86
|
+
interface CommandDef {
|
|
75
87
|
description: string
|
|
76
88
|
template: string
|
|
77
89
|
agent?: string
|
|
@@ -94,7 +106,7 @@ function commandDefinitions(dir: string): Record<string, CommandDef> {
|
|
|
94
106
|
return commands
|
|
95
107
|
}
|
|
96
108
|
|
|
97
|
-
interface AgentDef {
|
|
109
|
+
interface AgentDef {
|
|
98
110
|
description: string
|
|
99
111
|
mode: string
|
|
100
112
|
prompt: string
|
|
@@ -125,119 +137,27 @@ function uniqueStrings(existing: string[] = [], additions: string[] = []): strin
|
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
|
|
128
|
-
function
|
|
129
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function findLatestPlanFile(projectDir: string): string | null {
|
|
133
|
-
const projectName = getProjectName(projectDir)
|
|
134
|
-
const storage = planStorageDir()
|
|
135
|
-
if (!fs.existsSync(storage)) return null
|
|
136
|
-
const pattern = new RegExp(`^${regexEscape(projectName)}-plan-(\\d{3})\\.md$`)
|
|
137
|
-
let latest: string | null = null
|
|
138
|
-
let highest = -1
|
|
140
|
+
function ensureDir(dir: string): void {
|
|
139
141
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (m) {
|
|
143
|
-
const n = parseInt(m[1], 10)
|
|
144
|
-
if (n > highest) {
|
|
145
|
-
highest = n
|
|
146
|
-
latest = path.join(storage, entry)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
142
|
+
if (!fs.existsSync(dir)) {
|
|
143
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
149
144
|
}
|
|
150
|
-
} catch {
|
|
151
|
-
|
|
145
|
+
} catch (err) {
|
|
146
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
147
|
+
console.error(`[openhermes] Failed to create directory ${dir}: ${msg}`)
|
|
148
|
+
// Don't throw — let the plan system degrade gracefully
|
|
152
149
|
}
|
|
153
|
-
return latest
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function readPlanFromFile(filePath: string): string | null {
|
|
157
|
-
if (!fs.existsSync(filePath)) return null
|
|
158
|
-
const source = fs.readFileSync(filePath, "utf8")
|
|
159
|
-
const status = source.match(/^Status:\s*(.+)$/m)?.[1]?.trim()
|
|
160
|
-
const objective = source.match(/^Objective:\s*(.+)$/m)?.[1]?.trim()
|
|
161
|
-
if (!status && !objective) return null
|
|
162
|
-
const parts = [status ? `status=${status}` : null, objective ? `objective=${objective}` : null].filter(Boolean)
|
|
163
|
-
return `Active plan: ${parts.join(" | ")}`
|
|
164
150
|
}
|
|
165
151
|
|
|
166
|
-
function
|
|
167
|
-
const planFile = findLatestPlanFile(projectDir)
|
|
168
|
-
if (!planFile) return null
|
|
169
|
-
return readPlanFromFile(planFile)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function ensureDir(dir: string): void {
|
|
173
|
-
if (!fs.existsSync(dir)) {
|
|
174
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Ensure a plan file exists for the project.
|
|
180
|
-
* Creates a skeleton plan if none exists or if the latest is complete/abandoned.
|
|
181
|
-
* Reuses an existing active or in-progress plan.
|
|
182
|
-
* Returns the path to the plan file.
|
|
183
|
-
*/
|
|
184
|
-
function ensurePlanFile(projectDir: string): string {
|
|
185
|
-
const projectName = getProjectName(projectDir)
|
|
186
|
-
const storage = planStorageDir()
|
|
187
|
-
ensureDir(storage)
|
|
188
|
-
|
|
189
|
-
// Reuse active or in-progress plan
|
|
190
|
-
const latest = findLatestPlanFile(projectDir)
|
|
191
|
-
if (latest) {
|
|
192
|
-
const content = fs.readFileSync(latest, "utf8")
|
|
193
|
-
const status = content.match(/^Status:\s*(.+)$/m)?.[1]?.trim()
|
|
194
|
-
if (status === "active" || status === "in-progress") {
|
|
195
|
-
return latest
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Determine next sequence number
|
|
200
|
-
let nextSeq = 1
|
|
201
|
-
if (latest) {
|
|
202
|
-
const m = path.basename(latest).match(/-plan-(\d{3})\.md$/)
|
|
203
|
-
if (m) nextSeq = parseInt(m[1], 10) + 1
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const planId = `${projectName}-plan-${String(nextSeq).padStart(3, "0")}`
|
|
207
|
-
const planPath = path.join(storage, `${planId}.md`)
|
|
208
|
-
const now = new Date().toISOString().replace("T", " ").slice(0, 16)
|
|
209
|
-
|
|
210
|
-
const content = [
|
|
211
|
-
`# PLAN: ${projectName}`,
|
|
212
|
-
"",
|
|
213
|
-
`Plan ID: ${planId}`,
|
|
214
|
-
`Project: ${projectName}`,
|
|
215
|
-
`Status: active`,
|
|
216
|
-
`Created: ${now}`,
|
|
217
|
-
`Updated: ${now}`,
|
|
218
|
-
`Project Path: ${projectDir}`,
|
|
219
|
-
`Plan Path: ${planPath}`,
|
|
220
|
-
`Objective: (pending classification)`,
|
|
221
|
-
"",
|
|
222
|
-
"## Tasks",
|
|
223
|
-
"",
|
|
224
|
-
"- [ ] (discoverable — pending classification)",
|
|
225
|
-
"",
|
|
226
|
-
].join("\n")
|
|
227
|
-
|
|
228
|
-
fs.writeFileSync(planPath, content, "utf8")
|
|
229
|
-
return planPath
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export function buildCompactionContext(projectDir: string): string[] {
|
|
152
|
+
export function buildCompactionContext(projectDir: string): string[] {
|
|
233
153
|
const context = [
|
|
234
154
|
"OpenHermes: native-first, verify before claim, always delegate, concise over verbose.",
|
|
235
155
|
"Preserve domain terms: skill, command, agent, bootstrap, compaction.",
|
|
236
156
|
"Preserve blockers, current task, and next steps; do not invent durable state.",
|
|
237
157
|
]
|
|
238
158
|
|
|
239
|
-
const planSummary =
|
|
240
|
-
if (planSummary) context.push(planSummary)
|
|
159
|
+
const planSummary = resolvePlanAccess(projectDir)?.summary
|
|
160
|
+
if (planSummary) context.push(planSummary)
|
|
241
161
|
|
|
242
162
|
return context
|
|
243
163
|
}
|
|
@@ -277,6 +197,7 @@ interface OpenHermesConfig {
|
|
|
277
197
|
agent?: Record<string, unknown>
|
|
278
198
|
instructions?: string[]
|
|
279
199
|
default_agent?: string
|
|
200
|
+
[key: string]: unknown // allow additional SDK properties (experimental, etc.)
|
|
280
201
|
}
|
|
281
202
|
|
|
282
203
|
export const BootstrapPlugin: Plugin = async (ctx) => {
|
|
@@ -298,17 +219,54 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
|
|
|
298
219
|
// Auto-detect and wire user skills from ~/.agents/skills and ~/.config/opencode/skills
|
|
299
220
|
const userSkillPaths: string[] = []
|
|
300
221
|
for (const userDir of USER_SKILL_DIRS) {
|
|
301
|
-
ensureDir(userDir)
|
|
222
|
+
try { ensureDir(userDir) } catch {}
|
|
302
223
|
userSkillPaths.push(userDir)
|
|
303
224
|
await logToOC("info", `wired user skills from ${userDir}`)
|
|
304
225
|
}
|
|
305
226
|
|
|
306
227
|
const compactionContext = buildCompactionContext(ctx.directory)
|
|
307
228
|
// Ensure plan storage exists
|
|
308
|
-
ensureDir(planStorageDir())
|
|
229
|
+
try { ensureDir(planStorageDir()) } catch {}
|
|
230
|
+
|
|
231
|
+
|
|
309
232
|
|
|
310
233
|
return {
|
|
311
234
|
config: async (config: OpenHermesConfig) => {
|
|
235
|
+
|
|
236
|
+
// ── 1. Hooks System ─────────────────────────────────────────────────
|
|
237
|
+
// Read experimental.hooks config from the raw config object
|
|
238
|
+
const experimental = config.experimental as Record<string, unknown> | undefined;
|
|
239
|
+
const hooksConfig = (experimental?.hooks as
|
|
240
|
+
| Record<string, boolean>
|
|
241
|
+
| undefined)
|
|
242
|
+
const hooksEnabled = (hooksConfig?.enabled ?? true) as boolean
|
|
243
|
+
|
|
244
|
+
if (hooksEnabled) {
|
|
245
|
+
const reg = HookRegistry.getInstance()
|
|
246
|
+
|
|
247
|
+
// Check individual hook flags (default: true if not specified)
|
|
248
|
+
if (hooksConfig?.plan_check ?? true) reg.registerPreTool(planCheckHook)
|
|
249
|
+
if (hooksConfig?.shell_detect ?? true) reg.registerPreTool(shellDetectHook)
|
|
250
|
+
if (hooksConfig?.delegation_depth ?? true) reg.registerPreTool(delegationDepthHook)
|
|
251
|
+
reg.registerRoute(nextRouteHook)
|
|
252
|
+
if (hooksConfig?.confidence_gate ?? true) reg.registerRoute(confidenceGateHook)
|
|
253
|
+
if (hooksConfig?.error_recovery ?? true) reg.registerPostTool(errorRecoveryHook)
|
|
254
|
+
if (hooksConfig?.memory_sync ?? true) reg.registerPostTool(memorySyncHook)
|
|
255
|
+
if (hooksConfig?.sanity_check ?? true) reg.registerPostTool(sanityCheckHook)
|
|
256
|
+
if (hooksConfig?.dynamic_route ?? true) reg.registerPostTool(dynamicRouteHook)
|
|
257
|
+
if (hooksConfig?.route_tracking ?? true) {
|
|
258
|
+
reg.registerRoute(routeTrackingHook)
|
|
259
|
+
} else {
|
|
260
|
+
reg.unregister("route-tracking")
|
|
261
|
+
}
|
|
262
|
+
if (hooksConfig?.subagent_failure ?? true) reg.registerPostTool(subagentFailureHook)
|
|
263
|
+
|
|
264
|
+
await logToOC("info", `hooks: ${reg.getPreToolHooks().length + reg.getPostToolHooks().length + reg.getRouteHooks().length} registered`)
|
|
265
|
+
} else {
|
|
266
|
+
await logToOC("info", "hooks: disabled via config")
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── 2. Skills ──────────────────────────────────────────────────────
|
|
312
270
|
config.skills = config.skills || {}
|
|
313
271
|
// Built-in paths first, user paths last → user skills override built-in on name conflict
|
|
314
272
|
const allPaths = [skillsDir, ...userSkillPaths]
|
|
@@ -325,10 +283,17 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
|
|
|
325
283
|
config.command = { ...(config.command ?? {}), ...commandDefinitions(commandsDir) }
|
|
326
284
|
|
|
327
285
|
const loadedAgents = agentDefinitions(agentsDir)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
286
|
+
// Use composer for the OpenHermes agent prompt — assemble from fragments
|
|
287
|
+
let openHermesPrompt: string
|
|
288
|
+
try {
|
|
289
|
+
openHermesPrompt = compose()
|
|
290
|
+
} catch {
|
|
291
|
+
openHermesPrompt = loadedAgents[OPENHERMES_AGENT]?.prompt ?? "You are OpenHermes."
|
|
292
|
+
}
|
|
293
|
+
const openHermesAgent = {
|
|
294
|
+
description: loadedAgents[OPENHERMES_AGENT]?.description ?? "OpenHermes primary orchestrator",
|
|
295
|
+
mode: loadedAgents[OPENHERMES_AGENT]?.mode ?? "primary",
|
|
296
|
+
prompt: openHermesPrompt,
|
|
332
297
|
}
|
|
333
298
|
|
|
334
299
|
// Subagent permissions — tier-4 and tier-3 get execution access but cannot spawn orchestrators
|
|
@@ -336,18 +301,18 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
|
|
|
336
301
|
"oh-builder": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
337
302
|
"oh-browser": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
338
303
|
"oh-facade": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
304
|
+
"oh-fusion": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
339
305
|
"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
306
|
"oh-grill": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
344
307
|
"oh-investigate": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
308
|
+
"oh-manifest": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
345
309
|
"oh-plan-review": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
346
|
-
"oh-
|
|
310
|
+
"oh-planner": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
347
311
|
"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
312
|
"oh-retro": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
313
|
+
"oh-review": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
314
|
+
"oh-security": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
315
|
+
"oh-ship": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
351
316
|
"oh-skill-craft": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
352
317
|
}
|
|
353
318
|
|
|
@@ -385,11 +350,11 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
|
|
|
385
350
|
webfetch: "allow", // CAN fetch docs for context
|
|
386
351
|
question: "allow", // CAN ask user questions
|
|
387
352
|
websearch: "allow", // CAN search web for research context
|
|
388
|
-
external_directory: { // CAN read/write plan files outside worktree
|
|
389
|
-
"~/.local/share/
|
|
390
|
-
},
|
|
391
|
-
},
|
|
392
|
-
},
|
|
353
|
+
external_directory: { // CAN read/write plan files outside worktree
|
|
354
|
+
"~/.local/share/openhermes/plans/**": "allow",
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
393
358
|
}
|
|
394
359
|
|
|
395
360
|
config.default_agent = OPENHERMES_AGENT
|
|
@@ -405,9 +370,10 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
|
|
|
405
370
|
// creates plans on demand (see Task Flow step 1 in agent prompt).
|
|
406
371
|
// Auto-creation produced ghost skeletons like plan-004.
|
|
407
372
|
|
|
408
|
-
// Reset delegation depth on session start/error
|
|
373
|
+
// Reset delegation depth and subagent failures on session start/error
|
|
409
374
|
if (typed.type === "session.created" || typed.type === "session.error") {
|
|
410
|
-
|
|
375
|
+
resetDepthTracker()
|
|
376
|
+
resetSubagentFailures()
|
|
411
377
|
}
|
|
412
378
|
},
|
|
413
379
|
|
|
@@ -415,22 +381,208 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
|
|
|
415
381
|
output.context.push(...compactionContext)
|
|
416
382
|
},
|
|
417
383
|
|
|
418
|
-
//
|
|
384
|
+
// Hook-enabled tool execution — delegates to HookRegistry for lifecycle hooks
|
|
419
385
|
"tool.execute.before": async (input, output) => {
|
|
420
386
|
if (input.tool === "task") {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
387
|
+
const reg = HookRegistry.getInstance()
|
|
388
|
+
|
|
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
|
|
393
|
+
|
|
394
|
+
// Build hook context from input and current session state
|
|
395
|
+
const hookContext: HookContext = {
|
|
396
|
+
sessionId: ctx.directory, // project directory as session key
|
|
397
|
+
agent: agentName,
|
|
398
|
+
directory: ctx.directory,
|
|
399
|
+
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
|
+
}
|
|
406
|
+
|
|
407
|
+
// Run all registered PreToolUse hooks (plan check, shell detect, delegation depth)
|
|
408
|
+
let preToolResult: { result: HookResult; modifiedContext?: HookContext }
|
|
409
|
+
try {
|
|
410
|
+
preToolResult = await reg.executePreTool(hookContext)
|
|
411
|
+
} catch (err) {
|
|
412
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
413
|
+
const errOutput = output as { args: unknown; isError?: boolean; content?: unknown[] }
|
|
414
|
+
errOutput.isError = true
|
|
415
|
+
errOutput.content = [{ type: "text", text: `Hook error (PreTool): ${msg}` }]
|
|
416
|
+
return
|
|
417
|
+
}
|
|
425
418
|
|
|
426
|
-
if (
|
|
419
|
+
if (preToolResult.result === HookResult.STOP) {
|
|
420
|
+
// Depth exceeded or other stop condition
|
|
427
421
|
const errOutput = output as { args: unknown; isError?: boolean; content?: unknown[] }
|
|
428
422
|
errOutput.isError = true
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
423
|
+
const message = (preToolResult.modifiedContext?._depthError as string)
|
|
424
|
+
?? `LOOP GUARD: Delegation depth exceeded (max ${DEFAULT_GUARD_CONFIG.maxDelegationDepth}). ` +
|
|
425
|
+
"Surface to orchestrator with findings and stop delegating."
|
|
426
|
+
errOutput.content = [{ type: "text", text: message }]
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Handle INJECT from PreTool hooks (e.g. plan check wants a plan first)
|
|
431
|
+
if (preToolResult.result === HookResult.INJECT) {
|
|
432
|
+
const planInstruction = preToolResult.modifiedContext?._planCheckInstruction as string | undefined
|
|
433
|
+
if (planInstruction) {
|
|
434
|
+
// Plan check hook returned INJECT — inject "create plan" instruction
|
|
435
|
+
const inputAny = input as Record<string, unknown>
|
|
436
|
+
const existingPrompt = (inputAny.description as string) || (inputAny.prompt as string) || ""
|
|
437
|
+
if (inputAny.description) {
|
|
438
|
+
inputAny.description = `${planInstruction}\n\n${existingPrompt}`
|
|
439
|
+
} else if (inputAny.prompt) {
|
|
440
|
+
inputAny.prompt = `${planInstruction}\n\n${existingPrompt}`
|
|
441
|
+
} else {
|
|
442
|
+
inputAny.description = planInstruction
|
|
443
|
+
}
|
|
444
|
+
await logToOC("info", `Plan check: injected plan creation instruction into task for "${agentName}"`)
|
|
445
|
+
} else {
|
|
446
|
+
// Generic INJECT — hooks modified task context
|
|
447
|
+
await logToOC("debug", `PreTool INJECT: hooks modified task context for "${agentName}"`)
|
|
448
|
+
}
|
|
449
|
+
// Continue execution — INJECT is not a stop signal
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Run all registered RouteHooks — the agent/skill being delegated to IS the route
|
|
453
|
+
// This fires confidence-gate (inject confirm/question on MEDIUM/LOW confidence)
|
|
454
|
+
// and route-tracking (guard against infinite routing loops)
|
|
455
|
+
let routeResult: { result: HookResult; modifiedRoute?: string }
|
|
456
|
+
try {
|
|
457
|
+
routeResult = await reg.executeRoute(hookContext, agentName)
|
|
458
|
+
} catch (err) {
|
|
459
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
460
|
+
await logToOC("error", `Hook error (Route): ${msg}`)
|
|
461
|
+
// Route failed — don't change routing decision, just log
|
|
462
|
+
routeResult = { result: HookResult.CONTINUE }
|
|
463
|
+
}
|
|
464
|
+
|
|
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) {
|
|
486
|
+
// Confidence gate wants to inject a confirmation/pause into routing.
|
|
487
|
+
// Parse the modifiedRoute for markers and inject into task description/prompt.
|
|
488
|
+
const modifiedRoute: string = routeResult.modifiedRoute
|
|
489
|
+
const inputAny = input as Record<string, unknown>
|
|
490
|
+
const existingPrompt = (inputAny.description as string) || (inputAny.prompt as string) || ""
|
|
491
|
+
let gateMsg: string | undefined
|
|
492
|
+
|
|
493
|
+
if (modifiedRoute.includes("?echo=confirm")) {
|
|
494
|
+
gateMsg = "[CONFIDENCE: MEDIUM] Review your plan and confirm it before executing."
|
|
495
|
+
} else if (modifiedRoute.includes("?question=pause")) {
|
|
496
|
+
gateMsg = "[CONFIDENCE: LOW] Pause and ask the user for approval before proceeding."
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (gateMsg) {
|
|
500
|
+
if (inputAny.description) {
|
|
501
|
+
inputAny.description = `${gateMsg}\n${existingPrompt}`
|
|
502
|
+
} else if (inputAny.prompt) {
|
|
503
|
+
inputAny.prompt = `${gateMsg}\n${existingPrompt}`
|
|
504
|
+
} else {
|
|
505
|
+
inputAny.description = gateMsg
|
|
506
|
+
}
|
|
507
|
+
await logToOC("info", `Confidence gate: injected instruction into task to "${agentName}": ${gateMsg}`)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
// Hook-enabled post-execution — runs PostToolUse hooks
|
|
514
|
+
"tool.execute.after": async (input, output) => {
|
|
515
|
+
if (input.tool === "task") {
|
|
516
|
+
const reg = HookRegistry.getInstance()
|
|
517
|
+
|
|
518
|
+
// Access optional fields from input
|
|
519
|
+
const inputAny = input as Record<string, unknown>
|
|
520
|
+
const agentName = typeof inputAny.agent === "string" ? inputAny.agent : "unknown"
|
|
521
|
+
|
|
522
|
+
// 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
|
+
}
|
|
533
|
+
|
|
534
|
+
// Extract output text from tool result
|
|
535
|
+
// 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>
|
|
538
|
+
let outputText = ""
|
|
539
|
+
if (outputAny?.content) {
|
|
540
|
+
const content = outputAny.content
|
|
541
|
+
if (typeof content === "string") {
|
|
542
|
+
outputText = content
|
|
543
|
+
} else if (Array.isArray(content)) {
|
|
544
|
+
outputText = content
|
|
545
|
+
.map((c: Record<string, unknown>) => (typeof c.text === "string" ? c.text : ""))
|
|
546
|
+
.join("\n")
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Run all registered PostToolUse hooks
|
|
551
|
+
try {
|
|
552
|
+
const postToolResult = await reg.executePostTool(hookContext, outputText)
|
|
553
|
+
|
|
554
|
+
// Surface recovery instructions from errorRecoveryHook and/or sanityCheckHook
|
|
555
|
+
if (postToolResult.recovery) {
|
|
556
|
+
await logToOC("warn", `PostTool recovery instruction:\n${postToolResult.recovery}`)
|
|
557
|
+
}
|
|
558
|
+
|
|
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
|
+
} catch (err) {
|
|
583
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
584
|
+
await logToOC("error", `Hook error (PostTool): ${msg}`)
|
|
585
|
+
// Non-fatal — tool already executed
|
|
434
586
|
}
|
|
435
587
|
}
|
|
436
588
|
},
|
|
@@ -438,5 +590,3 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
|
|
|
438
590
|
}
|
|
439
591
|
}
|
|
440
592
|
|
|
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
|
|