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.
- package/ETHOS.md +6 -3
- package/LICENSE +21 -21
- package/README.md +109 -79
- package/bootstrap.ts +214 -8
- package/harness/agents/openhermes.md +45 -55
- package/harness/codex/AUTOPILOT.md +126 -0
- package/harness/codex/CONSTITUTION.md +14 -11
- package/harness/codex/ROUTING.md +35 -70
- package/harness/commands/oh-log.md +18 -0
- package/harness/instructions/RUNTIME.md +27 -52
- package/harness/skills/oh-builder/SKILL.md +13 -8
- package/harness/skills/oh-caveman/SKILL.md +9 -0
- package/harness/skills/oh-expert/SKILL.md +6 -0
- package/harness/skills/oh-facade/SKILL.md +298 -0
- package/harness/skills/oh-freeze/SKILL.md +9 -0
- package/harness/skills/oh-full-output/SKILL.md +81 -0
- package/harness/skills/oh-fusion/SKILL.md +314 -0
- package/harness/skills/oh-gauntlet/SKILL.md +9 -5
- package/harness/skills/oh-grill/SKILL.md +9 -5
- package/harness/skills/oh-guard/SKILL.md +9 -0
- package/harness/skills/oh-handoff/SKILL.md +9 -0
- package/harness/skills/oh-health/SKILL.md +8 -4
- package/harness/skills/oh-init/SKILL.md +28 -94
- package/harness/skills/oh-investigate/SKILL.md +10 -0
- package/harness/skills/oh-issue/SKILL.md +9 -0
- package/harness/skills/oh-learn/SKILL.md +13 -4
- package/harness/skills/oh-manifest/SKILL.md +15 -10
- package/harness/skills/oh-plan-review/SKILL.md +15 -8
- package/harness/skills/oh-planner/SKILL.md +18 -8
- package/harness/skills/oh-prd/SKILL.md +9 -0
- package/harness/skills/oh-refactor/SKILL.md +426 -0
- package/harness/skills/oh-retro/SKILL.md +9 -0
- package/harness/skills/oh-review/SKILL.md +11 -4
- package/harness/skills/oh-security/SKILL.md +4 -0
- package/harness/skills/oh-ship/SKILL.md +10 -0
- package/harness/skills/oh-skill-craft/SKILL.md +88 -0
- package/harness/skills/oh-skills-link/SKILL.md +9 -0
- package/harness/skills/oh-skills-list/SKILL.md +9 -0
- package/harness/skills/oh-triage/SKILL.md +11 -0
- package/lib/harness-resolver.ts +2 -2
- package/lib/logger.ts +7 -1
- package/package.json +6 -3
package/ETHOS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenHermes Ethos
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
15
|
-
|
|
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)
|
|
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"
|
|
3
|
-
<p align="center"><
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
34
|
+
## One sentence. Nine steps.
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
Add the plugin. Restart. Type:
|
|
27
37
|
|
|
28
|
-
|
|
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
|
-
|
|
40
|
+
You see output. Behind the scenes, this runs:
|
|
36
41
|
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
### Three safety layers
|
|
75
59
|
|
|
76
|
-
|
|
60
|
+
The loop runs unsupervised because these never turn off:
|
|
77
61
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
80
|
+
## 29 skills — three tiers
|
|
90
81
|
|
|
91
|
-
|
|
82
|
+
### Tier 4 — Pipeline orchestrators
|
|
83
|
+
Full multi-phase workflows:
|
|
92
84
|
|
|
93
|
-
|
|
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
|
-
|
|
93
|
+
### Tier 3 — Cross-cutting skills
|
|
94
|
+
Span multiple phases and coordinate other skills:
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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 #
|
|
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
|
|
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
|
|
119
|
-
│ ├── commands/ # Slash
|
|
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/ #
|
|
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
|
-
##
|
|
154
|
+
## Get started — 60 seconds
|
|
128
155
|
|
|
129
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|