openhermes 4.1.0 → 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 (42) hide show
  1. package/ETHOS.md +6 -3
  2. package/LICENSE +21 -21
  3. package/README.md +109 -79
  4. package/bootstrap.ts +214 -8
  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 -70
  9. package/harness/commands/oh-log.md +18 -0
  10. package/harness/instructions/RUNTIME.md +27 -52
  11. package/harness/skills/oh-builder/SKILL.md +13 -8
  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 +9 -5
  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 +28 -94
  24. package/harness/skills/oh-investigate/SKILL.md +10 -0
  25. package/harness/skills/oh-issue/SKILL.md +9 -0
  26. package/harness/skills/oh-learn/SKILL.md +13 -4
  27. package/harness/skills/oh-manifest/SKILL.md +15 -10
  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 +11 -4
  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/lib/harness-resolver.ts +2 -2
  41. package/lib/logger.ts +7 -1
  42. package/package.json +6 -3
package/ETHOS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenHermes Ethos
2
2
 
3
- Four immutable principles. Every skill, every command, every session.
3
+ Five immutable principles. Every skill, every command, every session.
4
4
 
5
5
  ## Native First
6
6
  OpenCode-native loading over manual copying or hidden state.
@@ -11,5 +11,8 @@ Every file earns its keep. Prefer markdown when behavior is declarative.
11
11
  ## Skills Over Glue
12
12
  Behavior lives in `SKILL.md`, `commands/*.md`, and `agents/*.md`.
13
13
 
14
- ## Delegate Hard Work
15
- Subagents for substantive work. Main context orchestrates and verifies.
14
+ ## Always Delegate Never Execute
15
+ OpenHermes orchestrates and reports. Sub-agents execute. OpenHermes never writes code, runs tests, or touches files directly.
16
+
17
+ ## Closed Loop
18
+ Auto-classify. Auto-route. Auto-execute. Only stop for blockers. No dead ends, no asking permission, no wasted cycles.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 nathwn12
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nathwn12
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,106 +1,130 @@
1
1
  <p align="center">
2
- <h1 align="center">&#9764; OpenHermes</h1>
3
- <p align="center"><i>Plan. Build. Ship. One plugin.</i></p>
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>
4
5
  </p>
5
6
 
6
7
  <p align="center">
7
8
  <a href="https://www.npmjs.com/package/openhermes"><img src="https://img.shields.io/npm/v/openhermes?style=for-the-badge&label=version&color=FFD700" alt="npm version"></a>
8
9
  <a href="https://github.com/nathwn12/openhermes/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="License: MIT"></a>
9
10
  <a href="https://opencode.ai"><img src="https://img.shields.io/badge/runs%20on-OpenCode-6366f1?style=for-the-badge" alt="Runs on OpenCode"></a>
11
+ <a href="https://github.com/nathwn12/openhermes"><img src="https://img.shields.io/badge/⭐%20star%20on-GitHub-181717?style=for-the-badge" alt="Star on GitHub"></a>
10
12
  </p>
11
13
 
12
14
  ---
13
15
 
14
- OpenHermes is an OpenCode plugin that gives your agent a complete skill system out of the box. Add one line to `opencode.json`, get 25 skills, a primary orchestrator, and a shared operating model.
16
+ **AI coding assistants stall.** They ask permission. They lose context mid-session. They wait for you to unstick them.
17
+
18
+ OpenHermes doesn't.
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.
15
21
 
16
22
  ```json
17
23
  { "plugin": ["openhermes@git+https://github.com/nathwn12/openhermes.git"] }
18
24
  ```
19
25
 
20
- No setup. No file copying. No extra dependencies.
26
+ To install from `dev` (latest features, may be unstable):
27
+
28
+ ```json
29
+ { "plugin": ["openhermes@git+https://github.com/nathwn12/openhermes.git#dev"] }
30
+ ```
21
31
 
22
32
  ---
23
33
 
24
- ## What you get
34
+ ## One sentence. Nine steps.
25
35
 
26
- ### Seven core skills
36
+ Add the plugin. Restart. Type:
27
37
 
28
- | Skill | What it does |
29
- |---|---|
30
- | **oh-planner** | Brainstorm, analyze architecture, run strategy reviews, auto-decide 90% of questions with gstack decision principles. Produces a consumable plan artifact. |
31
- | **oh-builder** | Prototype, TDD red-green-refactor, design interfaces in parallel sub-agents, implement from plan. Vertical tracer bullets, one test at a time. |
32
- | **oh-manifest** | Full build loop: planner → builder → verify → loop until done or a real blocker is surfaced. Auto-resolves intermediate questions; only interrupts you for genuine blockers. |
33
- | **oh-gauntlet** | Multi-axis testing gauntlet: unit tests, dual-axis review (Standards + Spec in parallel sub-agents), edge case sweep, QA tier, canary post-deploy. |
38
+ > *"Plan a CLI tool for managing dotfiles."*
34
39
 
35
- These seven form a pipeline: **think plan → build → test → ship → secure → monitor**. Each produces artifacts the next consumes.
40
+ You see output. Behind the scenes, this runs:
36
41
 
37
- ### Twenty-five skills total
42
+ | # | What fires | What it does |
43
+ |---|---|---|
44
+ | **1** | `AUTOPILOT.md` decision matrix | Multi-step, vague → `PLANNING NEEDED` |
45
+ | **2** | `oh-planner` | Brainstorm mode: architecture, user flow, risks |
46
+ | **3** | `oh-planner` → `oh-grill` | Plan passes → stress-test it |
47
+ | **4** | `oh-grill` → `oh-planner` (revise) | Gaps found → planner revises |
48
+ | **5** | `oh-planner` → `oh-manifest` | Plan solid → enter build loop |
49
+ | **6** | `oh-planner` → `oh-builder` → `oh-gauntlet` | Implement → test → review → loop |
50
+ | **7** | `oh-gauntlet` → `oh-ship` | Tests pass → PR pipeline |
51
+ | **8** | `oh-ship` → `oh-retro` | Shipped → retrospective |
52
+ | **9** | `oh-retro` → `oh-planner` | Ready for the next cycle |
38
53
 
39
- | Skill | Purpose |
40
- |---|---|
41
- | oh-planner, oh-builder, oh-manifest, oh-gauntlet | Core pipeline (above) |
42
- | oh-expert | AI self-diagnosis vocabulary — sycophancy, hallucination type, attention degradation |
43
- | oh-grill | Stress-test plans through Socratic questioning; optionally updates CONTEXT.md, ADRs, and extracts ubiquitous language |
44
- | oh-plan-review | Multi-lens plan review: Engineering, Design, DX, Strategy |
45
- | oh-security | Security audit: secrets archaeology, supply chain, CI/CD, OWASP, STRIDE, LLM security |
46
- | oh-health | Code quality dashboard: wraps tools, composite score, trend tracking |
47
- | oh-investigate | Systematic bug diagnosis |
48
- | oh-handoff | Compact session into structured handoff artifact for another agent |
49
- | oh-skill-craft | Create new skills for the harness (meta-skill) |
50
- | oh-init | Initialize project: scaffold CONTEXT.md, AGENTS.md, ADRs, issue tracker config, triage labels |
51
- | oh-retro | Retrospective after shipping |
52
- | oh-review | Two-axis review (Standards + Spec) in parallel sub-agents + architecture deepening |
53
- | oh-ship | PR, version bump, changelog, post-ship docs sync |
54
- | oh-triage | Issue triage state machine |
55
- | oh-issue | Break plans into vertical-slice issues |
56
- | oh-prd | Write structured PRDs |
57
- | oh-caveman | Ultra-compressed response mode |
58
- | oh-freeze | Restrict file edits to a specific directory |
59
- | oh-learn | Learn patterns from the codebase |
60
- | oh-guard | Safety confirmations for destructive operations |
61
- | oh-skills-link | Verify skills discovery |
62
- | oh-skills-list | List available skills |
63
-
64
- ### One orchestrator agent
65
-
66
- OpenHermes is the default primary agent — a hub-and-spoke commander that delegates to skills, spawns sub-agents for isolated context, and surfaces blockers instead of silently retrying.
67
-
68
- ### One diagnostic command
69
-
70
- `/oh-doctor` — inspect plugin load, skills discovery, command/agent registration, and config safety.
54
+ One sentence. Nine automated steps. Each skill loaded on demand, executed in isolation, routed to the next specialist. **Auto-classify, delegate, route, repeat.** That's the entire model.
71
55
 
72
56
  ---
73
57
 
74
- ## How it works
58
+ ### Three safety layers
75
59
 
76
- OpenHermes loads through OpenCode's native plugin system. On install:
60
+ The loop runs unsupervised because these never turn off:
77
61
 
78
- 1. `config.skills.paths` is pointed at the package-local `harness/skills/` skills load on demand through the `skill` tool, no preloading
79
- 2. Commands from `harness/commands/` register as slash commands
80
- 3. The agent manifest in `harness/agents/` sets OpenHermes as the primary orchestrator
81
- 4. Instructions from `CONSTITUTION.md`, `RUNTIME.md`, `CONTEXT.md`, and `ETHOS.md` are injected into every session
82
-
83
- Everything is package-local. Nothing is copied into your global config.
62
+ - **🔁 Loop Guard** stops if the same skill fires 3+ times or 5+ hops produce no progress
63
+ - **❓ Question Gate** never routes into uncertainty; surfaces if input is missing
64
+ - **📋 Auto-Handoff** writes a structured session artifact before context switches
84
65
 
85
66
  ---
86
67
 
87
- ## OptiRoute: Smart Auto-Routing Protocol
68
+ ## What you get
69
+
70
+ | Capability | Why it matters |
71
+ |---|---|
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. |
74
+ | **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
+ | **`/oh-doctor`** | Verify plugin load, skill discovery, command registration, config safety. |
76
+ | **`/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. |
78
+ | **Plan file storage** | `~/.local/share/opencode/openhermes/plans/`. Survives `npm update`. |
88
79
 
89
- Every skill routes to the next based on outcome pass, fail, or blocker. OptiRoute is a guard layer on top of that graph. Three rules:
80
+ ## 29 skillsthree tiers
90
81
 
91
- **Loop guard.** Tracks routing depth. If the same skill is visited 3+ times in one chain, or 5+ hops pass without measurable progress (new artifact, changed target), routing stops. No infinite loops.
82
+ ### Tier 4 Pipeline orchestrators
83
+ Full multi-phase workflows:
92
84
 
93
- **Question gate.** Before each routing hop, the protocol checks whether the next skill's inputs are satisfied. If the task is underspecified or information is missing, it asks the user instead of routing into uncertainty.
85
+ | Skill | Purpose |
86
+ |---|---|
87
+ | **oh-manifest** | Plan → build → verify → loop until done or blocker |
88
+ | **oh-facade** | Concept → design system → build → audit → iterate (full UI pipeline) |
89
+ | **oh-gauntlet** | Multi-axis testing: unit, integration, edge cases, dual-axis review |
90
+ | **oh-builder** | ALL-arounder builder — prototype, TDD, implement from plan |
91
+ | **oh-ship** | Deploy and PR pipeline: test, bump, changelog, PR, deploy, verify |
94
92
 
95
- **Auto-handoff.** When the loop guard triggers, it writes an OptiRoute report into `plan.md` the routing chain, the trigger threshold, current state, and the blocker. Then it surfaces the blocker and awaits direction.
93
+ ### Tier 3Cross-cutting skills
94
+ Span multiple phases and coordinate other skills:
96
95
 
97
- ```md
98
- OPTIROUTE STOP: 5-hop ceiling reached, no progress
99
- Chain: planner grill planner builder → gauntlet → planner
100
- See .opencode/plan.md for full report
101
- ```
96
+ | Skill | Purpose |
97
+ |---|---|
98
+ | **oh-planner** | Brainstorm, architect, autoplan, decision pipeline |
99
+ | **oh-grill** | Stress-test plans through relentless Socratic questioning |
100
+ | **oh-plan-review** | Multi-lens review: Engineering, Design, DX, Strategy |
101
+ | **oh-security** | Audit: secrets, supply chain, CI/CD, OWASP, LLM security |
102
+ | **oh-refactor** | Surgical behavior-preserving refactoring |
103
+ | **oh-review** | Two-axis review (Standards + Spec) in parallel sub-agents |
104
+ | **oh-fusion** | Skill ingestion pipeline: discover → analyze → adapt → fuse → integrate |
105
+ | **oh-retro** | Weekly retrospective — analyze commit history and patterns |
106
+
107
+ ### Tier 2 — Focused skills
108
+ Single-purpose, one thing well:
102
109
 
103
- No loops. No guessing. No silent death.
110
+ | Skill | Purpose |
111
+ |---|---|
112
+ | **oh-expert** | AI self-diagnosis: sycophancy, hallucination, attention dynamics |
113
+ | **oh-full-output** | Override truncation, ban placeholders, enforce complete generation |
114
+ | **oh-health** | Code quality dashboard: tools, composite score, trend |
115
+ | **oh-investigate** | Systematic bug diagnosis with root cause investigation |
116
+ | **oh-handoff** | Compact session state → structured handoff document |
117
+ | **oh-skill-craft** | Create new agent skills with frontmatter and bundled resources |
118
+ | **oh-init** | Wire AGENTS.md, domain docs, issue tracker, triage labels |
119
+ | **oh-triage** | Issue triage state machine — classify, prioritise, assign |
120
+ | **oh-issue** | Break a plan/spec/PRD into independently-grabbable issues |
121
+ | **oh-prd** | Conversation → PRD → GitHub issue |
122
+ | **oh-caveman** | Ultra-compressed mode — cut token usage ~75% |
123
+ | **oh-freeze** | Restrict file edits to a specific directory |
124
+ | **oh-learn** | Extract, evolve, promote session learnings as instincts |
125
+ | **oh-guard** | Safety confirmation — warn before destructive operations |
126
+ | **oh-skills-link** | Verify OpenCode discovers the skill directory |
127
+ | **oh-skills-list** | List all available `oh-*` skills |
104
128
 
105
129
  ---
106
130
 
@@ -108,28 +132,34 @@ No loops. No guessing. No silent death.
108
132
 
109
133
  ```
110
134
  openhermes-pkg/
111
- ├── AGENTS.md # Skill/command/agent inventory
112
- ├── CONTEXT.md # Shared language
135
+ ├── AGENTS.md # User-side routing overlay
136
+ ├── CONTEXT.md # Shared domain language
113
137
  ├── ETHOS.md # Operating principles
114
- ├── bootstrap.ts # Plugin loader — registers everything
138
+ ├── bootstrap.ts # Plugin entry — registers everything
115
139
  ├── index.ts # Package entrypoint
140
+ ├── lib/ # harness-resolver.ts, logger.ts
116
141
  ├── harness/
117
- │ ├── agents/ # Agent manifests (OpenHermes)
118
- │ ├── codex/ # CONSTITUTION.md
119
- │ ├── commands/ # Slash command manifests (/oh-doctor)
142
+ │ ├── agents/ # Agent manifests (OpenHermes primary)
143
+ │ ├── codex/ # CONSTITUTION, AUTOPILOT, ROUTING
144
+ │ ├── commands/ # Slash commands (/oh-doctor, /oh-log)
120
145
  │ ├── instructions/ # RUNTIME.md
121
- │ └── skills/ # 25 skill SKILL.md files
146
+ │ └── skills/ # 29 skill SKILL.md files
122
147
  └── test/
123
148
  ```
124
149
 
150
+ Plan files: `~/.local/share/opencode/openhermes/plans/<project>-plan-<nnn>.md`
151
+
125
152
  ---
126
153
 
127
- ## Inspiration
154
+ ## Get started — 60 seconds
128
155
 
129
- OpenHermes merges high-signal patterns from:
156
+ 1. Add the plugin line to `opencode.json`
157
+ 2. Restart or reload OpenCode
158
+ 3. Run `/oh-doctor` to verify everything loaded
159
+ 4. Type *any* prompt — "plan a feature", "investigate this bug", "refactor this module"
160
+
161
+ The first time you see OpenHermes auto-route to a specialist skill without you asking — you'll feel the loop.
162
+
163
+ ---
130
164
 
131
- - **superpowers**skill loading model (config.skills.paths + bootstrap injection)
132
- - **dictionary-of-ai-coding** — shared vocabulary for agent self-diagnosis
133
- - **skills (mattpocock)** — TDD discipline, write-a-skill meta, design-an-interface parallel sub-agents, dual-axis review
134
- - **gstack** — preamble-tier seniority, artifact-chain pipeline, decision principles
135
- - **opencode-orchestrator** — hub-and-spoke delegation, session pool discipline, pipelined verification
165
+ **Star on [GitHub](https://github.com/nathwn12/openhermes)** bug reports, feature requests, and contributions welcome.
package/bootstrap.ts CHANGED
@@ -1,15 +1,34 @@
1
1
  import path from "node:path"
2
2
  import fs from "node:fs"
3
+ import os from "node:os"
3
4
  import { fileURLToPath } from "node:url"
4
5
  import type { Plugin } from "@opencode-ai/plugin"
5
6
  import { createLogger } from "./lib/logger.ts"
6
7
  import { getHarnessDir, setHarnessRootForTest, resolveHarnessRoot } from "./lib/harness-resolver.ts"
7
8
 
8
9
  const log = createLogger("bootstrap")
10
+ const sessionLog = createLogger("session")
9
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
10
12
  const BOOTSTRAP_MARKER = "OPENHERMES_BOOTSTRAP"
11
13
  const OPENHERMES_AGENT = "OpenHermes"
12
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
+
13
32
  export { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir }
14
33
 
15
34
  function parseFrontmatter(raw: string | undefined): Record<string, string> {
@@ -113,23 +132,179 @@ function readText(filePath: string): string {
113
132
  return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : ""
114
133
  }
115
134
 
116
- function buildBootstrapContent(hDir: string): string {
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 {
117
284
  const parts = [
118
285
  `<${BOOTSTRAP_MARKER}>`,
119
286
  `You are OpenHermes.`,
120
- `OpenHermes is OpenCode-native: load skills on demand, prefer subagents for substantive work, and keep the surface small.`,
287
+ `OpenHermes is OpenCode-native: load skills on demand, always delegate, never execute tasks directly, and keep the surface small.`,
121
288
  `Durable state is removed for now. Do not invent a persistence layer unless the user explicitly asks for one later.`,
122
289
  ]
123
290
 
291
+ const autopilot = readText(path.join(hDir, "codex", "AUTOPILOT.md"))
124
292
  const constitution = readText(path.join(hDir, "codex", "CONSTITUTION.md"))
125
293
  const runtime = readText(path.join(hDir, "instructions", "RUNTIME.md"))
126
294
  const context = readText(path.join(__dirname, "CONTEXT.md"))
127
295
  const ethos = readText(path.join(__dirname, "ETHOS.md"))
128
296
 
297
+ if (autopilot) parts.push(`<AUTOPILOT>\n${autopilot}\n</AUTOPILOT>`)
129
298
  if (constitution) parts.push(`<CONSTITUTION>\n${constitution}\n</CONSTITUTION>`)
130
299
  if (runtime) parts.push(`<RUNTIME>\n${runtime}\n</RUNTIME>`)
131
300
  if (context) parts.push(`<CONTEXT>\n${context}\n</CONTEXT>`)
132
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
+
133
308
  parts.push(`</${BOOTSTRAP_MARKER}>`)
134
309
 
135
310
  return parts.join("\n\n")
@@ -143,17 +318,39 @@ interface OpenHermesConfig {
143
318
  default_agent?: string
144
319
  }
145
320
 
146
- export const BootstrapPlugin: Plugin = async () => {
321
+ export const BootstrapPlugin: Plugin = async (ctx) => {
147
322
  const hDir = getHarnessDir()
148
323
  const skillsDir = path.join(hDir, "skills")
149
324
  const commandsDir = path.join(hDir, "commands")
150
325
  const agentsDir = path.join(hDir, "agents")
151
- const bootstrapContent = buildBootstrapContent(hDir)
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())
152
345
 
153
346
  return {
154
347
  config: async (config: OpenHermesConfig) => {
155
348
  config.skills = config.skills || {}
156
- config.skills.paths = uniqueStrings(config.skills.paths || [], [skillsDir])
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))`)
157
354
 
158
355
  config.command = { ...(config.command ?? {}), ...commandDefinitions(commandsDir) }
159
356
 
@@ -183,14 +380,23 @@ export const BootstrapPlugin: Plugin = async () => {
183
380
  config.default_agent = OPENHERMES_AGENT
184
381
  },
185
382
 
186
- "experimental.chat.messages.transform": async (_input: unknown, output: { messages?: Array<{ info?: { role?: string }; parts?: Array<{ text?: string }> }> }) => {
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 }> }> }) => {
187
394
  try {
188
395
  if (!output.messages?.length) return
189
396
  const firstUser = output.messages.find(m => m?.info?.role === "user")
190
397
  if (!firstUser?.parts?.length) return
191
398
  if (firstUser.parts.some(p => p.text?.includes(BOOTSTRAP_MARKER))) return
192
- const ref = firstUser.parts[0]
193
- firstUser.parts.unshift({ ...ref, type: "text", text: bootstrapContent })
399
+ firstUser.parts.unshift({ type: "text", text: bootstrapContent })
194
400
  } catch (err: unknown) {
195
401
  log.error("transform error:", (err as Error)?.message)
196
402
  }