openhermes 4.3.0 → 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 +10 -1
- package/README.md +54 -42
- package/bootstrap.ts +396 -142
- package/harness/agents/oh-browser.md +97 -0
- package/harness/agents/oh-builder.md +78 -0
- package/harness/agents/oh-facade.md +75 -0
- package/harness/agents/oh-fusion.md +45 -0
- package/harness/agents/oh-gauntlet.md +71 -0
- package/harness/agents/oh-grill.md +71 -0
- package/harness/agents/oh-investigate.md +60 -0
- package/harness/agents/oh-manifest.md +95 -0
- package/harness/agents/oh-plan-review.md +40 -0
- package/harness/agents/oh-planner.md +50 -0
- package/harness/agents/oh-refactor.md +37 -0
- package/harness/agents/oh-retro.md +46 -0
- package/harness/agents/oh-review.md +85 -0
- package/harness/agents/oh-security.md +83 -0
- package/harness/agents/oh-ship.md +76 -0
- package/harness/agents/oh-skill-craft.md +38 -0
- package/harness/agents/openhermes.md +28 -73
- package/harness/codex/AUTOPILOT.md +235 -87
- package/harness/codex/CHARTER.md +80 -0
- package/harness/instructions/SHELL.md +76 -0
- 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-ascii/DEEP.md +292 -0
- package/harness/skills/oh-ascii/SKILL.md +31 -0
- package/harness/skills/oh-ascii/scripts/check_ascii_alignment.py +596 -0
- package/harness/skills/oh-browser/DEEP.md +54 -0
- package/harness/skills/oh-browser/SKILL.md +30 -0
- package/harness/skills/oh-builder/DEEP.md +63 -0
- package/harness/skills/oh-builder/SKILL.md +12 -90
- package/harness/skills/oh-expert/DEEP.md +85 -0
- package/harness/skills/oh-expert/SKILL.md +13 -106
- package/harness/skills/oh-facade/DEEP.md +182 -0
- package/harness/skills/oh-facade/SKILL.md +15 -279
- package/harness/skills/oh-freeze/DEEP.md +18 -0
- package/harness/skills/oh-freeze/SKILL.md +10 -19
- package/harness/skills/oh-full-output/DEEP.md +25 -0
- package/harness/skills/oh-full-output/SKILL.md +12 -65
- package/harness/skills/oh-fusion/DEEP.md +120 -0
- package/harness/skills/oh-fusion/SKILL.md +17 -295
- package/harness/skills/oh-gauntlet/DEEP.md +77 -0
- package/harness/skills/oh-gauntlet/SKILL.md +13 -105
- package/harness/skills/oh-grill/DEEP.md +51 -0
- package/harness/skills/oh-grill/SKILL.md +12 -63
- package/harness/skills/oh-guard/DEEP.md +19 -0
- package/harness/skills/oh-guard/SKILL.md +10 -24
- package/harness/skills/oh-handoff/DEEP.md +48 -0
- package/harness/skills/oh-handoff/SKILL.md +13 -23
- package/harness/skills/oh-health/DEEP.md +74 -0
- package/harness/skills/oh-health/SKILL.md +13 -76
- package/harness/skills/oh-init/DEEP.md +85 -0
- package/harness/skills/oh-init/SKILL.md +13 -127
- package/harness/skills/oh-investigate/DEEP.md +171 -0
- package/harness/skills/oh-investigate/SKILL.md +13 -66
- package/harness/skills/oh-issue/DEEP.md +21 -0
- package/harness/skills/oh-issue/SKILL.md +11 -27
- package/harness/skills/oh-manifest/DEEP.md +92 -0
- package/harness/skills/oh-manifest/SKILL.md +12 -109
- package/harness/skills/oh-plan-review/DEEP.md +90 -0
- package/harness/skills/oh-plan-review/SKILL.md +13 -115
- package/harness/skills/oh-planner/DEEP.md +172 -0
- package/harness/skills/oh-planner/SKILL.md +12 -149
- package/harness/skills/oh-prd/DEEP.md +45 -0
- package/harness/skills/oh-prd/SKILL.md +10 -26
- package/harness/skills/oh-refactor/DEEP.md +122 -0
- package/harness/skills/oh-refactor/SKILL.md +17 -410
- package/harness/skills/oh-retro/DEEP.md +26 -0
- package/harness/skills/oh-retro/SKILL.md +12 -24
- package/harness/skills/oh-review/DEEP.md +87 -0
- package/harness/skills/oh-review/SKILL.md +11 -97
- package/harness/skills/oh-security/DEEP.md +83 -0
- package/harness/skills/oh-security/SKILL.md +14 -96
- package/harness/skills/oh-ship/DEEP.md +141 -0
- package/harness/skills/oh-ship/SKILL.md +14 -32
- package/harness/skills/oh-skill-craft/DEEP.md +369 -0
- package/harness/skills/oh-skill-craft/SKILL.md +13 -177
- package/harness/skills/oh-skills-link/DEEP.md +16 -0
- package/harness/skills/oh-skills-link/SKILL.md +10 -20
- package/harness/skills/oh-skills-list/DEEP.md +20 -0
- package/harness/skills/oh-skills-list/SKILL.md +9 -22
- package/harness/skills/oh-triage/DEEP.md +23 -0
- package/harness/skills/oh-triage/SKILL.md +8 -24
- package/harness/skills/oh-worktree/DEEP.md +169 -0
- package/harness/skills/oh-worktree/SKILL.md +32 -0
- package/lib/harness-resolver.ts +8 -10
- package/package.json +7 -5
- package/tsconfig.json +1 -1
- package/harness/codex/CONSTITUTION.md +0 -73
- package/harness/codex/ROUTING.md +0 -92
- package/harness/commands/oh-doctor.md +0 -26
- package/harness/commands/oh-log.md +0 -18
- package/harness/instructions/RUNTIME.md +0 -30
- package/harness/skills/oh-caveman/SKILL.md +0 -42
- package/harness/skills/oh-learn/SKILL.md +0 -101
- package/lib/logger.ts +0 -75
package/bootstrap.ts
CHANGED
|
@@ -1,35 +1,46 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
2
|
import fs from "node:fs"
|
|
3
3
|
import os from "node:os"
|
|
4
|
-
import { fileURLToPath } from "node:url"
|
|
5
4
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
6
|
-
import { createLogger } from "./lib/logger.ts"
|
|
7
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"
|
|
8
22
|
|
|
9
|
-
const log = createLogger("bootstrap")
|
|
10
|
-
const sessionLog = createLogger("session")
|
|
11
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
-
const BOOTSTRAP_MARKER = "OPENHERMES_BOOTSTRAP"
|
|
13
23
|
const OPENHERMES_AGENT = "OpenHermes"
|
|
14
24
|
|
|
25
|
+
// User skill directories — auto-discovered on every session, survive npm updates
|
|
26
|
+
const USER_SKILL_DIRS: ReadonlyArray<string> = [
|
|
27
|
+
path.join(os.homedir(), ".agents", "skills"),
|
|
28
|
+
path.join(os.homedir(), ".config", "opencode", "skills"),
|
|
29
|
+
path.join(os.homedir(), ".claude", "skills"), // Claude Code backward compat
|
|
30
|
+
]
|
|
31
|
+
|
|
15
32
|
// Canonical storage under OpenCode's data directory — survives npm updates
|
|
16
33
|
let _planStorageOverride: string | undefined
|
|
17
34
|
export function setPlanStorageDirForTest(dir: string | undefined): void { _planStorageOverride = dir }
|
|
18
35
|
function planStorageDir(): string {
|
|
19
|
-
return _planStorageOverride ?? path.join(os.homedir(), ".local", "share", "
|
|
36
|
+
return _planStorageOverride ?? path.join(os.homedir(), ".local", "share", "openhermes", "plans")
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
function getProjectName(projectDir: string): string {
|
|
23
40
|
return path.basename(projectDir)
|
|
24
41
|
}
|
|
25
42
|
|
|
26
|
-
|
|
27
|
-
const USER_SKILL_DIRS: ReadonlyArray<string> = [
|
|
28
|
-
path.join(os.homedir(), ".agents", "skills"),
|
|
29
|
-
path.join(os.homedir(), ".config", "opencode", "skills"),
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
export { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir }
|
|
43
|
+
export { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir, ensurePlanFile, findLatestPlanFile }
|
|
33
44
|
|
|
34
45
|
function parseFrontmatter(raw: string | undefined): Record<string, string> {
|
|
35
46
|
const frontmatter: Record<string, string> = {}
|
|
@@ -128,29 +139,22 @@ function uniqueStrings(existing: string[] = [], additions: string[] = []): strin
|
|
|
128
139
|
return merged
|
|
129
140
|
}
|
|
130
141
|
|
|
131
|
-
function readText(filePath: string): string {
|
|
132
|
-
return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : ""
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function regexEscape(s: string): string {
|
|
136
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
137
|
-
}
|
|
138
142
|
|
|
139
143
|
function findLatestPlanFile(projectDir: string): string | null {
|
|
140
144
|
const projectName = getProjectName(projectDir)
|
|
141
145
|
const storage = planStorageDir()
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
const projectDirPath = path.join(storage, projectName)
|
|
147
|
+
if (!fs.existsSync(projectDirPath)) return null
|
|
144
148
|
let latest: string | null = null
|
|
145
149
|
let highest = -1
|
|
146
150
|
try {
|
|
147
|
-
for (const entry of fs.readdirSync(
|
|
148
|
-
const m = entry.match(
|
|
151
|
+
for (const entry of fs.readdirSync(projectDirPath)) {
|
|
152
|
+
const m = entry.match(/^plan-(\d{3})\.md$/)
|
|
149
153
|
if (m) {
|
|
150
154
|
const n = parseInt(m[1], 10)
|
|
151
155
|
if (n > highest) {
|
|
152
156
|
highest = n
|
|
153
|
-
latest = path.join(
|
|
157
|
+
latest = path.join(projectDirPath, entry)
|
|
154
158
|
}
|
|
155
159
|
}
|
|
156
160
|
}
|
|
@@ -177,20 +181,77 @@ function readPlanSummary(projectDir: string): string | null {
|
|
|
177
181
|
}
|
|
178
182
|
|
|
179
183
|
function ensureDir(dir: string): void {
|
|
180
|
-
|
|
181
|
-
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
|
|
182
192
|
}
|
|
183
193
|
}
|
|
184
194
|
|
|
185
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Ensure a plan file exists for the project.
|
|
197
|
+
* Creates a skeleton plan if none exists or if the latest is complete/abandoned.
|
|
198
|
+
* Reuses an existing active or in-progress plan.
|
|
199
|
+
* Returns the path to the plan file.
|
|
200
|
+
*/
|
|
201
|
+
function ensurePlanFile(projectDir: string): string {
|
|
202
|
+
const projectName = getProjectName(projectDir)
|
|
203
|
+
const storage = planStorageDir()
|
|
204
|
+
const projectDirPath = path.join(storage, projectName)
|
|
205
|
+
ensureDir(projectDirPath)
|
|
206
|
+
|
|
207
|
+
// Reuse active or in-progress plan
|
|
208
|
+
const latest = findLatestPlanFile(projectDir)
|
|
209
|
+
if (latest) {
|
|
210
|
+
const content = fs.readFileSync(latest, "utf8")
|
|
211
|
+
const status = content.match(/^Status:\s*(.+)$/m)?.[1]?.trim()
|
|
212
|
+
if (status === "active" || status === "in-progress") {
|
|
213
|
+
return latest
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Determine next sequence number
|
|
218
|
+
let nextSeq = 1
|
|
219
|
+
if (latest) {
|
|
220
|
+
const m = path.basename(latest).match(/^plan-(\d{3})\.md$/)
|
|
221
|
+
if (m) nextSeq = parseInt(m[1], 10) + 1
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const seq = String(nextSeq).padStart(3, "0")
|
|
225
|
+
const planId = `${projectName}/plan-${seq}.md`
|
|
226
|
+
const planPath = path.join(projectDirPath, `plan-${seq}.md`)
|
|
227
|
+
const now = new Date().toISOString().replace("T", " ").slice(0, 16)
|
|
228
|
+
|
|
229
|
+
const content = [
|
|
230
|
+
`# PLAN: ${projectName}`,
|
|
231
|
+
"",
|
|
232
|
+
`Plan ID: ${planId}`,
|
|
233
|
+
`Project: ${projectName}`,
|
|
234
|
+
`Status: active`,
|
|
235
|
+
`Created: ${now}`,
|
|
236
|
+
`Updated: ${now}`,
|
|
237
|
+
`Project Path: ${projectDir}`,
|
|
238
|
+
`Plan Path: ${planPath}`,
|
|
239
|
+
`Objective: (pending classification)`,
|
|
240
|
+
"",
|
|
241
|
+
"## Tasks",
|
|
242
|
+
"",
|
|
243
|
+
"- [ ] (discoverable — pending classification)",
|
|
244
|
+
"",
|
|
245
|
+
].join("\n")
|
|
246
|
+
|
|
186
247
|
try {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
return 0
|
|
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
|
|
193
253
|
}
|
|
254
|
+
return planPath
|
|
194
255
|
}
|
|
195
256
|
|
|
196
257
|
export function buildCompactionContext(projectDir: string): string[] {
|
|
@@ -232,83 +293,8 @@ export function formatSessionEvent(event: SessionLifecycleEvent): { level: "info
|
|
|
232
293
|
}
|
|
233
294
|
}
|
|
234
295
|
|
|
235
|
-
function parseRouteYaml(raw: string): { pass: string; fail: string; blocker: string } {
|
|
236
|
-
const def: { pass: string; fail: string; blocker: string } = { pass: "surface", fail: "surface", blocker: "surface" }
|
|
237
|
-
const m = raw.match(/route:\n((?: [^\n]*\n?)*)/)
|
|
238
|
-
if (!m) return def
|
|
239
|
-
const block = m[1]
|
|
240
|
-
|
|
241
|
-
const kv = (key: string): string | undefined => {
|
|
242
|
-
// Single-line: pass: oh-builder (horizontal whitespace only, no newlines)
|
|
243
|
-
const s = block.match(new RegExp(` ${key}:[ \\t]*(\\S.*)`))
|
|
244
|
-
if (s) return s[1].trim()
|
|
245
|
-
// Multi-line array: pass:\n - oh-builder\n - oh-gauntlet
|
|
246
|
-
const a = block.match(new RegExp(` ${key}:\\n((?: - .+\\n?)*)`))
|
|
247
|
-
if (a) {
|
|
248
|
-
const items = a[1].match(/ - (.+)/g)?.map(i => i.replace(/ - /, "").trim()) ?? []
|
|
249
|
-
return items.length > 0 ? `[${items.join(", ")}]` : undefined
|
|
250
|
-
}
|
|
251
|
-
return undefined
|
|
252
|
-
}
|
|
253
296
|
|
|
254
|
-
const p = kv("pass")
|
|
255
|
-
const f = kv("fail")
|
|
256
|
-
const b = kv("blocker")
|
|
257
|
-
if (p) def.pass = p
|
|
258
|
-
if (f) def.fail = f
|
|
259
|
-
if (b) def.blocker = b
|
|
260
|
-
return def
|
|
261
|
-
}
|
|
262
297
|
|
|
263
|
-
function buildRoutingInventory(skillDirs: string[]): string {
|
|
264
|
-
const rows: string[] = []
|
|
265
|
-
for (const dir of skillDirs) {
|
|
266
|
-
let entries: string[] = []
|
|
267
|
-
try { entries = fs.readdirSync(dir).filter(e => fs.statSync(path.join(dir, e)).isDirectory()) } catch { continue }
|
|
268
|
-
for (const name of entries.sort()) {
|
|
269
|
-
const skPath = path.join(dir, name, "SKILL.md")
|
|
270
|
-
if (!fs.existsSync(skPath)) continue
|
|
271
|
-
const raw = fs.readFileSync(skPath, "utf8").replace(/\r\n/g, "\n")
|
|
272
|
-
const fm = raw.match(/^---\n([\s\S]*?)\n---/)
|
|
273
|
-
if (!fm) continue
|
|
274
|
-
const route = parseRouteYaml(fm[1])
|
|
275
|
-
rows.push(`| **${name}** | ${route.pass} | ${route.fail} | ${route.blocker} |`)
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
if (rows.length === 0) return ""
|
|
279
|
-
const header = "## Dynamic Routing Inventory\n\nAll skills and their routes:\n\n| Skill | pass | fail | blocker |\n|---|---|---|---|\n"
|
|
280
|
-
return header + rows.join("\n")
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function buildBootstrapContent(hDir: string, extraDirs: string[] = []): string {
|
|
284
|
-
const parts = [
|
|
285
|
-
`<${BOOTSTRAP_MARKER}>`,
|
|
286
|
-
`You are OpenHermes.`,
|
|
287
|
-
`OpenHermes is OpenCode-native: load skills on demand, always delegate, never execute tasks directly, and keep the surface small.`,
|
|
288
|
-
`Durable state is removed for now. Do not invent a persistence layer unless the user explicitly asks for one later.`,
|
|
289
|
-
]
|
|
290
|
-
|
|
291
|
-
const autopilot = readText(path.join(hDir, "codex", "AUTOPILOT.md"))
|
|
292
|
-
const constitution = readText(path.join(hDir, "codex", "CONSTITUTION.md"))
|
|
293
|
-
const runtime = readText(path.join(hDir, "instructions", "RUNTIME.md"))
|
|
294
|
-
const context = readText(path.join(__dirname, "CONTEXT.md"))
|
|
295
|
-
const ethos = readText(path.join(__dirname, "ETHOS.md"))
|
|
296
|
-
|
|
297
|
-
if (autopilot) parts.push(`<AUTOPILOT>\n${autopilot}\n</AUTOPILOT>`)
|
|
298
|
-
if (constitution) parts.push(`<CONSTITUTION>\n${constitution}\n</CONSTITUTION>`)
|
|
299
|
-
if (runtime) parts.push(`<RUNTIME>\n${runtime}\n</RUNTIME>`)
|
|
300
|
-
if (context) parts.push(`<CONTEXT>\n${context}\n</CONTEXT>`)
|
|
301
|
-
if (ethos) parts.push(`<ETHOS>\n${ethos}\n</ETHOS>`)
|
|
302
|
-
|
|
303
|
-
// Dynamic routing inventory: built-in skills + user skills
|
|
304
|
-
const allSkillDirs = [path.join(hDir, "skills"), ...extraDirs.filter(Boolean)]
|
|
305
|
-
const inventory = buildRoutingInventory(allSkillDirs)
|
|
306
|
-
if (inventory) parts.push(inventory)
|
|
307
|
-
|
|
308
|
-
parts.push(`</${BOOTSTRAP_MARKER}>`)
|
|
309
|
-
|
|
310
|
-
return parts.join("\n\n")
|
|
311
|
-
}
|
|
312
298
|
|
|
313
299
|
interface OpenHermesConfig {
|
|
314
300
|
skills?: { paths?: string[] }
|
|
@@ -316,6 +302,7 @@ interface OpenHermesConfig {
|
|
|
316
302
|
agent?: Record<string, unknown>
|
|
317
303
|
instructions?: string[]
|
|
318
304
|
default_agent?: string
|
|
305
|
+
[key: string]: unknown // allow additional SDK properties (experimental, etc.)
|
|
319
306
|
}
|
|
320
307
|
|
|
321
308
|
export const BootstrapPlugin: Plugin = async (ctx) => {
|
|
@@ -323,56 +310,143 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
|
|
|
323
310
|
const skillsDir = path.join(hDir, "skills")
|
|
324
311
|
const commandsDir = path.join(hDir, "commands")
|
|
325
312
|
const agentsDir = path.join(hDir, "agents")
|
|
313
|
+
const client = ctx.client // SDK client for structured logging
|
|
314
|
+
|
|
315
|
+
// Safe logging — uses OpenCode SDK when available, falls back to stdout for tests
|
|
316
|
+
async function logToOC(level: "info" | "warn" | "error" | "debug", message: string): Promise<void> {
|
|
317
|
+
if (client?.app?.log) {
|
|
318
|
+
await client.app.log({ body: { service: "openhermes", level, message } })
|
|
319
|
+
} else {
|
|
320
|
+
console.log(`[openhermes] [${level.toUpperCase()}] ${message}`)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
326
324
|
// Auto-detect and wire user skills from ~/.agents/skills and ~/.config/opencode/skills
|
|
327
|
-
// (Must happen before bootstrapContent is built so routing inventory includes user skills)
|
|
328
325
|
const userSkillPaths: string[] = []
|
|
329
326
|
for (const userDir of USER_SKILL_DIRS) {
|
|
330
|
-
ensureDir(userDir)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
userSkillPaths.push(userDir)
|
|
334
|
-
log.info(`found ${count} user skill(s) in ${userDir}`)
|
|
335
|
-
}
|
|
327
|
+
try { ensureDir(userDir) } catch {}
|
|
328
|
+
userSkillPaths.push(userDir)
|
|
329
|
+
await logToOC("info", `wired user skills from ${userDir}`)
|
|
336
330
|
}
|
|
337
331
|
|
|
338
|
-
const bootstrapContent = buildBootstrapContent(hDir, userSkillPaths)
|
|
339
332
|
const compactionContext = buildCompactionContext(ctx.directory)
|
|
340
|
-
const builtInCount = countSkills(skillsDir)
|
|
341
|
-
const userCount = userSkillPaths.reduce((sum, d) => sum + countSkills(d), 0)
|
|
342
|
-
|
|
343
333
|
// Ensure plan storage exists
|
|
344
|
-
ensureDir(planStorageDir())
|
|
334
|
+
try { ensureDir(planStorageDir()) } catch {}
|
|
345
335
|
|
|
346
336
|
return {
|
|
347
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 ──────────────────────────────────────────────────────
|
|
348
364
|
config.skills = config.skills || {}
|
|
349
365
|
// Built-in paths first, user paths last → user skills override built-in on name conflict
|
|
350
366
|
const allPaths = [skillsDir, ...userSkillPaths]
|
|
351
367
|
config.skills.paths = uniqueStrings(config.skills.paths || [], allPaths)
|
|
352
368
|
|
|
353
|
-
|
|
369
|
+
await logToOC("info", `skills: ${allPaths.length} path(s)`)
|
|
370
|
+
|
|
371
|
+
// Register harness docs as native OpenCode instructions — no prompt-embedding needed
|
|
372
|
+
config.instructions = uniqueStrings(config.instructions ?? [], [
|
|
373
|
+
path.join(hDir, "codex"),
|
|
374
|
+
path.join(hDir, "instructions"),
|
|
375
|
+
])
|
|
354
376
|
|
|
355
377
|
config.command = { ...(config.command ?? {}), ...commandDefinitions(commandsDir) }
|
|
356
378
|
|
|
357
379
|
const loadedAgents = agentDefinitions(agentsDir)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Subagent permissions — tier-4 and tier-3 get execution access but cannot spawn orchestrators
|
|
394
|
+
const SUBAGENT_PERMISSIONS: Record<string, Record<string, unknown>> = {
|
|
395
|
+
"oh-builder": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
396
|
+
"oh-browser": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
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" } },
|
|
399
|
+
"oh-gauntlet": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
400
|
+
"oh-grill": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
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" } },
|
|
403
|
+
"oh-plan-review": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
404
|
+
"oh-planner": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
405
|
+
"oh-refactor": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
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" } },
|
|
410
|
+
"oh-skill-craft": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
|
|
362
411
|
}
|
|
363
412
|
|
|
364
413
|
config.agent = {
|
|
365
414
|
...(config.agent ?? {}),
|
|
366
415
|
...loadedAgents,
|
|
416
|
+
// Apply permissions + hidden flag to subagents
|
|
417
|
+
...Object.fromEntries(
|
|
418
|
+
Object.entries(loadedAgents)
|
|
419
|
+
.filter(([name]) => name !== OPENHERMES_AGENT)
|
|
420
|
+
.map(([name, agentDef]) => [
|
|
421
|
+
name,
|
|
422
|
+
{
|
|
423
|
+
...agentDef,
|
|
424
|
+
permission: SUBAGENT_PERMISSIONS[name] ?? { bash: { "*": "deny" }, edit: "deny", read: "allow" },
|
|
425
|
+
// Hide routing-internal subagents from @-menu
|
|
426
|
+
// Only agents with existing .md files can be hidden — names without files are no-ops
|
|
427
|
+
...(["oh-planner", "oh-grill", "oh-skill-craft"].includes(name) ? { hidden: true } : {}),
|
|
428
|
+
},
|
|
429
|
+
])
|
|
430
|
+
),
|
|
367
431
|
[OPENHERMES_AGENT]: {
|
|
368
432
|
...openHermesAgent,
|
|
369
433
|
description: openHermesAgent.description || "OpenHermes primary orchestrator",
|
|
370
434
|
mode: "primary",
|
|
435
|
+
steps: 15, // Max agentic iterations — prevents runaway loops
|
|
371
436
|
permission: {
|
|
372
|
-
bash: { "*": "
|
|
373
|
-
edit: "
|
|
374
|
-
read: "allow",
|
|
375
|
-
|
|
437
|
+
bash: { "*": "deny" }, // CANNOT execute commands
|
|
438
|
+
edit: "deny", // CANNOT write/edit files
|
|
439
|
+
read: "allow", // CAN read for classification
|
|
440
|
+
glob: "allow", // CAN search for files
|
|
441
|
+
grep: "allow", // CAN search content
|
|
442
|
+
task: { "*": "allow" }, // MUST delegate via subagents
|
|
443
|
+
skill: "allow", // CAN load skill instructions
|
|
444
|
+
webfetch: "allow", // CAN fetch docs for context
|
|
445
|
+
question: "allow", // CAN ask user questions
|
|
446
|
+
websearch: "allow", // CAN search web for research context
|
|
447
|
+
external_directory: { // CAN read/write plan files outside worktree
|
|
448
|
+
"~/.local/share/opencode/openhermes/**": "allow",
|
|
449
|
+
},
|
|
376
450
|
},
|
|
377
451
|
},
|
|
378
452
|
}
|
|
@@ -381,25 +455,205 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
|
|
|
381
455
|
},
|
|
382
456
|
|
|
383
457
|
event: async ({ event }) => {
|
|
384
|
-
const
|
|
458
|
+
const typed = event as SessionLifecycleEvent
|
|
459
|
+
const record = formatSessionEvent(typed)
|
|
385
460
|
if (!record) return
|
|
386
|
-
|
|
461
|
+
await logToOC(record.level, record.message)
|
|
462
|
+
|
|
463
|
+
// NOTE: Plan files are NOT auto-created here. The LLM agent
|
|
464
|
+
// creates plans on demand (see Task Flow step 1 in agent prompt).
|
|
465
|
+
// Auto-creation produced ghost skeletons like plan-004.
|
|
466
|
+
|
|
467
|
+
// Reset delegation depth on session start/error
|
|
468
|
+
if (typed.type === "session.created" || typed.type === "session.error") {
|
|
469
|
+
resetDepthTracker()
|
|
470
|
+
}
|
|
387
471
|
},
|
|
388
472
|
|
|
389
473
|
"experimental.session.compacting": async (_input, output) => {
|
|
390
474
|
output.context.push(...compactionContext)
|
|
391
475
|
},
|
|
392
476
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
477
|
+
// Hook-enabled tool execution — delegates to HookRegistry for lifecycle hooks
|
|
478
|
+
"tool.execute.before": async (input, output) => {
|
|
479
|
+
if (input.tool === "task") {
|
|
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
|
+
}
|
|
512
|
+
|
|
513
|
+
if (preToolResult.result === HookResult.STOP) {
|
|
514
|
+
// Depth exceeded or other stop condition
|
|
515
|
+
const errOutput = output as { args: unknown; isError?: boolean; content?: unknown[] }
|
|
516
|
+
errOutput.isError = true
|
|
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
|
+
}
|
|
402
595
|
}
|
|
403
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
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
|
|
404
657
|
}
|
|
405
658
|
}
|
|
659
|
+
|