openhermes 4.3.0 → 4.9.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.
Files changed (96) hide show
  1. package/CONTEXT.md +9 -0
  2. package/README.md +26 -15
  3. package/bootstrap.ts +161 -124
  4. package/harness/agents/oh-browser.md +97 -0
  5. package/harness/agents/oh-builder.md +78 -0
  6. package/harness/agents/oh-facade.md +75 -0
  7. package/harness/agents/oh-fusion.md +45 -0
  8. package/harness/agents/oh-gauntlet.md +71 -0
  9. package/harness/agents/oh-grill.md +71 -0
  10. package/harness/agents/oh-investigate.md +60 -0
  11. package/harness/agents/oh-manifest.md +95 -0
  12. package/harness/agents/oh-plan-review.md +40 -0
  13. package/harness/agents/oh-planner.md +50 -0
  14. package/harness/agents/oh-refactor.md +37 -0
  15. package/harness/agents/oh-retro.md +46 -0
  16. package/harness/agents/oh-review.md +85 -0
  17. package/harness/agents/oh-security.md +83 -0
  18. package/harness/agents/oh-ship.md +76 -0
  19. package/harness/agents/oh-skill-craft.md +38 -0
  20. package/harness/agents/openhermes.md +107 -53
  21. package/harness/codex/AUTOPILOT.md +143 -91
  22. package/harness/codex/CHARTER.md +81 -0
  23. package/harness/commands/oh-doctor.md +193 -14
  24. package/harness/instructions/SHELL.md +76 -0
  25. package/harness/skills/oh-ascii/DEEP.md +292 -0
  26. package/harness/skills/oh-ascii/SKILL.md +31 -0
  27. package/harness/skills/oh-ascii/scripts/check_ascii_alignment.py +596 -0
  28. package/harness/skills/oh-browser/DEEP.md +54 -0
  29. package/harness/skills/oh-browser/SKILL.md +30 -0
  30. package/harness/skills/oh-builder/DEEP.md +63 -0
  31. package/harness/skills/oh-builder/SKILL.md +12 -90
  32. package/harness/skills/oh-expert/DEEP.md +85 -0
  33. package/harness/skills/oh-expert/SKILL.md +13 -106
  34. package/harness/skills/oh-facade/DEEP.md +182 -0
  35. package/harness/skills/oh-facade/SKILL.md +15 -279
  36. package/harness/skills/oh-freeze/DEEP.md +18 -0
  37. package/harness/skills/oh-freeze/SKILL.md +10 -19
  38. package/harness/skills/oh-full-output/DEEP.md +25 -0
  39. package/harness/skills/oh-full-output/SKILL.md +12 -65
  40. package/harness/skills/oh-fusion/DEEP.md +120 -0
  41. package/harness/skills/oh-fusion/SKILL.md +17 -295
  42. package/harness/skills/oh-gauntlet/DEEP.md +77 -0
  43. package/harness/skills/oh-gauntlet/SKILL.md +13 -105
  44. package/harness/skills/oh-grill/DEEP.md +51 -0
  45. package/harness/skills/oh-grill/SKILL.md +12 -63
  46. package/harness/skills/oh-guard/DEEP.md +19 -0
  47. package/harness/skills/oh-guard/SKILL.md +10 -24
  48. package/harness/skills/oh-handoff/DEEP.md +48 -0
  49. package/harness/skills/oh-handoff/SKILL.md +13 -23
  50. package/harness/skills/oh-health/DEEP.md +74 -0
  51. package/harness/skills/oh-health/SKILL.md +13 -76
  52. package/harness/skills/oh-init/DEEP.md +85 -0
  53. package/harness/skills/oh-init/SKILL.md +13 -127
  54. package/harness/skills/oh-investigate/DEEP.md +171 -0
  55. package/harness/skills/oh-investigate/SKILL.md +13 -66
  56. package/harness/skills/oh-issue/DEEP.md +21 -0
  57. package/harness/skills/oh-issue/SKILL.md +11 -27
  58. package/harness/skills/oh-learn/DEEP.md +44 -0
  59. package/harness/skills/oh-learn/SKILL.md +12 -83
  60. package/harness/skills/oh-manifest/DEEP.md +92 -0
  61. package/harness/skills/oh-manifest/SKILL.md +11 -108
  62. package/harness/skills/oh-plan-review/DEEP.md +90 -0
  63. package/harness/skills/oh-plan-review/SKILL.md +13 -115
  64. package/harness/skills/oh-planner/DEEP.md +172 -0
  65. package/harness/skills/oh-planner/SKILL.md +12 -149
  66. package/harness/skills/oh-prd/DEEP.md +45 -0
  67. package/harness/skills/oh-prd/SKILL.md +10 -26
  68. package/harness/skills/oh-refactor/DEEP.md +122 -0
  69. package/harness/skills/oh-refactor/SKILL.md +17 -410
  70. package/harness/skills/oh-retro/DEEP.md +26 -0
  71. package/harness/skills/oh-retro/SKILL.md +12 -24
  72. package/harness/skills/oh-review/DEEP.md +87 -0
  73. package/harness/skills/oh-review/SKILL.md +11 -97
  74. package/harness/skills/oh-security/DEEP.md +83 -0
  75. package/harness/skills/oh-security/SKILL.md +14 -96
  76. package/harness/skills/oh-ship/DEEP.md +141 -0
  77. package/harness/skills/oh-ship/SKILL.md +13 -31
  78. package/harness/skills/oh-skill-craft/DEEP.md +369 -0
  79. package/harness/skills/oh-skill-craft/SKILL.md +17 -178
  80. package/harness/skills/oh-skills-link/DEEP.md +16 -0
  81. package/harness/skills/oh-skills-link/SKILL.md +10 -20
  82. package/harness/skills/oh-skills-list/DEEP.md +20 -0
  83. package/harness/skills/oh-skills-list/SKILL.md +9 -22
  84. package/harness/skills/oh-triage/DEEP.md +23 -0
  85. package/harness/skills/oh-triage/SKILL.md +8 -24
  86. package/harness/skills/oh-worktree/DEEP.md +169 -0
  87. package/harness/skills/oh-worktree/SKILL.md +32 -0
  88. package/lib/harness-resolver.ts +8 -10
  89. package/package.json +5 -3
  90. package/scripts/count-tokens.mjs +158 -0
  91. package/scripts/oh-doctor.ps1 +342 -0
  92. package/harness/codex/CONSTITUTION.md +0 -73
  93. package/harness/codex/ROUTING.md +0 -92
  94. package/harness/instructions/RUNTIME.md +0 -30
  95. package/harness/skills/oh-caveman/SKILL.md +0 -42
  96. package/lib/logger.ts +0 -75
package/CONTEXT.md CHANGED
@@ -8,6 +8,15 @@
8
8
  **Instruction** — Markdown loaded through `AGENTS.md` or `opencode.json` instructions.
9
9
  **Bootstrap** — The first-message context injected by the OpenHermes plugin.
10
10
 
11
+ ### Confidence Gate Terms
12
+ **Confidence Gate** — Phase 0.5 protocol in the autopilot loop that evaluates signal strength before routing. Bounded to 1 conversational exchange max.
13
+ **Confidence Level** — One of HIGH, MEDIUM, LOW, derived from signal axis evaluation.
14
+ **Transparent Gate** — HIGH confidence behavior: zero conversational overhead, proceed directly to Auto-Classify.
15
+ **Echo Gate** — MEDIUM confidence behavior: one-liner echo to confirm understanding, then classify.
16
+ **Question Gate** — LOW confidence behavior: one targeted question, then classify. Fallback to oh-planner on no answer.
17
+ **1 Exchange** — One user response to one orchestrator prompt. The gate is bounded to exactly 0 (HIGH) or 1 (MEDIUM/LOW) exchanges.
18
+ **Signal** — Evidence in user input used to evaluate confidence across 6 axes (domain vocabulary, deliverable clarity, scope, ambiguity, file reference, domain count).
19
+
11
20
  ## Relationships
12
21
  - OpenHermes contains many Skills, Commands, Agents, and Instructions.
13
22
  - Skills are invoked on demand.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <p align="center">
2
2
  <h1 align="center">⟳ OpenHermes</h1>
3
- <p align="center"><b>Closed loop. Zero permission.</b><br>
4
- <i>The AI orchestrator that never asks "should I continue?" it just routes.</i></p>
3
+ <p align="center"><b>Pragmatic. Task-focused. Concise.</b><br>
4
+ <i>The AI orchestrator that never stalls it classifies, delegates, and routes.</i></p>
5
5
  </p>
6
6
 
7
7
  <p align="center">
@@ -17,7 +17,7 @@
17
17
 
18
18
  OpenHermes doesn't.
19
19
 
20
- Drop it into OpenCode. Get a self-driving pipeline: auto-classify every request, delegate to specialists, route results automatically. No "can I?", no "shall I?", no "what next?" — just execution until the job is done.
20
+ Drop it into OpenCode. Get a closed-loop pipeline: auto-classify every request, delegate to specialists, route results automatically. No "can I?", no "shall I?", no "what next?" — just concise execution until the job is done.
21
21
 
22
22
  ```json
23
23
  { "plugin": ["openhermes@git+https://github.com/nathwn12/openhermes.git"] }
@@ -55,12 +55,20 @@ One sentence. Nine automated steps. Each skill loaded on demand, executed in iso
55
55
 
56
56
  ---
57
57
 
58
- ### Three safety layers
58
+ ### Four safety layers
59
59
 
60
60
  The loop runs unsupervised because these never turn off:
61
61
 
62
- - **🔁 Loop Guard** — stops if the same skill fires 3+ times or 5+ hops produce no progress
62
+ - **🔁 Loop Guard** — stops if the same skill fires 5+ times or 8+ hops produce no progress
63
63
  - **❓ Question Gate** — never routes into uncertainty; surfaces if input is missing
64
+ - **💬 Confidence Gate** — calibrates whether to skip, echo, or ask before classifying
65
+
66
+ ```
67
+ HIGH ──→ classify silently (transparent gate)
68
+ MEDIUM ──→ echo + confirm, then classify
69
+ LOW ──→ ask + classify (defaults to oh-planner)
70
+ ```
71
+
64
72
  - **📋 Auto-Handoff** — writes a structured session artifact before context switches
65
73
 
66
74
  ---
@@ -69,15 +77,16 @@ The loop runs unsupervised because these never turn off:
69
77
 
70
78
  | Capability | Why it matters |
71
79
  |---|---|
72
- | **Self-driving loop** | Type once. OpenHermes classifies, delegates, and routes — no pauses, no asking permission. |
73
- | **29 specialist skills** | Planning → building → testing → security → review → shipping → retro. Every dev cycle phase. |
80
+ | **Self-driving loop** | Type once. OpenHermes classifies, delegates, and routes — no pauses, no asking permission, no verbosity. |
81
+ | **31 specialist skills** | Planning → building → testing → browser → security → review → shipping → retro. Every dev cycle phase. |
74
82
  | **Auto-detected user skills** | Drop a skill in `~/.agents/skills/`. OpenHermes finds it. Same name as a built-in? Your version wins. Survives `npm update`. |
75
83
  | **`/oh-doctor`** | Verify plugin load, skill discovery, command registration, config safety. |
76
84
  | **`/oh-log`** | Session log — routing hops, skill loads, compaction events. |
77
- | **Shared operating model** | CONSTITUTION + RUNTIME + CONTEXT + ETHOS injected every session. Every interaction grounded in the same rules. |
85
+ | **Shared operating model** | CHARTER + AUTOPILOT + CONTEXT + ETHOS injected every session. Every interaction grounded in the same rules. |
86
+ | **CORE/DEEP skill format** | Every skill is a two-file system: CORE (SKILL.md) handles 80% of passes in one read. DEEP.md loads on demand for hard cases. |
78
87
  | **Plan file storage** | `~/.local/share/opencode/openhermes/plans/`. Survives `npm update`. |
79
88
 
80
- ## 29 skills — three tiers
89
+ ## 31 skills — three tiers
81
90
 
82
91
  ### Tier 4 — Pipeline orchestrators
83
92
  Full multi-phase workflows:
@@ -95,20 +104,23 @@ Span multiple phases and coordinate other skills:
95
104
 
96
105
  | Skill | Purpose |
97
106
  |---|---|
98
- | **oh-planner** | Brainstorm, architect, autoplan, decision pipeline |
107
+ | **oh-browser** | Browser automation via agent-browser CLI. Navigate pages, fill forms, take screenshots, scrape data, test web apps. |
99
108
  | **oh-grill** | Stress-test plans through relentless Socratic questioning |
100
109
  | **oh-plan-review** | Multi-lens review: Engineering, Design, DX, Strategy |
110
+ | **oh-planner** | Brainstorm, architect, autoplan, decision pipeline |
101
111
  | **oh-security** | Audit: secrets, supply chain, CI/CD, OWASP, LLM security |
102
112
  | **oh-refactor** | Surgical behavior-preserving refactoring |
103
113
  | **oh-review** | Two-axis review (Standards + Spec) in parallel sub-agents |
104
114
  | **oh-fusion** | Skill ingestion pipeline: discover → analyze → adapt → fuse → integrate |
105
115
  | **oh-retro** | Weekly retrospective — analyze commit history and patterns |
116
+ | **oh-worktree** | Workspace isolation via git worktrees. Detect existing isolation, create isolated workspaces, run project setup, verify clean baseline. |
106
117
 
107
118
  ### Tier 2 — Focused skills
108
119
  Single-purpose, one thing well:
109
120
 
110
121
  | Skill | Purpose |
111
122
  |---|---|
123
+ | **oh-ascii** | Complete ASCII diagramming: design patterns, generation, structural validation |
112
124
  | **oh-expert** | AI self-diagnosis: sycophancy, hallucination, attention dynamics |
113
125
  | **oh-full-output** | Override truncation, ban placeholders, enforce complete generation |
114
126
  | **oh-health** | Code quality dashboard: tools, composite score, trend |
@@ -119,7 +131,6 @@ Single-purpose, one thing well:
119
131
  | **oh-triage** | Issue triage state machine — classify, prioritise, assign |
120
132
  | **oh-issue** | Break a plan/spec/PRD into independently-grabbable issues |
121
133
  | **oh-prd** | Conversation → PRD → GitHub issue |
122
- | **oh-caveman** | Ultra-compressed mode — cut token usage ~75% |
123
134
  | **oh-freeze** | Restrict file edits to a specific directory |
124
135
  | **oh-learn** | Extract, evolve, promote session learnings as instincts |
125
136
  | **oh-guard** | Safety confirmation — warn before destructive operations |
@@ -137,13 +148,13 @@ openhermes-pkg/
137
148
  ├── ETHOS.md # Operating principles
138
149
  ├── bootstrap.ts # Plugin entry — registers everything
139
150
  ├── index.ts # Package entrypoint
140
- ├── lib/ # harness-resolver.ts, logger.ts
151
+ ├── lib/ # harness-resolver.ts
141
152
  ├── harness/
142
153
  │ ├── agents/ # Agent manifests (OpenHermes primary)
143
- │ ├── codex/ # CONSTITUTION, AUTOPILOT, ROUTING
154
+ │ ├── codex/ # CHARTER, AUTOPILOT
144
155
  │ ├── commands/ # Slash commands (/oh-doctor, /oh-log)
145
- │ ├── instructions/ # RUNTIME.md
146
- │ └── skills/ # 29 skill SKILL.md files
156
+ │ ├── instructions/ # SHELL.md
157
+ │ └── skills/ # 31 skill SKILL.md files (CORE/DEEP format)
147
158
  └── test/
148
159
  ```
149
160
 
package/bootstrap.ts CHANGED
@@ -1,17 +1,18 @@
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"
8
6
 
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
7
  const OPENHERMES_AGENT = "OpenHermes"
14
8
 
9
+ // User skill directories — auto-discovered on every session, survive npm updates
10
+ const USER_SKILL_DIRS: ReadonlyArray<string> = [
11
+ path.join(os.homedir(), ".agents", "skills"),
12
+ path.join(os.homedir(), ".config", "opencode", "skills"),
13
+ path.join(os.homedir(), ".claude", "skills"), // Claude Code backward compat
14
+ ]
15
+
15
16
  // Canonical storage under OpenCode's data directory — survives npm updates
16
17
  let _planStorageOverride: string | undefined
17
18
  export function setPlanStorageDirForTest(dir: string | undefined): void { _planStorageOverride = dir }
@@ -23,13 +24,8 @@ function getProjectName(projectDir: string): string {
23
24
  return path.basename(projectDir)
24
25
  }
25
26
 
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
27
 
32
- export { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir }
28
+ export { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir, ensurePlanFile }
33
29
 
34
30
  function parseFrontmatter(raw: string | undefined): Record<string, string> {
35
31
  const frontmatter: Record<string, string> = {}
@@ -128,9 +124,6 @@ function uniqueStrings(existing: string[] = [], additions: string[] = []): strin
128
124
  return merged
129
125
  }
130
126
 
131
- function readText(filePath: string): string {
132
- return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : ""
133
- }
134
127
 
135
128
  function regexEscape(s: string): string {
136
129
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
@@ -182,15 +175,58 @@ function ensureDir(dir: string): void {
182
175
  }
183
176
  }
184
177
 
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
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
+ }
193
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
194
230
  }
195
231
 
196
232
  export function buildCompactionContext(projectDir: string): string[] {
@@ -232,83 +268,8 @@ export function formatSessionEvent(event: SessionLifecycleEvent): { level: "info
232
268
  }
233
269
  }
234
270
 
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
271
 
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
272
 
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
273
 
313
274
  interface OpenHermesConfig {
314
275
  skills?: { paths?: string[] }
@@ -323,23 +284,26 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
323
284
  const skillsDir = path.join(hDir, "skills")
324
285
  const commandsDir = path.join(hDir, "commands")
325
286
  const agentsDir = path.join(hDir, "agents")
287
+ const client = ctx.client // SDK client for structured logging
288
+
289
+ // Safe logging — uses OpenCode SDK when available, falls back to stdout for tests
290
+ async function logToOC(level: "info" | "warn" | "error" | "debug", message: string): Promise<void> {
291
+ if (client?.app?.log) {
292
+ await client.app.log({ body: { service: "openhermes", level, message } })
293
+ } else {
294
+ console.log(`[openhermes] [${level.toUpperCase()}] ${message}`)
295
+ }
296
+ }
297
+
326
298
  // 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
299
  const userSkillPaths: string[] = []
329
300
  for (const userDir of USER_SKILL_DIRS) {
330
301
  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
- }
302
+ userSkillPaths.push(userDir)
303
+ await logToOC("info", `wired user skills from ${userDir}`)
336
304
  }
337
305
 
338
- const bootstrapContent = buildBootstrapContent(hDir, userSkillPaths)
339
306
  const compactionContext = buildCompactionContext(ctx.directory)
340
- const builtInCount = countSkills(skillsDir)
341
- const userCount = userSkillPaths.reduce((sum, d) => sum + countSkills(d), 0)
342
-
343
307
  // Ensure plan storage exists
344
308
  ensureDir(planStorageDir())
345
309
 
@@ -350,7 +314,13 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
350
314
  const allPaths = [skillsDir, ...userSkillPaths]
351
315
  config.skills.paths = uniqueStrings(config.skills.paths || [], allPaths)
352
316
 
353
- log.info(`skills: ${builtInCount} built-in + ${userCount} user (${allPaths.length} path(s))`)
317
+ await logToOC("info", `skills: ${allPaths.length} path(s)`)
318
+
319
+ // Register harness docs as native OpenCode instructions — no prompt-embedding needed
320
+ config.instructions = uniqueStrings(config.instructions ?? [], [
321
+ path.join(hDir, "codex"),
322
+ path.join(hDir, "instructions"),
323
+ ])
354
324
 
355
325
  config.command = { ...(config.command ?? {}), ...commandDefinitions(commandsDir) }
356
326
 
@@ -361,18 +331,63 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
361
331
  prompt: "You are OpenHermes.",
362
332
  }
363
333
 
334
+ // Subagent permissions — tier-4 and tier-3 get execution access but cannot spawn orchestrators
335
+ const SUBAGENT_PERMISSIONS: Record<string, Record<string, unknown>> = {
336
+ "oh-builder": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
337
+ "oh-browser": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
338
+ "oh-facade": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
339
+ "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
+ "oh-grill": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
344
+ "oh-investigate": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
345
+ "oh-plan-review": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
346
+ "oh-security": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
347
+ "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
+ "oh-retro": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
351
+ "oh-skill-craft": { bash: { "*": "allow" }, edit: "allow", read: "allow", glob: "allow", grep: "allow", task: { "oh-*": "deny" } },
352
+ }
353
+
364
354
  config.agent = {
365
355
  ...(config.agent ?? {}),
366
356
  ...loadedAgents,
357
+ // Apply permissions + hidden flag to subagents
358
+ ...Object.fromEntries(
359
+ Object.entries(loadedAgents)
360
+ .filter(([name]) => name !== OPENHERMES_AGENT)
361
+ .map(([name, agentDef]) => [
362
+ name,
363
+ {
364
+ ...agentDef,
365
+ permission: SUBAGENT_PERMISSIONS[name] ?? { bash: { "*": "deny" }, edit: "deny", read: "allow" },
366
+ // Hide routing-internal subagents from @-menu
367
+ // Only agents with existing .md files can be hidden — names without files are no-ops
368
+ ...(["oh-planner", "oh-grill", "oh-skill-craft"].includes(name) ? { hidden: true } : {}),
369
+ },
370
+ ])
371
+ ),
367
372
  [OPENHERMES_AGENT]: {
368
373
  ...openHermesAgent,
369
374
  description: openHermesAgent.description || "OpenHermes primary orchestrator",
370
375
  mode: "primary",
376
+ steps: 15, // Max agentic iterations — prevents runaway loops
371
377
  permission: {
372
- bash: { "*": "allow" },
373
- edit: "allow",
374
- read: "allow",
375
- task: { "*": "allow" },
378
+ bash: { "*": "deny" }, // CANNOT execute commands
379
+ edit: "deny", // CANNOT write/edit files
380
+ read: "allow", // CAN read for classification
381
+ glob: "allow", // CAN search for files
382
+ grep: "allow", // CAN search content
383
+ task: { "*": "allow" }, // MUST delegate via subagents
384
+ skill: "allow", // CAN load skill instructions
385
+ webfetch: "allow", // CAN fetch docs for context
386
+ question: "allow", // CAN ask user questions
387
+ websearch: "allow", // CAN search web for research context
388
+ external_directory: { // CAN read/write plan files outside worktree
389
+ "~/.local/share/opencode/openhermes/**": "allow",
390
+ },
376
391
  },
377
392
  },
378
393
  }
@@ -381,25 +396,47 @@ export const BootstrapPlugin: Plugin = async (ctx) => {
381
396
  },
382
397
 
383
398
  event: async ({ event }) => {
384
- const record = formatSessionEvent(event as SessionLifecycleEvent)
399
+ const typed = event as SessionLifecycleEvent
400
+ const record = formatSessionEvent(typed)
385
401
  if (!record) return
386
- sessionLog[record.level](record.message)
402
+ await logToOC(record.level, record.message)
403
+
404
+ // NOTE: Plan files are NOT auto-created here. The LLM agent
405
+ // creates plans on demand (see Task Flow step 1 in agent prompt).
406
+ // Auto-creation produced ghost skeletons like plan-004.
407
+
408
+ // Reset delegation depth on session start/error
409
+ if (typed.type === "session.created" || typed.type === "session.error") {
410
+ delegationDepths.delete(`delegation:${ctx.directory}`)
411
+ }
387
412
  },
388
413
 
389
414
  "experimental.session.compacting": async (_input, output) => {
390
415
  output.context.push(...compactionContext)
391
416
  },
392
417
 
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)
418
+ // Mechanical delegation loop guard prevents runaway agent nesting
419
+ "tool.execute.before": async (input, output) => {
420
+ if (input.tool === "task") {
421
+ // Track delegation depth per project (one session per project at a time)
422
+ const depthKey = `delegation:${ctx.directory}`
423
+ const currentDepth = (delegationDepths.get(depthKey) ?? 0) + 1
424
+ delegationDepths.set(depthKey, currentDepth)
425
+
426
+ if (currentDepth >= 10) {
427
+ const errOutput = output as { args: unknown; isError?: boolean; content?: unknown[] }
428
+ errOutput.isError = true
429
+ errOutput.content = [{
430
+ type: "text",
431
+ text: "LOOP GUARD: Delegation depth exceeded (max 10). " +
432
+ "Surface to orchestrator with findings and stop delegating."
433
+ }]
434
+ }
402
435
  }
403
436
  },
437
+
404
438
  }
405
439
  }
440
+
441
+ // Module-level delegation depth tracker — reset per project session
442
+ const delegationDepths = new Map<string, number>()
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: oh-browser
3
+ description: "Browser automation via agent-browser CLI. Navigate pages, fill forms, click buttons, take screenshots, extract data, test web apps. Use when the user needs to interact with websites, automate browser tasks, scrape data, or test web applications."
4
+ mode: subagent
5
+ ---
6
+
7
+ ## Shell Pre-flight (Windows)
8
+
9
+ You are on Windows. Before ANY command execution, detect your shell:
10
+ - `$PSVersionTable` exists → PowerShell (`powershell` or `pwsh`)
11
+ - `%CMDCMDLINE%` is set → CMD
12
+ - `$0` or `$BASH` → Bash (Git Bash)
13
+
14
+ Operation → required shell:
15
+ - File ops (`Remove-Item`, `New-Item`), scoop, `.ps1` scripts, `$env:VAR` → **PowerShell**
16
+ - `git`, `bun`, `npm`, `node` → **any shell** (all work)
17
+ - `rm -rf`, `make`, Unix tools → **Git Bash**
18
+ - `.bat`/`.cmd` files → **CMD**
19
+
20
+ Wrong shell? Switch:
21
+ - → PowerShell: `powershell.exe -NoProfile -Command "..."`
22
+ - → Git Bash: `& "C:\Program Files\Git\bin\bash.exe" -c "..."`
23
+ - → CMD: `cmd.exe /c "..."`
24
+
25
+ Always know before you go.
26
+
27
+ # oh-browser
28
+
29
+ Browser automation via agent-browser CLI. Fast native Rust CLI wrapping Chrome/Chromium via CDP.
30
+
31
+ ## Prerequisites
32
+
33
+ - agent-browser installed globally: `npm install -g agent-browser && agent-browser install`
34
+ - Chrome/Chromium (auto-downloaded by `agent-browser install`)
35
+ - State files contain session tokens — add to `.gitignore`, never commit
36
+
37
+ ## Workflow
38
+
39
+ 1. **Launch browser** — `agent-browser open <url>` or `agent-browser open` (blank page then navigate)
40
+ 2. **Snapshot page state** — `agent-browser snapshot` returns accessibility tree with `@eN` refs
41
+ 3. **Interact** — use `@eN` refs from snapshot:
42
+ - `agent-browser click @eN`
43
+ - `agent-browser fill @eN "value"`
44
+ - `agent-browser select @eN "option"`
45
+ - `agent-browser hover @eN`
46
+ 4. **Extract data** — `agent-browser get text @eN`, `agent-browser get html @eN`, `agent-browser screenshot`
47
+ 5. **Close** — `agent-browser close`
48
+
49
+ ## Common Patterns
50
+
51
+ - **Annotated screenshots**: `agent-browser screenshot --annotate` — overlays numbered labels matching `@eN` refs.
52
+ - **Batch execution**: `agent-browser batch "open url" "snapshot" "click @e1"` — avoids per-command startup overhead.
53
+ - **Session persistence**: `--session-name <name>` — auto-saves/restores cookies and localStorage.
54
+ - **Auth vault**: `agent-browser auth save <name> --url <url> --username <user>` — encrypted credentials.
55
+ - **Diff**: `agent-browser diff snapshot` for change detection. `agent-browser diff screenshot --baseline before.png` for visual diff.
56
+ - **Chrome profile reuse**: `--profile Default` — use existing Chrome login state.
57
+ - **Tab labeling**: `agent-browser tab new --label docs <url>` — memorable labels.
58
+ - **Parallel scrape**: Use `batch --json` with piped command arrays.
59
+
60
+ ## Common Commands Reference
61
+
62
+ | Task | Command |
63
+ |---|---|
64
+ | Open URL | `agent-browser open <url>` |
65
+ | Get page state | `agent-browser snapshot -i` |
66
+ | Click | `agent-browser click @eN` or `agent-browser click "css-selector"` |
67
+ | Type text | `agent-browser fill @eN "text"` |
68
+ | Screenshot | `agent-browser screenshot --annotate` |
69
+ | Extract text | `agent-browser get text @eN` |
70
+ | Run JS | `agent-browser eval "document.title"` |
71
+ | Wait for element | `agent-browser wait ".selector"` |
72
+ | Scroll | `agent-browser scroll down 200` |
73
+ | Multi-step | `agent-browser batch "cmd1" "cmd2" "cmd3"` |
74
+
75
+ ## Anti-patterns
76
+
77
+ - Forgetting `agent-browser install` first
78
+ - Not closing browser sessions (daemon processes leak)
79
+ - Using CSS selectors when `@eN` refs are faster
80
+ - Running individual commands instead of batch for multi-step
81
+ - Passing credentials in prompts instead of auth vault
82
+ - Committing state files with session tokens
83
+
84
+ ## Security
85
+
86
+ - Use `--allowed-domains` to restrict navigation
87
+ - Use auth vault instead of passing credentials in prompts
88
+ - Session state files contain tokens — keep in `.gitignore`
89
+ - `--content-boundaries` wraps page output in delimiters
90
+
91
+ ## Routing
92
+
93
+ | Outcome | Route |
94
+ |---------|-------|
95
+ | pass | → surface (results to user) |
96
+ | fail | → oh-browser (retry with corrected approach) |
97
+ | blocker | → surface with error details |