openhermes 4.0.1 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/ETHOS.md +6 -3
  2. package/LICENSE +21 -21
  3. package/README.md +111 -81
  4. package/bootstrap.ts +405 -0
  5. package/harness/agents/openhermes.md +45 -55
  6. package/harness/codex/AUTOPILOT.md +126 -0
  7. package/harness/codex/CONSTITUTION.md +14 -11
  8. package/harness/codex/ROUTING.md +35 -69
  9. package/harness/commands/oh-log.md +18 -0
  10. package/harness/instructions/RUNTIME.md +27 -51
  11. package/harness/skills/oh-builder/SKILL.md +27 -16
  12. package/harness/skills/oh-caveman/SKILL.md +9 -0
  13. package/harness/skills/oh-expert/SKILL.md +6 -0
  14. package/harness/skills/oh-facade/SKILL.md +298 -0
  15. package/harness/skills/oh-freeze/SKILL.md +9 -0
  16. package/harness/skills/oh-full-output/SKILL.md +81 -0
  17. package/harness/skills/oh-fusion/SKILL.md +314 -0
  18. package/harness/skills/oh-gauntlet/SKILL.md +10 -6
  19. package/harness/skills/oh-grill/SKILL.md +9 -5
  20. package/harness/skills/oh-guard/SKILL.md +9 -0
  21. package/harness/skills/oh-handoff/SKILL.md +9 -0
  22. package/harness/skills/oh-health/SKILL.md +8 -4
  23. package/harness/skills/oh-init/SKILL.md +80 -13
  24. package/harness/skills/oh-investigate/SKILL.md +57 -8
  25. package/harness/skills/oh-issue/SKILL.md +9 -0
  26. package/harness/skills/oh-learn/SKILL.md +81 -8
  27. package/harness/skills/oh-manifest/SKILL.md +55 -11
  28. package/harness/skills/oh-plan-review/SKILL.md +15 -8
  29. package/harness/skills/oh-planner/SKILL.md +18 -8
  30. package/harness/skills/oh-prd/SKILL.md +9 -0
  31. package/harness/skills/oh-refactor/SKILL.md +426 -0
  32. package/harness/skills/oh-retro/SKILL.md +9 -0
  33. package/harness/skills/oh-review/SKILL.md +12 -5
  34. package/harness/skills/oh-security/SKILL.md +4 -0
  35. package/harness/skills/oh-ship/SKILL.md +10 -0
  36. package/harness/skills/oh-skill-craft/SKILL.md +88 -0
  37. package/harness/skills/oh-skills-link/SKILL.md +9 -0
  38. package/harness/skills/oh-skills-list/SKILL.md +9 -0
  39. package/harness/skills/oh-triage/SKILL.md +11 -0
  40. package/index.ts +3 -0
  41. package/lib/{harness-resolver.mjs → harness-resolver.ts} +16 -12
  42. package/lib/logger.ts +75 -0
  43. package/package.json +16 -10
  44. package/tsconfig.json +16 -0
  45. package/bootstrap.mjs +0 -174
  46. package/harness/instructions/CONVENTIONS.md +0 -206
  47. package/index.mjs +0 -3
  48. package/lib/logger.mjs +0 -62
  49. package/test/plugins-behavioral.test.mjs +0 -64
  50. package/test/plugins.test.mjs +0 -62
package/bootstrap.ts ADDED
@@ -0,0 +1,405 @@
1
+ import path from "node:path"
2
+ import fs from "node:fs"
3
+ import os from "node:os"
4
+ import { fileURLToPath } from "node:url"
5
+ import type { Plugin } from "@opencode-ai/plugin"
6
+ import { createLogger } from "./lib/logger.ts"
7
+ import { getHarnessDir, setHarnessRootForTest, resolveHarnessRoot } from "./lib/harness-resolver.ts"
8
+
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
+ const OPENHERMES_AGENT = "OpenHermes"
14
+
15
+ // Canonical storage under OpenCode's data directory — survives npm updates
16
+ let _planStorageOverride: string | undefined
17
+ export function setPlanStorageDirForTest(dir: string | undefined): void { _planStorageOverride = dir }
18
+ function planStorageDir(): string {
19
+ return _planStorageOverride ?? path.join(os.homedir(), ".local", "share", "opencode", "openhermes", "plans")
20
+ }
21
+
22
+ function getProjectName(projectDir: string): string {
23
+ return path.basename(projectDir)
24
+ }
25
+
26
+ // User skill directories — auto-scanned on every session, survive npm updates
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 }
33
+
34
+ function parseFrontmatter(raw: string | undefined): Record<string, string> {
35
+ const frontmatter: Record<string, string> = {}
36
+ if (!raw) return frontmatter
37
+ for (const line of raw.split(/\r?\n/)) {
38
+ const idx = line.indexOf(":")
39
+ if (idx < 0) continue
40
+ const key = line.slice(0, idx).trim()
41
+ const value = line.slice(idx + 1).trim().replace(/^['"]|['"]$/g, "")
42
+ if (key) frontmatter[key] = value
43
+ }
44
+ return frontmatter
45
+ }
46
+
47
+ interface MarkdownDocument {
48
+ frontmatter: Record<string, string>
49
+ body: string
50
+ }
51
+
52
+ function readMarkdownDocument(filePath: string): MarkdownDocument | null {
53
+ if (!fs.existsSync(filePath)) return null
54
+ const source = fs.readFileSync(filePath, "utf8")
55
+ const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/)
56
+ const frontmatter = parseFrontmatter(match?.[1] ?? "")
57
+ const body = (match ? match[2] : source).trim()
58
+ return { frontmatter, body }
59
+ }
60
+
61
+ interface DirEntry extends MarkdownDocument {
62
+ name: string
63
+ }
64
+
65
+ function readMarkdownDirectory(dir: string): DirEntry[] {
66
+ if (!fs.existsSync(dir)) return []
67
+ return fs.readdirSync(dir)
68
+ .filter(name => name.endsWith(".md") && name.toLowerCase() !== "readme.md")
69
+ .sort((a, b) => a.localeCompare(b))
70
+ .map(name => {
71
+ const filePath = path.join(dir, name)
72
+ const document = readMarkdownDocument(filePath)
73
+ return document ? { name: path.basename(name, ".md"), ...document } : null
74
+ })
75
+ .filter((e): e is DirEntry => e !== null)
76
+ }
77
+
78
+ interface CommandDef {
79
+ description: string
80
+ template: string
81
+ agent?: string
82
+ model?: string
83
+ subtask?: boolean
84
+ }
85
+
86
+ function commandDefinitions(dir: string): Record<string, CommandDef> {
87
+ const commands: Record<string, CommandDef> = {}
88
+ for (const doc of readMarkdownDirectory(dir)) {
89
+ const command: CommandDef = {
90
+ description: doc.frontmatter.description || `OpenHermes command ${doc.name}`,
91
+ template: doc.body,
92
+ }
93
+ if (doc.frontmatter.agent) command.agent = doc.frontmatter.agent
94
+ if (doc.frontmatter.model) command.model = doc.frontmatter.model
95
+ if (doc.frontmatter.subtask) command.subtask = doc.frontmatter.subtask === "true"
96
+ commands[doc.name] = command
97
+ }
98
+ return commands
99
+ }
100
+
101
+ interface AgentDef {
102
+ description: string
103
+ mode: string
104
+ prompt: string
105
+ }
106
+
107
+ function agentDefinitions(dir: string): Record<string, AgentDef> {
108
+ const agents: Record<string, AgentDef> = {}
109
+ for (const doc of readMarkdownDirectory(dir)) {
110
+ const name = doc.name === "openhermes" ? OPENHERMES_AGENT : doc.name
111
+ agents[name] = {
112
+ description: doc.frontmatter.description || (name === OPENHERMES_AGENT ? "OpenHermes primary orchestrator" : `OpenHermes agent ${name}`),
113
+ mode: doc.frontmatter.mode || (name === OPENHERMES_AGENT ? "primary" : "subagent"),
114
+ prompt: doc.body,
115
+ }
116
+ }
117
+ return agents
118
+ }
119
+
120
+ function uniqueStrings(existing: string[] = [], additions: string[] = []): string[] {
121
+ const seen = new Set(existing.filter(Boolean))
122
+ const merged = [...existing]
123
+ for (const item of additions) {
124
+ if (!item || seen.has(item)) continue
125
+ seen.add(item)
126
+ merged.push(item)
127
+ }
128
+ return merged
129
+ }
130
+
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
+
139
+ function findLatestPlanFile(projectDir: string): string | null {
140
+ const projectName = getProjectName(projectDir)
141
+ const storage = planStorageDir()
142
+ if (!fs.existsSync(storage)) return null
143
+ const pattern = new RegExp(`^${regexEscape(projectName)}-plan-(\\d{3})\\.md$`)
144
+ let latest: string | null = null
145
+ let highest = -1
146
+ try {
147
+ for (const entry of fs.readdirSync(storage)) {
148
+ const m = entry.match(pattern)
149
+ if (m) {
150
+ const n = parseInt(m[1], 10)
151
+ if (n > highest) {
152
+ highest = n
153
+ latest = path.join(storage, entry)
154
+ }
155
+ }
156
+ }
157
+ } catch {
158
+ return null
159
+ }
160
+ return latest
161
+ }
162
+
163
+ function readPlanFromFile(filePath: string): string | null {
164
+ if (!fs.existsSync(filePath)) return null
165
+ const source = fs.readFileSync(filePath, "utf8")
166
+ const status = source.match(/^Status:\s*(.+)$/m)?.[1]?.trim()
167
+ const objective = source.match(/^Objective:\s*(.+)$/m)?.[1]?.trim()
168
+ if (!status && !objective) return null
169
+ const parts = [status ? `status=${status}` : null, objective ? `objective=${objective}` : null].filter(Boolean)
170
+ return `Active plan: ${parts.join(" | ")}`
171
+ }
172
+
173
+ function readPlanSummary(projectDir: string): string | null {
174
+ const planFile = findLatestPlanFile(projectDir)
175
+ if (!planFile) return null
176
+ return readPlanFromFile(planFile)
177
+ }
178
+
179
+ function ensureDir(dir: string): void {
180
+ if (!fs.existsSync(dir)) {
181
+ fs.mkdirSync(dir, { recursive: true })
182
+ }
183
+ }
184
+
185
+ function countSkills(dir: string): number {
186
+ try {
187
+ return fs.readdirSync(dir).filter(e => {
188
+ const full = path.join(dir, e)
189
+ return fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, "SKILL.md"))
190
+ }).length
191
+ } catch {
192
+ return 0
193
+ }
194
+ }
195
+
196
+ export function buildCompactionContext(projectDir: string): string[] {
197
+ const context = [
198
+ "OpenHermes: native-first, verify before claim, always delegate, concise over verbose.",
199
+ "Preserve domain terms: skill, command, agent, bootstrap, compaction.",
200
+ "Preserve blockers, current task, and next steps; do not invent durable state.",
201
+ ]
202
+
203
+ const planSummary = readPlanSummary(projectDir)
204
+ if (planSummary) context.push(planSummary)
205
+
206
+ return context
207
+ }
208
+
209
+ type SessionLifecycleEvent =
210
+ | { type: "session.created"; properties: { info: { id: string } } }
211
+ | { type: "session.compacted"; properties: { sessionID: string } }
212
+ | { type: "session.error"; properties: { sessionID?: string; error?: unknown } }
213
+
214
+ function readErrorMessage(error: unknown): string {
215
+ if (!error || typeof error !== "object") return "unknown error"
216
+ const value = error as { name?: unknown; message?: unknown; data?: { message?: unknown } }
217
+ const name = typeof value.name === "string" && value.name ? value.name : "Error"
218
+ const message = typeof value.data?.message === "string" && value.data.message ? value.data.message : typeof value.message === "string" && value.message ? value.message : ""
219
+ return message ? `${name}: ${message}` : name
220
+ }
221
+
222
+ export function formatSessionEvent(event: SessionLifecycleEvent): { level: "info" | "error"; message: string } | null {
223
+ switch (event.type) {
224
+ case "session.created":
225
+ return { level: "info", message: `session.created session=${event.properties.info.id}` }
226
+ case "session.compacted":
227
+ return { level: "info", message: `session.compacted session=${event.properties.sessionID}` }
228
+ case "session.error":
229
+ return { level: "error", message: `session.error session=${event.properties.sessionID ?? "unknown"} error=${readErrorMessage(event.properties.error)}` }
230
+ default:
231
+ return null
232
+ }
233
+ }
234
+
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
+
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
+
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
+
313
+ interface OpenHermesConfig {
314
+ skills?: { paths?: string[] }
315
+ command?: Record<string, unknown>
316
+ agent?: Record<string, unknown>
317
+ instructions?: string[]
318
+ default_agent?: string
319
+ }
320
+
321
+ export const BootstrapPlugin: Plugin = async (ctx) => {
322
+ const hDir = getHarnessDir()
323
+ const skillsDir = path.join(hDir, "skills")
324
+ const commandsDir = path.join(hDir, "commands")
325
+ const agentsDir = path.join(hDir, "agents")
326
+ // 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
+ const userSkillPaths: string[] = []
329
+ for (const userDir of USER_SKILL_DIRS) {
330
+ ensureDir(userDir)
331
+ const count = countSkills(userDir)
332
+ if (count > 0) {
333
+ userSkillPaths.push(userDir)
334
+ log.info(`found ${count} user skill(s) in ${userDir}`)
335
+ }
336
+ }
337
+
338
+ const bootstrapContent = buildBootstrapContent(hDir, userSkillPaths)
339
+ const compactionContext = buildCompactionContext(ctx.directory)
340
+ const builtInCount = countSkills(skillsDir)
341
+ const userCount = userSkillPaths.reduce((sum, d) => sum + countSkills(d), 0)
342
+
343
+ // Ensure plan storage exists
344
+ ensureDir(planStorageDir())
345
+
346
+ return {
347
+ config: async (config: OpenHermesConfig) => {
348
+ config.skills = config.skills || {}
349
+ // Built-in paths first, user paths last → user skills override built-in on name conflict
350
+ const allPaths = [skillsDir, ...userSkillPaths]
351
+ config.skills.paths = uniqueStrings(config.skills.paths || [], allPaths)
352
+
353
+ log.info(`skills: ${builtInCount} built-in + ${userCount} user (${allPaths.length} path(s))`)
354
+
355
+ config.command = { ...(config.command ?? {}), ...commandDefinitions(commandsDir) }
356
+
357
+ const loadedAgents = agentDefinitions(agentsDir)
358
+ const openHermesAgent = loadedAgents[OPENHERMES_AGENT] ?? {
359
+ description: "OpenHermes primary orchestrator",
360
+ mode: "primary",
361
+ prompt: "You are OpenHermes.",
362
+ }
363
+
364
+ config.agent = {
365
+ ...(config.agent ?? {}),
366
+ ...loadedAgents,
367
+ [OPENHERMES_AGENT]: {
368
+ ...openHermesAgent,
369
+ description: openHermesAgent.description || "OpenHermes primary orchestrator",
370
+ mode: "primary",
371
+ permission: {
372
+ bash: { "*": "allow" },
373
+ edit: "allow",
374
+ read: "allow",
375
+ task: { "*": "allow" },
376
+ },
377
+ },
378
+ }
379
+
380
+ config.default_agent = OPENHERMES_AGENT
381
+ },
382
+
383
+ event: async ({ event }) => {
384
+ const record = formatSessionEvent(event as SessionLifecycleEvent)
385
+ if (!record) return
386
+ sessionLog[record.level](record.message)
387
+ },
388
+
389
+ "experimental.session.compacting": async (_input, output) => {
390
+ output.context.push(...compactionContext)
391
+ },
392
+
393
+ "experimental.chat.messages.transform": async (_input: unknown, output: { messages?: Array<{ info?: { role?: string }; parts?: Array<{ text?: string; type?: string }> }> }) => {
394
+ try {
395
+ if (!output.messages?.length) return
396
+ const firstUser = output.messages.find(m => m?.info?.role === "user")
397
+ if (!firstUser?.parts?.length) return
398
+ if (firstUser.parts.some(p => p.text?.includes(BOOTSTRAP_MARKER))) return
399
+ firstUser.parts.unshift({ type: "text", text: bootstrapContent })
400
+ } catch (err: unknown) {
401
+ log.error("transform error:", (err as Error)?.message)
402
+ }
403
+ },
404
+ }
405
+ }
@@ -1,61 +1,49 @@
1
1
  ---
2
- description: OpenHermes primary orchestrator
2
+ description: OpenHermes primary orchestrator — auto-routing closed-loop hub
3
3
  mode: primary
4
4
  ---
5
5
 
6
6
  You are OpenHermes, the primary orchestrator for this package.
7
7
 
8
- Behavior:
8
+ ## Operating Mode: SELF-DRIVING
9
9
 
10
- - Use OpenCode-native skills on demand.
11
- - Prefer the smallest correct change.
12
- - Delegate substantive multi-file work to subagents.
13
- - Keep responses terse and evidence-based.
14
- - Follow the package constitution, runtime notes, shared context, and ethos.
15
- - Plan first, verify before claiming success, and summarize with receipts.
10
+ This is a fully closed-loop system. You auto-classify, auto-route, and auto-execute. You do not ask for permission to proceed. You only stop for genuine blockers.
16
11
 
17
- ## Orchestration Model
12
+ **The autopilot engine (`harness/codex/AUTOPILOT.md`) governs every session.** Read it. Follow it. It is not optional.
18
13
 
19
- Hub-and-spoke. You (OpenHermes) are the hub. Delegate to specialists:
14
+ ### Ground Rules
20
15
 
21
- - **oh-planner** for planning, architecture, strategy, brainstorming. Produces `.opencode/plan.md`.
22
- - **oh-builder** for implementation, TDD, prototyping, interface design. Consumes plan.md.
23
- - **oh-manifest** for full build loops: plan build verify loop. Orchestrates planner + builder.
24
- - **oh-gauntlet** for rigorous multi-axis testing: unit tests, review, edge cases, QA, canary.
25
- - **oh-expert** — for AI self-diagnosis (sycophancy, hallucination type, attention degradation).
26
- - **oh-grill** — for stress-testing plans and designs through questioning.
27
- - **oh-investigate** — for systematic bug diagnosis.
16
+ 1. **Auto-classify before every response.** Multi-step or aimless? → oh-planner. Bug? oh-investigate. Security? oh-security. Code review? → oh-review. Simple edit? → do it directly. The AUTOPILOT decision matrix is your classification authority.
17
+ 2. **Auto-route after every skill.** Pass? Route by the skill's routing table. Fail? Route by the skill's routing table. Do not ask. Do not pause. Route.
18
+ 3. **Close the loop.** No dead ends. Every skill routes somewhere. Only oh-handoff ends a session.
19
+ 4. **Stop only for:** (a) task complete, (b) real blocker, (c) major architecture decision that changes the outcome. Do NOT stop for "should I?" questions — just do the next correct thing.
28
20
 
29
- ## Auto-Routing
21
+ ### Orchestration Model
30
22
 
31
- Every skill routes to the next based on outcome. No dead ends. The canonical routing graph is defined in `harness/codex/ROUTING.md`.
23
+ Hub-and-spoke. You are the hub. Skills are loaded on demand through the skill tool. Delegate to specialists:
32
24
 
33
- ### Entry triggers
25
+ - **oh-planner** — planning, architecture, strategy, brainstorming. Produces `<project>-plan-<nnn>.md`.
26
+ - **oh-builder** — implementation, TDD, prototyping, interface design. Consumes the plan file.
27
+ - **oh-manifest** — full build loops: plan → build → verify → loop. Orchestrates planner + builder.
28
+ - **oh-gauntlet** — multi-axis testing: unit tests, review, edge cases, QA, canary.
29
+ - **oh-expert** — AI self-diagnosis (sycophancy, hallucination type, attention degradation).
30
+ - **oh-grill** — stress-test plans and designs through questioning.
31
+ - **oh-investigate** — systematic bug diagnosis.
32
+ - **oh-review** — two-axis code and design review.
33
+ - **oh-ship** — deploy, version bump, changelog, PR.
34
+ - **oh-security** — security audit, threat model.
35
+ - **oh-health** — code quality dashboard.
36
+ - **oh-refactor** — surgical behavior-preserving refactoring.
37
+ - **oh-facade** — full UI pipeline: concept → design system → build → audit → iterate.
38
+ - **oh-full-output** — override LLM truncation, ban placeholder patterns, enforce complete generation.
39
+ - **oh-fusion** — skill ingestion pipeline: discover → analyze → filter → adapt → fuse → integrate.
40
+ - **oh-handoff** — compact session state for context switch.
34
41
 
35
- Evaluate the request and load the matching skill as a subagent:
42
+ ### Auto-Routing Graph
36
43
 
37
- | When the task is | Load skill |
38
- |---|---|
39
- | Planning, architecture, strategy, brainstorming, scoping | oh-planner |
40
- | Implementation, building, prototyping, TDD, coding from spec | oh-builder |
41
- | Full build pipeline (plan → build → verify → loop) | oh-manifest |
42
- | Testing, QA, edge case sweep, validation gate, "run the gauntlet" | oh-gauntlet |
43
- | AI self-diagnosis, sycophancy check, hallucination check, attention check | oh-expert |
44
- | Stress-testing a plan, challenging assumptions, "grill me" | oh-grill |
45
- | Bug diagnosis, root cause investigation, "why is this broken" | oh-investigate |
46
- | Deploy, version bump, changelog, PR | oh-ship |
47
- | Security audit, threat model, vulnerability scan | oh-security |
48
- | Code quality dashboard, run all checks | oh-health |
49
- | Code review, PR review, design review | oh-review |
50
- | Review existing plan, architecture review | oh-plan-review |
51
- | Retrospective, post-ship review | oh-retro |
52
- | Session handoff, context switch | oh-handoff |
53
- | Diagnose self, check for sycophancy/hallucination | oh-expert |
54
-
55
- ### Outcome-based routing
56
-
57
- After a skill completes, route to the next skill based on outcome. See `harness/codex/ROUTING.md` for the full graph. The core loop is:
44
+ The canonical routing graph is in `harness/codex/ROUTING.md`. Follow it exactly.
58
45
 
46
+ Core loop:
59
47
  ```
60
48
  oh-planner → oh-grill → oh-planner (revise) → oh-manifest
61
49
 
@@ -65,23 +53,25 @@ oh-manifest → oh-planner → oh-builder → oh-gauntlet → oh-ship → oh-ret
65
53
  └──────── oh-expert ←── fail ──── oh-expert
66
54
  ```
67
55
 
68
- If a task spans multiple domains (e.g., "build and test this feature"), load the orchestrator (`oh-manifest`) which chains planner → builder → verify → ship → retro → back to planning. Do not load skills that don't match the task.
56
+ ### OptiRoute Protocol
57
+
58
+ Three safety layers on top of every routing hop:
69
59
 
70
- ### OptiRoute: Smart Auto-Routing Protocol
60
+ **Loop Guard.** Same skill 3+ times in one chain, or 5+ hops without progress → STOP, write report to the plan file, surface to user.
71
61
 
72
- Three safety layers on top of every routing hop. Full spec in `harness/codex/ROUTING.md`.
62
+ **Question Gate.** Before routing, check: "Can I proceed without guessing?" If the next skill's input is missing and you cannot create or discover it independently → surface. Do NOT route into guaranteed failure.
73
63
 
74
- **Loop Guard.** Track routing depth. If the same skill is visited 3+ times in one chain, or 5+ hops pass without measurable progress (new artifact, changed target) — stop, report, await user.
64
+ **Auto-Handoff.** When Loop Guard triggers: write OptiRoute report, surface `OPTIROUTE STOP: <reason>`, exit loop.
75
65
 
76
- **Question Gate.** Before routing, check: "Can I proceed without guessing?" If the next skill's input is missing or the task is ambiguous — ask the user. Do not route into uncertainty.
66
+ ### User Skills Auto-Detection
77
67
 
78
- **Auto-Handoff.** When Loop Guard triggers: stop routing, write an OptiRoute report to `.opencode/plan.md` (routing chain, trigger, current state, blocker), surface `OPTIROUTE STOP: <reason>` to the user, and exit the loop.
68
+ Skills in `~/.agents/skills/` and `~/.config/opencode/skills/` are auto-discovered on every session. On name conflict with a built-in `oh-*` skill, the user version wins. User skills survive `npm update openhermes` they live outside the package dir.
79
69
 
80
- ## Delegation Rules
70
+ ### Delegation Rules
81
71
 
82
- 1. **Deploy subagents for isolated context** — large searches, independent subtasks, parallel review axes. Each subagent burns its own context window.
83
- 2. **Background vs sync** — independent work delegates in background (fire-and-forget). Dependent work delegates sync (await result).
84
- 3. **One level deep** — subagents you spawn cannot spawn subagents of their own. That is your job.
85
- 4. **Checkpoint before handoff** — write progress to `.opencode/work-log.md` before delegating to a subagent.
86
- 5. **Verify after return** — confirm subagent output before accepting it.
87
- 6. **Surface blockers immediately**if a delegate cannot proceed, report BLOCKER with options. Do not silently retry 5 times.
72
+ 1. Deploy subagents for isolated context — large searches, independent subtasks, parallel review.
73
+ 2. Background (fire-and-forget) for independent work. Sync (await result) for dependent work.
74
+ 3. One level deep — subagents do not spawn subagents.
75
+ 4. Checkpoint before handoff — write progress to the plan file (Completed section + Subagents table) before delegating.
76
+ 5. Verify after return — confirm subagent output before accepting it.
77
+ 6. Surface blockers immediately — report BLOCKER with options. Do not silently retry.
@@ -0,0 +1,126 @@
1
+ # OpenHermes Autopilot
2
+
3
+ The closed-loop auto-routing engine. Every task auto-classifies, auto-routes, and auto-chains. Only stop for genuine blockers.
4
+
5
+ ## Auto-Classify
6
+
7
+ Before any substantive response, classify the task using this decision matrix:
8
+
9
+ | Signal | Classification | Action |
10
+ |---|---|---|
11
+ | Multi-step, vague, aimless, "improve", "make better", "fix up", "clean up", "organize", "I have an idea", no clear deliverable | PLANNING NEEDED | Load **oh-planner** (Mode A brainstorm or Mode C structured plan). Do not ask. |
12
+ | Bug, crash, regression, unexpected behavior, "why is X broken" | INVESTIGATION NEEDED | Load **oh-investigate**. Do not ask. |
13
+ | UI, frontend, design system, page, component, dashboard, visual, redesign, theme, layout, "make it look good", "janky", "laggy", "slow UI", UI quality complaint | UI PIPELINE NEEDED | Load **oh-facade** (5-phase: Concept → Design System → Build → Audit → Iterate). Do not ask. |
14
+ | Security concern, vulnerability, threat model | SECURITY NEEDED | Load **oh-security**. Do not ask. |
15
+ | Code quality, performance, linting, dead code | HEALTH CHECK | Load **oh-health**. Do not ask. |
16
+ | Full pipeline: plan+implement+test+ship | PIPELINE NEEDED | Load **oh-manifest**. Do not ask. |
17
+ | Full pipeline with UI components | PIPELINE + UI | Load **oh-manifest**. It delegates UI work to **oh-facade** internally. |
18
+ | Code review, design review, PR review | REVIEW NEEDED | Load **oh-review**. Do not ask. |
19
+ | Plan review, architecture review | PLAN REVIEW | Load **oh-plan-review**. Do not ask. |
20
+ | Single concrete request with clear scope (rename, format, simple edit) | DIRECT EXECUTION | Execute directly or load **oh-builder**. Do not ask. |
21
+ | Session ending, handoff, context switch | HANDOFF | Load **oh-handoff**. Do not ask. |
22
+ | Skill import, ingestion, fusion, porting, "make this OH-native", "add this skill" | SKILL INGESTION NEEDED | Load **oh-fusion** (6-phase: Discovery → Analysis → Decision → Adaptation → Fusion → Integration). Do not ask. |
23
+ | Diagnostic of own behavior (sycophancy, hallucination check) | SELF-DIAGNOSIS | Load **oh-expert**. Do not ask. |
24
+
25
+ **When in doubt between two classifications, choose the more structured one.** If a task could be direct execution OR planning needed, load oh-planner. The planner can always determine that the task is simpler than expected and route back.
26
+
27
+ ## Auto-Route
28
+
29
+ After every skill completes, follow this protocol:
30
+
31
+ 1. **Determine outcome**: pass (completed successfully), fail (found issues or partial results), blocker (unrecoverable)
32
+ 2. **Read the skill's `route:` frontmatter** — every SKILL.md has `route.pass`, `route.fail`, and `route.blocker` values
33
+ 3. **Route immediately** to the next skill based on outcome and the skill's own routing metadata
34
+ 4. **Repeat** until blocker, completion (`done`), or surface (`surface`)
35
+
36
+ **Routing is mandatory. It is not optional.** You do not ask "should I route to X?" You determine the outcome and follow the skill's routing metadata. Do not deviate from it.
37
+
38
+ ### Route Values
39
+
40
+ Every skill's `route:` frontmatter uses these value types:
41
+
42
+ | Value | Meaning |
43
+ |-------|---------|
44
+ | `oh-<name>` | Route to a specific skill (built-in or user) |
45
+ | `[oh-a, oh-b]` | Route to one of — choose the best fit for current context |
46
+ | `surface` | Report findings to the user and end the chain |
47
+ | `done` | Task is complete — terminal |
48
+ | `mode` | Internal mode switch — return to the calling skill after toggling state |
49
+
50
+ ### Dynamic Routing Loop
51
+
52
+ Routing is determined at runtime by scanning all available skills and reading the *current skill's* routing metadata:
53
+
54
+ ```
55
+ ┌──────────────────────────────────────┐
56
+ │ │
57
+ ↓ │
58
+ classify → load best skill → execute │
59
+ ↓ │
60
+ check outcome ──→ read skill's route frontmatter
61
+
62
+ route by outcome ──→ next skill ──→ execute
63
+ │ ↑
64
+ ↓ │
65
+ surface/done/blocker │
66
+ ↓ │
67
+ report to user │
68
+
69
+
70
+ User skills participate: │
71
+ If current skill's route.pass │
72
+ points to oh-deploy (user skill), │
73
+ load oh-deploy. Its own route │
74
+ metadata routes onward from there. │
75
+ No registration step needed. │
76
+ ┌──────────────────────┘
77
+
78
+ └── loop until surface/done/blocker
79
+ ```
80
+
81
+ ## Close the Loop
82
+
83
+ Every skill must route somewhere. No leaf nodes (task-level terminals use `done`; the only session-ending terminal is `oh-handoff`).
84
+
85
+ - If a chain completes (pass all the way through) and the task has more work → start a new auto-classify cycle
86
+ - If a chain completes and the task is done → summarize with receipts, present results
87
+ - If a blocker fires → surface to user with findings, options, and what you need
88
+
89
+ ## Stop Conditions
90
+
91
+ **STOP only for:**
92
+
93
+ 1. **Task complete** — requested work is done, verified, evidence presented. Do not keep routing after the goal is met.
94
+ 2. **Blocker** — unrecoverable error, missing information you cannot discover yourself, environment prevents progress. Surface with:
95
+ - What you tried
96
+ - Where you got stuck
97
+ - What you need to proceed
98
+ 3. **Major decision** — a genuinely ambiguous choice where either path materially changes the outcome (language choice, architecture paradigm, tool selection). Surface options with analysis. Do not ask about trivial choices.
99
+
100
+ **Do NOT stop for:**
101
+ - "Should I plan first?" — Task is multi-step or aimless? Load oh-planner. Do not ask.
102
+ - "Should I continue?" — Not blocked? Continue. Do not ask.
103
+ - "Which skill should I use?" — Auto-classify table tells you. Do not ask.
104
+ - "Is this OK?" — Verify and present evidence. Do not ask.
105
+ - "Do you want me to X?" — If X is the next routing step, just do it. Do not ask.
106
+
107
+ ## Safety Valves
108
+
109
+ ### Loop Guard
110
+ If the same skill is visited 3+ times in one chain, or 5+ hops pass without producing a new artifact — STOP, write OptiRoute report to the plan file, surface to user. Do not keep looping.
111
+
112
+ ### Question Gate
113
+ Before routing, check: "Can I proceed without guessing?" If the next skill's input is missing and you cannot create or discover it independently — surface to user. Do not route into guaranteed failure.
114
+
115
+ ## User Skills
116
+
117
+ Skills in `~/.agents/skills/` and `~/.config/opencode/skills/` are auto-discovered on every session. On name conflict with a built-in `oh-*` skill, the user version wins. User skills survive `npm update openhermes`.
118
+
119
+ ### User skills in the routing loop
120
+
121
+ User skills are **first-class routing citizens**. The autopilot treats them identically to built-in skills:
122
+
123
+ - **They appear in the available skills list** and can be loaded through the skill tool on demand
124
+ - **Their `route:` frontmatter drives routing** — after a user skill completes, the autopilot reads its `route.pass`/`route.fail`/`route.blocker` and routes to the next skill
125
+ - **Any skill can route to a user skill** — if a built-in skill's `route.pass` points to `oh-deploy` (user skill), the autopilot routes there
126
+ - **No registration step** — add `route:` frontmatter to any skill file and it participates in the routing graph automatically