openhermes 4.0.0 → 4.1.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/README.md +5 -5
- package/{bootstrap.mjs → bootstrap.ts} +53 -28
- package/harness/codex/ROUTING.md +3 -2
- package/harness/instructions/RUNTIME.md +2 -1
- package/harness/skills/oh-builder/SKILL.md +15 -9
- package/harness/skills/oh-gauntlet/SKILL.md +1 -1
- package/harness/skills/oh-init/SKILL.md +141 -8
- package/harness/skills/oh-investigate/SKILL.md +47 -8
- package/harness/skills/oh-learn/SKILL.md +72 -8
- package/harness/skills/oh-manifest/SKILL.md +40 -1
- package/harness/skills/oh-planner/SKILL.md +3 -1
- package/harness/skills/oh-review/SKILL.md +1 -1
- package/index.ts +3 -0
- package/lib/{harness-resolver.mjs → harness-resolver.ts} +15 -11
- package/lib/{logger.mjs → logger.ts} +21 -14
- package/package.json +11 -8
- package/tsconfig.json +16 -0
- package/harness/instructions/CONVENTIONS.md +0 -206
- package/index.mjs +0 -3
- package/test/plugins-behavioral.test.mjs +0 -64
- package/test/plugins.test.mjs +0 -62
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ These seven form a pipeline: **think → plan → build → test → ship → se
|
|
|
55
55
|
| oh-issue | Break plans into vertical-slice issues |
|
|
56
56
|
| oh-prd | Write structured PRDs |
|
|
57
57
|
| oh-caveman | Ultra-compressed response mode |
|
|
58
|
-
| oh-freeze |
|
|
58
|
+
| oh-freeze | Restrict file edits to a specific directory |
|
|
59
59
|
| oh-learn | Learn patterns from the codebase |
|
|
60
60
|
| oh-guard | Safety confirmations for destructive operations |
|
|
61
61
|
| oh-skills-link | Verify skills discovery |
|
|
@@ -111,14 +111,14 @@ openhermes-pkg/
|
|
|
111
111
|
├── AGENTS.md # Skill/command/agent inventory
|
|
112
112
|
├── CONTEXT.md # Shared language
|
|
113
113
|
├── ETHOS.md # Operating principles
|
|
114
|
-
├── bootstrap.
|
|
115
|
-
├── index.
|
|
114
|
+
├── bootstrap.ts # Plugin loader — registers everything
|
|
115
|
+
├── index.ts # Package entrypoint
|
|
116
116
|
├── harness/
|
|
117
117
|
│ ├── agents/ # Agent manifests (OpenHermes)
|
|
118
118
|
│ ├── codex/ # CONSTITUTION.md
|
|
119
119
|
│ ├── commands/ # Slash command manifests (/oh-doctor)
|
|
120
|
-
│ ├── instructions/ # RUNTIME.md
|
|
121
|
-
│
|
|
120
|
+
│ ├── instructions/ # RUNTIME.md
|
|
121
|
+
│ └── skills/ # 25 skill SKILL.md files
|
|
122
122
|
└── test/
|
|
123
123
|
```
|
|
124
124
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
2
|
import fs from "node:fs"
|
|
3
3
|
import { fileURLToPath } from "node:url"
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
5
|
+
import { createLogger } from "./lib/logger.ts"
|
|
6
|
+
import { getHarnessDir, setHarnessRootForTest, resolveHarnessRoot } from "./lib/harness-resolver.ts"
|
|
6
7
|
|
|
7
8
|
const log = createLogger("bootstrap")
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
@@ -11,8 +12,8 @@ const OPENHERMES_AGENT = "OpenHermes"
|
|
|
11
12
|
|
|
12
13
|
export { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir }
|
|
13
14
|
|
|
14
|
-
function parseFrontmatter(raw) {
|
|
15
|
-
const frontmatter = {}
|
|
15
|
+
function parseFrontmatter(raw: string | undefined): Record<string, string> {
|
|
16
|
+
const frontmatter: Record<string, string> = {}
|
|
16
17
|
if (!raw) return frontmatter
|
|
17
18
|
for (const line of raw.split(/\r?\n/)) {
|
|
18
19
|
const idx = line.indexOf(":")
|
|
@@ -24,16 +25,25 @@ function parseFrontmatter(raw) {
|
|
|
24
25
|
return frontmatter
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
interface MarkdownDocument {
|
|
29
|
+
frontmatter: Record<string, string>
|
|
30
|
+
body: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readMarkdownDocument(filePath: string): MarkdownDocument | null {
|
|
28
34
|
if (!fs.existsSync(filePath)) return null
|
|
29
35
|
const source = fs.readFileSync(filePath, "utf8")
|
|
30
|
-
const match = source.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
|
|
36
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/)
|
|
31
37
|
const frontmatter = parseFrontmatter(match?.[1] ?? "")
|
|
32
38
|
const body = (match ? match[2] : source).trim()
|
|
33
39
|
return { frontmatter, body }
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
interface DirEntry extends MarkdownDocument {
|
|
43
|
+
name: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readMarkdownDirectory(dir: string): DirEntry[] {
|
|
37
47
|
if (!fs.existsSync(dir)) return []
|
|
38
48
|
return fs.readdirSync(dir)
|
|
39
49
|
.filter(name => name.endsWith(".md") && name.toLowerCase() !== "readme.md")
|
|
@@ -43,13 +53,21 @@ function readMarkdownDirectory(dir) {
|
|
|
43
53
|
const document = readMarkdownDocument(filePath)
|
|
44
54
|
return document ? { name: path.basename(name, ".md"), ...document } : null
|
|
45
55
|
})
|
|
46
|
-
.filter(
|
|
56
|
+
.filter((e): e is DirEntry => e !== null)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface CommandDef {
|
|
60
|
+
description: string
|
|
61
|
+
template: string
|
|
62
|
+
agent?: string
|
|
63
|
+
model?: string
|
|
64
|
+
subtask?: boolean
|
|
47
65
|
}
|
|
48
66
|
|
|
49
|
-
function commandDefinitions(dir) {
|
|
50
|
-
const commands = {}
|
|
67
|
+
function commandDefinitions(dir: string): Record<string, CommandDef> {
|
|
68
|
+
const commands: Record<string, CommandDef> = {}
|
|
51
69
|
for (const doc of readMarkdownDirectory(dir)) {
|
|
52
|
-
const command = {
|
|
70
|
+
const command: CommandDef = {
|
|
53
71
|
description: doc.frontmatter.description || `OpenHermes command ${doc.name}`,
|
|
54
72
|
template: doc.body,
|
|
55
73
|
}
|
|
@@ -61,8 +79,14 @@ function commandDefinitions(dir) {
|
|
|
61
79
|
return commands
|
|
62
80
|
}
|
|
63
81
|
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
interface AgentDef {
|
|
83
|
+
description: string
|
|
84
|
+
mode: string
|
|
85
|
+
prompt: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function agentDefinitions(dir: string): Record<string, AgentDef> {
|
|
89
|
+
const agents: Record<string, AgentDef> = {}
|
|
66
90
|
for (const doc of readMarkdownDirectory(dir)) {
|
|
67
91
|
const name = doc.name === "openhermes" ? OPENHERMES_AGENT : doc.name
|
|
68
92
|
agents[name] = {
|
|
@@ -74,7 +98,7 @@ function agentDefinitions(dir) {
|
|
|
74
98
|
return agents
|
|
75
99
|
}
|
|
76
100
|
|
|
77
|
-
function uniqueStrings(existing = [], additions = []) {
|
|
101
|
+
function uniqueStrings(existing: string[] = [], additions: string[] = []): string[] {
|
|
78
102
|
const seen = new Set(existing.filter(Boolean))
|
|
79
103
|
const merged = [...existing]
|
|
80
104
|
for (const item of additions) {
|
|
@@ -85,11 +109,11 @@ function uniqueStrings(existing = [], additions = []) {
|
|
|
85
109
|
return merged
|
|
86
110
|
}
|
|
87
111
|
|
|
88
|
-
function readText(filePath) {
|
|
112
|
+
function readText(filePath: string): string {
|
|
89
113
|
return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : ""
|
|
90
114
|
}
|
|
91
115
|
|
|
92
|
-
function buildBootstrapContent(hDir) {
|
|
116
|
+
function buildBootstrapContent(hDir: string): string {
|
|
93
117
|
const parts = [
|
|
94
118
|
`<${BOOTSTRAP_MARKER}>`,
|
|
95
119
|
`You are OpenHermes.`,
|
|
@@ -111,7 +135,15 @@ function buildBootstrapContent(hDir) {
|
|
|
111
135
|
return parts.join("\n\n")
|
|
112
136
|
}
|
|
113
137
|
|
|
114
|
-
|
|
138
|
+
interface OpenHermesConfig {
|
|
139
|
+
skills?: { paths?: string[] }
|
|
140
|
+
command?: Record<string, unknown>
|
|
141
|
+
agent?: Record<string, unknown>
|
|
142
|
+
instructions?: string[]
|
|
143
|
+
default_agent?: string
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const BootstrapPlugin: Plugin = async () => {
|
|
115
147
|
const hDir = getHarnessDir()
|
|
116
148
|
const skillsDir = path.join(hDir, "skills")
|
|
117
149
|
const commandsDir = path.join(hDir, "commands")
|
|
@@ -119,7 +151,7 @@ export const BootstrapPlugin = async () => {
|
|
|
119
151
|
const bootstrapContent = buildBootstrapContent(hDir)
|
|
120
152
|
|
|
121
153
|
return {
|
|
122
|
-
config: async (config) => {
|
|
154
|
+
config: async (config: OpenHermesConfig) => {
|
|
123
155
|
config.skills = config.skills || {}
|
|
124
156
|
config.skills.paths = uniqueStrings(config.skills.paths || [], [skillsDir])
|
|
125
157
|
|
|
@@ -149,16 +181,9 @@ export const BootstrapPlugin = async () => {
|
|
|
149
181
|
}
|
|
150
182
|
|
|
151
183
|
config.default_agent = OPENHERMES_AGENT
|
|
152
|
-
|
|
153
|
-
config.instructions = uniqueStrings(config.instructions || [], [
|
|
154
|
-
path.join(hDir, "codex", "CONSTITUTION.md"),
|
|
155
|
-
path.join(hDir, "instructions", "RUNTIME.md"),
|
|
156
|
-
path.join(__dirname, "CONTEXT.md"),
|
|
157
|
-
path.join(__dirname, "ETHOS.md"),
|
|
158
|
-
])
|
|
159
184
|
},
|
|
160
185
|
|
|
161
|
-
"experimental.chat.messages.transform": async (_input, output) => {
|
|
186
|
+
"experimental.chat.messages.transform": async (_input: unknown, output: { messages?: Array<{ info?: { role?: string }; parts?: Array<{ text?: string }> }> }) => {
|
|
162
187
|
try {
|
|
163
188
|
if (!output.messages?.length) return
|
|
164
189
|
const firstUser = output.messages.find(m => m?.info?.role === "user")
|
|
@@ -166,8 +191,8 @@ export const BootstrapPlugin = async () => {
|
|
|
166
191
|
if (firstUser.parts.some(p => p.text?.includes(BOOTSTRAP_MARKER))) return
|
|
167
192
|
const ref = firstUser.parts[0]
|
|
168
193
|
firstUser.parts.unshift({ ...ref, type: "text", text: bootstrapContent })
|
|
169
|
-
} catch (err) {
|
|
170
|
-
log.error("transform error:", err?.message)
|
|
194
|
+
} catch (err: unknown) {
|
|
195
|
+
log.error("transform error:", (err as Error)?.message)
|
|
171
196
|
}
|
|
172
197
|
},
|
|
173
198
|
}
|
package/harness/codex/ROUTING.md
CHANGED
|
@@ -17,6 +17,7 @@ If a skill has no explicit route for an outcome, the fallback is always **surfac
|
|
|
17
17
|
## Canonical routing table
|
|
18
18
|
|
|
19
19
|
### Workflow skills
|
|
20
|
+
*Includes oh-doctor (command, not skill) for diagnostic routing.*
|
|
20
21
|
|
|
21
22
|
| Skill | pass | fail | blocker |
|
|
22
23
|
|-------|------|------|---------|
|
|
@@ -49,8 +50,8 @@ If a skill has no explicit route for an outcome, the fallback is always **surfac
|
|
|
49
50
|
| **oh-triage** | → oh-issue or oh-handoff | → oh-expert (clarify) | surface |
|
|
50
51
|
| **oh-retro** | → oh-planner (next cycle) | → oh-handoff (if blocked) | surface |
|
|
51
52
|
| **oh-handoff** | → [end of session — intended terminal] | → [surface blocker] | surface |
|
|
52
|
-
| **oh-
|
|
53
|
-
| **oh-skills-link** | → [report link status] | → oh-
|
|
53
|
+
| **oh-skill-craft** | → oh-skills-link (verify discovery) | → oh-expert (diagnose) | surface |
|
|
54
|
+
| **oh-skills-link** | → [report link status] | → oh-skill-craft (fix skill) | surface |
|
|
54
55
|
| **oh-skills-list** | → [done — read-only] | → [surface issue] | surface |
|
|
55
56
|
|
|
56
57
|
### Mode skills (no routing — mode switches)
|
|
@@ -42,6 +42,7 @@ Key skills:
|
|
|
42
42
|
- `.opencode/plan.md` — produced by oh-planner, consumed by oh-builder and oh-manifest
|
|
43
43
|
- `.opencode/work-log.md` — progress tracking across subagent delegations
|
|
44
44
|
- `.opencode/todo.md` — task tracking for multi-step work
|
|
45
|
+
- `.opencode/instincts.jsonl` — behavioral patterns (trigger-action-confidence) extracted by oh-learn. On session start, read the highest-confidence entries (≥0.7) into context so past patterns inform current work. This is not durable state — it is an opt-in config that grows organically.
|
|
45
46
|
|
|
46
47
|
**Bootstrap**: `harness/codex/CONSTITUTION.md`, this file, `CONTEXT.md`, and `ETHOS.md` are injected into the first user message so the agent starts with the same operating model every session.
|
|
47
48
|
|
|
@@ -50,5 +51,5 @@ Key skills:
|
|
|
50
51
|
## Conventions
|
|
51
52
|
|
|
52
53
|
Security, coding style, testing, and orchestration standards:
|
|
53
|
-
-
|
|
54
|
+
- For coding conventions, see the Constitution.
|
|
54
55
|
- Skills provide the detailed walkthroughs for specialized workflows.
|
|
@@ -23,15 +23,21 @@ The ALL-arounder builder. Merges prototyping, TDD, implementation from plan, and
|
|
|
23
23
|
### Mode A: Prototype (exploratory)
|
|
24
24
|
When you need to answer a question before committing.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
**Pick a branch based on the question being asked:**
|
|
27
|
+
|
|
28
|
+
- **"Does this logic / state model feel right?"** → **Terminal branch.** Build a tiny interactive terminal app that pushes the state machine through cases that are hard to reason about on paper.
|
|
29
|
+
- **"What should this look like?"** → **UI branch.** Generate several radically different visual variations, switchable via a URL param or floating control bar.
|
|
30
|
+
|
|
31
|
+
If the question is genuinely ambiguous, default to whichever branch better matches the surrounding code (backend module → terminal, page/component → UI) and state the assumption.
|
|
32
|
+
|
|
33
|
+
**Rules that apply to both branches:**
|
|
34
|
+
|
|
35
|
+
1. **Throwaway from day one, clearly marked.** Name it so a casual reader sees it's a prototype.
|
|
36
|
+
2. **One command to run.** Whatever the project's task runner supports — `pnpm <name>`, `bun <path>`, etc.
|
|
37
|
+
3. **No persistence by default.** State lives in memory. If the question involves a database, hit a scratch DB with a clear "PROTOTYPE — wipe me" name.
|
|
38
|
+
4. **Skip the polish.** No tests, no error handling beyond what makes it runnable. The point is to learn and then delete.
|
|
39
|
+
5. **Surface the state.** After every action (terminal) or on every variant switch (UI), show the full relevant state so the user sees what changed.
|
|
40
|
+
6. **Delete or absorb when done.** The answer is the only thing worth keeping. Capture it in a commit, ADR, or note — then delete the prototype code.
|
|
35
41
|
|
|
36
42
|
### Mode B: TDD (test-first implementation)
|
|
37
43
|
When building production code from a plan or spec. Red-green-refactor with vertical tracer bullets.
|
|
@@ -34,7 +34,7 @@ If tests are missing or weak, flag what should be added. Do not add them here
|
|
|
34
34
|
|
|
35
35
|
Spawn two sub-agents simultaneously:
|
|
36
36
|
|
|
37
|
-
**Standards sub-agent:** Read the repo's documented standards (CONTEXT.md, AGENTS.md, eslint config, ADRs
|
|
37
|
+
**Standards sub-agent:** Read the repo's documented standards (CONTEXT.md, AGENTS.md, eslint config, ADRs). Then read the diff. Report every place the diff violates a documented standard. Cite the standard source. Distinguish hard violations from judgement calls.
|
|
38
38
|
|
|
39
39
|
**Spec sub-agent:** Read the spec source (plan.md, issue, PRD, or user's description). Then read the diff. Report: (a) requirements that are missing or partial, (b) scope creep (behavior not asked for), (c) requirements that look implemented but wrong. Quote the spec.
|
|
40
40
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oh-init
|
|
3
|
-
description: "Initialize project for
|
|
3
|
+
description: "Initialize project for OpenHermes takeover: scaffold .opencode/ runtime skeleton, wire AGENTS.md, configure domain docs, issue tracker, and triage labels."
|
|
4
4
|
tier: 2
|
|
5
5
|
triggers:
|
|
6
6
|
- "init project"
|
|
@@ -8,15 +8,144 @@ triggers:
|
|
|
8
8
|
- "initialize"
|
|
9
9
|
- "onboard"
|
|
10
10
|
- "scaffold"
|
|
11
|
+
- "takeover"
|
|
11
12
|
---
|
|
12
13
|
|
|
13
14
|
# oh-init
|
|
14
15
|
|
|
15
|
-
Per-repo setup for
|
|
16
|
+
Per-repo setup for OpenHermes-assisted development. Run once per repo. Wires the `.opencode/` runtime skeleton, connects `AGENTS.md` to the orchestrator, then walks through domain/issue configuration decisions one at a time.
|
|
17
|
+
|
|
18
|
+
Complements OpenCode's built-in `/init` command (which creates `AGENTS.md` with project build/test/architecture notes). Run oh-init after or instead — they serve different layers.
|
|
16
19
|
|
|
17
20
|
## Process
|
|
18
21
|
|
|
19
|
-
###
|
|
22
|
+
### Phase 0: Check Existing State
|
|
23
|
+
Before writing anything, detect what already exists:
|
|
24
|
+
|
|
25
|
+
- ☐ `.opencode/` directory present?
|
|
26
|
+
- ☐ `.opencode/plan.md` exists?
|
|
27
|
+
- ☐ `.opencode/todo.md` exists?
|
|
28
|
+
- ☐ `.opencode/work-log.md` exists?
|
|
29
|
+
- ☐ `.opencode/instincts.jsonl` exists?
|
|
30
|
+
- ☐ `AGENTS.md` exists? (If yes, was it created by OpenCode `/init` or manually?)
|
|
31
|
+
- ☐ `opencode.json` / `opencode.jsonc` present?
|
|
32
|
+
|
|
33
|
+
Report findings. If everything exists, offer to skip or verify and exit.
|
|
34
|
+
|
|
35
|
+
### Phase 1: .opencode/ Runtime Skeleton
|
|
36
|
+
Create `.opencode/` directory if missing. Scaffold shared state files:
|
|
37
|
+
|
|
38
|
+
**`.opencode/plan.md`** — working plan for the current session. Uses the same format as the global permanent plan directory (`%USERPROFILE%/.config/opencode/task/<project>-plan-<nnn>.md`). When a plan is completed, copy to the global directory with sequenced naming for permanent archive.
|
|
39
|
+
|
|
40
|
+
```markdown
|
|
41
|
+
# PLAN: <project-name>
|
|
42
|
+
|
|
43
|
+
Plan ID: <project-name>-plan-<nnn>
|
|
44
|
+
Project: <project-name>
|
|
45
|
+
Status: active
|
|
46
|
+
Created: <local-date-time>
|
|
47
|
+
Updated: <local-date-time>
|
|
48
|
+
Project Path: <absolute-project-path>
|
|
49
|
+
Plan Path: .opencode/plan.md
|
|
50
|
+
Objective: <short objective>
|
|
51
|
+
|
|
52
|
+
## Current State
|
|
53
|
+
|
|
54
|
+
## Assumptions
|
|
55
|
+
|
|
56
|
+
## Tasks
|
|
57
|
+
|
|
58
|
+
- [ ] Task 1
|
|
59
|
+
- [ ] Subtask 1.1
|
|
60
|
+
|
|
61
|
+
## Active Task
|
|
62
|
+
|
|
63
|
+
## Subagents
|
|
64
|
+
|
|
65
|
+
| Agent | Purpose | Status | Findings |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
|
|
68
|
+
## Completed
|
|
69
|
+
|
|
70
|
+
## Blockers
|
|
71
|
+
|
|
72
|
+
- None
|
|
73
|
+
|
|
74
|
+
## Validation
|
|
75
|
+
|
|
76
|
+
- [ ] Static checks
|
|
77
|
+
- [ ] Formatting checks
|
|
78
|
+
- [ ] Type checks
|
|
79
|
+
- [ ] Unit tests
|
|
80
|
+
- [ ] Integration checks
|
|
81
|
+
- [ ] Manual verification
|
|
82
|
+
|
|
83
|
+
## Decisions
|
|
84
|
+
|
|
85
|
+
## Notes
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**`.opencode/todo.md`** — task tracking for multi-step work (start empty).
|
|
89
|
+
|
|
90
|
+
**`.opencode/work-log.md`** — progress tracking across subagent delegations:
|
|
91
|
+
```markdown
|
|
92
|
+
# Work Log
|
|
93
|
+
|
|
94
|
+
## <date> — <description>
|
|
95
|
+
- Started: <time>
|
|
96
|
+
- Completed: <task>
|
|
97
|
+
- Next: <next task>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**`.opencode/instincts.jsonl`** — behavioral pattern store for oh-learn (start as empty file). Will grow organically as the agent extracts patterns from sessions.
|
|
101
|
+
|
|
102
|
+
### Phase 2: AGENTS.md Wiring
|
|
103
|
+
|
|
104
|
+
Check if AGENTS.md exists:
|
|
105
|
+
|
|
106
|
+
**If AGENTS.md does not exist:**
|
|
107
|
+
Create it with OpenHermes orchestrator header + prompts for project info:
|
|
108
|
+
|
|
109
|
+
```markdown
|
|
110
|
+
# <project-name>
|
|
111
|
+
|
|
112
|
+
OpenHermes is the primary orchestrator. All routing, planning, and delegation flows through oh-* skills.
|
|
113
|
+
|
|
114
|
+
## Project Context
|
|
115
|
+
|
|
116
|
+
- **Language**: <fill in>
|
|
117
|
+
- **Package manager**: <fill in>
|
|
118
|
+
- **Build command**: <fill in>
|
|
119
|
+
- **Test command**: <fill in>
|
|
120
|
+
- **Lint/type check**: <fill in>
|
|
121
|
+
|
|
122
|
+
## Key Directives
|
|
123
|
+
|
|
124
|
+
- Plan first. Write to `.opencode/plan.md` before multi-file changes.
|
|
125
|
+
- Verify before claiming success. Read files, run commands, confirm output.
|
|
126
|
+
- Delegate substantive work to subagents — main context orchestrates.
|
|
127
|
+
- Use oh-* skills on demand. Load via OpenCode's skill tool when relevant.
|
|
128
|
+
- Shared state lives in `.opencode/` (plan.md, todo.md, work-log.md, instincts.jsonl).
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Then ask the user to fill in the Project Context fields. Offer to auto-detect from package manifests.
|
|
132
|
+
|
|
133
|
+
**If AGENTS.md exists** (e.g., created by OpenCode `/init`):
|
|
134
|
+
Append an `## OpenHermes Orchestrator` section to the end:
|
|
135
|
+
|
|
136
|
+
```markdown
|
|
137
|
+
## OpenHermes Orchestrator
|
|
138
|
+
|
|
139
|
+
OpenHermes is the primary orchestrator for this session.
|
|
140
|
+
|
|
141
|
+
- **Orchestrator**: OpenHermes — hub-and-spoke routing through oh-* skills
|
|
142
|
+
- **Plan**: `.opencode/plan.md` — always check before starting work
|
|
143
|
+
- **Shared state**: `.opencode/todo.md`, `.opencode/work-log.md`, `.opencode/instincts.jsonl`
|
|
144
|
+
- **Verify before claim**: read files, run commands, confirm output
|
|
145
|
+
- **Delegate**: subagents for implementation, main context orchestrates
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Phase 3: Issue Tracker
|
|
20
149
|
Detect the git hosting platform:
|
|
21
150
|
- **GitHub** — `gh` CLI
|
|
22
151
|
- **GitLab** — `glab` CLI
|
|
@@ -25,7 +154,7 @@ Detect the git hosting platform:
|
|
|
25
154
|
|
|
26
155
|
Confirm with the user. Write the result to `docs/agents/issue-tracker.md`.
|
|
27
156
|
|
|
28
|
-
###
|
|
157
|
+
### Phase 4: Triage Labels
|
|
29
158
|
The `triage` skill uses these label strings to move issues through a state machine:
|
|
30
159
|
- `needs-triage` — maintainer needs to evaluate
|
|
31
160
|
- `needs-info` — waiting on reporter
|
|
@@ -35,7 +164,7 @@ The `triage` skill uses these label strings to move issues through a state machi
|
|
|
35
164
|
|
|
36
165
|
If the repo already has different label names, map them. Write to `docs/agents/triage-labels.md`.
|
|
37
166
|
|
|
38
|
-
###
|
|
167
|
+
### Phase 5: Domain Docs
|
|
39
168
|
Configure how the project organizes domain language:
|
|
40
169
|
- **Single-context** — one `CONTEXT.md` + `docs/adr/` at repo root
|
|
41
170
|
- **Multi-context** — `CONTEXT-MAP.md` pointing to per-context files
|
|
@@ -44,7 +173,7 @@ Scaffold `CONTEXT.md` with project name, domain description, and placeholder glo
|
|
|
44
173
|
|
|
45
174
|
Write to `docs/agents/domain.md`.
|
|
46
175
|
|
|
47
|
-
###
|
|
176
|
+
### Phase 6: Agent Skills Block
|
|
48
177
|
Add a `## Agent skills` section to `AGENTS.md` (or `CLAUDE.md` if it exists):
|
|
49
178
|
|
|
50
179
|
```markdown
|
|
@@ -60,14 +189,18 @@ Add a `## Agent skills` section to `AGENTS.md` (or `CLAUDE.md` if it exists):
|
|
|
60
189
|
<summary>. See docs/agents/domain.md.
|
|
61
190
|
```
|
|
62
191
|
|
|
63
|
-
###
|
|
64
|
-
Record: "oh-init completed for project
|
|
192
|
+
### Phase 7: Decision Record
|
|
193
|
+
Record: "oh-init completed for project <name> on <date>."
|
|
65
194
|
|
|
66
195
|
## Anti-patterns
|
|
67
196
|
- Running init without understanding the project domain
|
|
68
197
|
- Scaffolding CONTEXT.md without populating any terms
|
|
69
198
|
- Creating ADR directory but never writing ADRs
|
|
70
199
|
- Creating both AGENTS.md and CLAUDE.md — edit the one that exists
|
|
200
|
+
- Overwriting an existing AGENTS.md created by OpenCode `/init` (append instead)
|
|
201
|
+
- Scaffolding `.opencode/` files that already exist (check first, skip duplicates)
|
|
202
|
+
- Empty instinct file never getting populated (run oh-learn extract periodically)
|
|
203
|
+
- Never archiving completed plans to the global task directory (completed plans rot in `.opencode/` instead of becoming permanent records)
|
|
71
204
|
|
|
72
205
|
## Routing
|
|
73
206
|
|
|
@@ -8,14 +8,52 @@ description: "Systematic bug diagnosis with root cause investigation"
|
|
|
8
8
|
## When to Use
|
|
9
9
|
When a bug is reported, a test fails, or unexpected behavior occurs. Use this before attempting any fix.
|
|
10
10
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
## Phase 0 — Build a feedback loop
|
|
12
|
+
|
|
13
|
+
**This is the actual skill. Everything else is mechanical.**
|
|
14
|
+
|
|
15
|
+
If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation are just consuming that signal. If you don't have one, no amount of staring at code will save you.
|
|
16
|
+
|
|
17
|
+
Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**
|
|
18
|
+
|
|
19
|
+
### Ways to construct a feedback loop (try in this order)
|
|
20
|
+
|
|
21
|
+
1. **Failing test** at whatever seam reaches the bug.
|
|
22
|
+
2. **Curl / HTTP script** against a running dev server.
|
|
23
|
+
3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot.
|
|
24
|
+
4. **Headless browser script** — drive the UI, assert on DOM/console/network.
|
|
25
|
+
5. **Replay a captured trace** — save a real payload/event log, replay it in isolation.
|
|
26
|
+
6. **Throwaway harness** — minimal subset of the system exercising the bug code path with a single call.
|
|
27
|
+
7. **Property / fuzz loop** — run 1000 random inputs, look for the failure mode.
|
|
28
|
+
8. **Bisection harness** — automate "boot at state X, check, repeat" so you can `git bisect run` it.
|
|
29
|
+
9. **Differential loop** — run same input through old-version vs new-version, diff outputs.
|
|
30
|
+
10. **HITL script** — last resort. Drive a human with a structured loop.
|
|
31
|
+
|
|
32
|
+
### Iterate on the loop itself
|
|
33
|
+
|
|
34
|
+
- Can I make it faster? (Cache setup, skip unrelated init, narrow the scope.)
|
|
35
|
+
- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".)
|
|
36
|
+
- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem.)
|
|
37
|
+
|
|
38
|
+
A 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower.
|
|
39
|
+
|
|
40
|
+
### Non-deterministic bugs
|
|
41
|
+
|
|
42
|
+
The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows. A 50%-flake bug is debuggable; 1% is not.
|
|
43
|
+
|
|
44
|
+
### When you genuinely cannot build a loop
|
|
45
|
+
|
|
46
|
+
Stop and say so explicitly. List what you tried. Do **not** proceed to hypothesise without a loop.
|
|
47
|
+
|
|
48
|
+
## Workflow (consumes the loop)
|
|
49
|
+
|
|
50
|
+
1. **Reproduce** — run the loop, confirm the bug appears. The loop must match the user's described failure, not a different nearby failure.
|
|
51
|
+
2. **Minimise** — strip away unrelated code until the minimal reproduction remains.
|
|
52
|
+
3. **Hypothesise** — generate 3–5 ranked falsifiable hypotheses before testing any. Each must state a prediction: "If X is the cause, then changing Y will make the bug disappear".
|
|
53
|
+
4. **Instrument** — one probe per hypothesis. Change one variable at a time. Tag every debug log with a unique prefix (e.g. `[DEBUG-a4f2]`) for easy cleanup.
|
|
54
|
+
5. **Fix** — write the regression test at a correct seam first. Watch it fail. Apply the smallest correct change. Watch it pass. Re-run the Phase 0 loop against the original scenario.
|
|
55
|
+
6. **Regression test** — verify fix doesn't break existing behavior. If no correct seam exists for a regression test, that itself is a finding — flag the architecture gap.
|
|
56
|
+
7. **Document** — log the root cause and fix in the handoff, issue, or relevant docs. State which hypothesis was correct so the next debugger learns.
|
|
19
57
|
|
|
20
58
|
## Iron Law
|
|
21
59
|
No fixes without root cause. Surface-level fixes compound into technical debt.
|
|
@@ -25,6 +63,7 @@ No fixes without root cause. Surface-level fixes compound into technical debt.
|
|
|
25
63
|
- Changing code without reproducing the bug first
|
|
26
64
|
- "Shotgun" debugging — changing multiple things hoping one sticks
|
|
27
65
|
- Not documenting root cause for future reference
|
|
66
|
+
- Proceeding to hypothesise without a feedback loop
|
|
28
67
|
|
|
29
68
|
## Routing
|
|
30
69
|
|
|
@@ -1,28 +1,92 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oh-learn
|
|
3
|
-
description: "Review, search, prune,
|
|
3
|
+
description: "Extract, evolve, and promote session learnings as instincts. Review, search, prune, export."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# oh-learn
|
|
7
7
|
|
|
8
|
+
Learning engine for the harness. Distills patterns from sessions into **instincts** (trigger-action pairs with confidence), clusters them into skill candidates, and graduates high-signal patterns from project to global scope.
|
|
9
|
+
|
|
10
|
+
## Instinct Data Model
|
|
11
|
+
|
|
12
|
+
Every learning stored as one JSONL line in `.opencode/instincts.jsonl`:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{ "trigger": "situation pattern", "action": "recommended response", "confidence": 0.5, "applications": 1, "successes": 1, "category": "coding", "source": "oh-learn:extract", "ts": "2026-05-15T12:00:00Z" }
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Rules:**
|
|
19
|
+
- **Trigger** — specific, matchable situation. *Not* general advice.
|
|
20
|
+
- **Action** — executable response. *Not* a belief.
|
|
21
|
+
- **Confidence** — starts at 0.5, increments +0.05 per successful application, decays -0.02 per day without use.
|
|
22
|
+
- **Category** — one of: `coding`, `testing`, `security`, `git`, `planning`, `orchestration`, `debugging`, `ux`.
|
|
23
|
+
|
|
8
24
|
## When to Use
|
|
9
|
-
To review what the agent has learned across sessions, search for specific patterns, prune stale knowledge, or export learnings for documentation.
|
|
10
25
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
26
|
+
After completing a significant piece of work, at session handoff, or when you notice the same pattern repeat 2+ times in one session. Also on explicit user request.
|
|
27
|
+
|
|
28
|
+
## Workflows
|
|
29
|
+
|
|
30
|
+
### Extract
|
|
31
|
+
Mine the current session for reusable patterns.
|
|
32
|
+
|
|
33
|
+
1. Scan recent conversation + code changes for repeated decision patterns
|
|
34
|
+
2. For each distinct pattern write an instinct: trigger, action, confidence=0.5, category
|
|
35
|
+
3. Read existing `.opencode/instincts.jsonl`, check for near-duplicate triggers
|
|
36
|
+
4. If duplicate found: merge — `confidence = max(existing, 0.8 × new)`, increment applications
|
|
37
|
+
5. If new: append line to file
|
|
38
|
+
|
|
39
|
+
**Good instinct:** trigger=`"tsc --noEmit shows 10+ errors after batch edit"`, action=`"Fix errors one at a time, re-running tsc after each, rather than batch-fixing"`, category=`"debugging"`
|
|
40
|
+
|
|
41
|
+
**Bad instinct:** `"Write clean code"` — too vague to trigger on.
|
|
42
|
+
|
|
43
|
+
### Evolve
|
|
44
|
+
Cluster related instincts into skill/command/agent candidates.
|
|
45
|
+
|
|
46
|
+
1. Read all instincts from `.opencode/instincts.jsonl`
|
|
47
|
+
2. Group by `category`, then by trigger topic similarity
|
|
48
|
+
3. **If cluster ≥ 5 instincts AND avg confidence ≥ 0.7** → generate `oh-skill-craft` spec for a new skill
|
|
49
|
+
4. **If cluster 3-4 instincts with confidence ≥ 0.8** → suggest update to existing skill
|
|
50
|
+
5. Output candidate summary with trigger list and extracted core pattern
|
|
51
|
+
|
|
52
|
+
### Promote
|
|
53
|
+
Graduate high-confidence instincts from project to global scope.
|
|
54
|
+
|
|
55
|
+
1. Scan `.opencode/instincts.jsonl` for instincts with `confidence >= 0.85 AND applications >= 10`
|
|
56
|
+
2. Filter out project-specific patterns (reference paths, local APIs, domain terms)
|
|
57
|
+
3. Append filtered candidates to `%USERPROFILE%\.config\opencode\instincts.jsonl` (global)
|
|
58
|
+
4. Tag promoted instincts with `"promoted": true` in project file
|
|
59
|
+
5. Report: "Promoted N instincts to global scope"
|
|
60
|
+
|
|
61
|
+
### Review
|
|
62
|
+
Show instinct summary: total count, confidence distribution, category breakdown, recently promoted.
|
|
63
|
+
|
|
64
|
+
### Search
|
|
65
|
+
Find instincts by topic, trigger fragment, category, or confidence range.
|
|
66
|
+
|
|
67
|
+
### Prune
|
|
68
|
+
Remove instincts stale for 30+ days with confidence < 0.3, or superseded by a higher-confidence instinct covering the same trigger.
|
|
69
|
+
|
|
70
|
+
### Export
|
|
71
|
+
Serialize instincts to portable JSON for sharing across projects or teams:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{ "version": 1, "exported": "2026-05-15T12:00:00Z", "instincts": [...] }
|
|
75
|
+
```
|
|
16
76
|
|
|
17
77
|
## Anti-patterns
|
|
78
|
+
|
|
18
79
|
- Hoarding every observation (most things aren't learnings)
|
|
19
80
|
- Never pruning (stale knowledge is worse than no knowledge)
|
|
20
81
|
- Storing what, not why (context-less facts are forgettable)
|
|
82
|
+
- Over-promoting: not every pattern is globally useful
|
|
83
|
+
- Extracting without applying: instincts that never trigger are noise
|
|
84
|
+
- Ignoring confidence: treating all instincts as equally reliable
|
|
21
85
|
|
|
22
86
|
## Routing
|
|
23
87
|
|
|
24
88
|
| Outcome | Route |
|
|
25
89
|
|---------|-------|
|
|
26
|
-
| pass | → [done —
|
|
90
|
+
| pass | → [done — report summary] |
|
|
27
91
|
| fail | → [surface gaps to user] |
|
|
28
92
|
| blocker | → surface to user |
|
|
@@ -15,10 +15,21 @@ triggers:
|
|
|
15
15
|
|
|
16
16
|
# oh-manifest
|
|
17
17
|
|
|
18
|
-
Full build orchestration loop. Runs planner → builder → verify → repeat until done or a blocker is surfaced. Uses
|
|
18
|
+
Full build orchestration loop. Runs pre-flight checks → planner → builder → verify → repeat until done or a blocker is surfaced. Uses decision principles to auto-resolve intermediate questions. Only interrupts the user for genuine blockers.
|
|
19
19
|
|
|
20
20
|
## Pipeline
|
|
21
21
|
|
|
22
|
+
### Phase 0: Pre-Flight
|
|
23
|
+
|
|
24
|
+
Before any work begins, ALL of these MUST pass:
|
|
25
|
+
|
|
26
|
+
- ☐ **Quality baseline** — existing tests pass (if any). Capture output for before/after comparison.
|
|
27
|
+
- ☐ **Rollback path** — clean `git stash` or a committed state you can return to.
|
|
28
|
+
- ☐ **Branch isolation** — confirm you are on a working branch, not main/master.
|
|
29
|
+
- ☐ **Scope documented** — plan or task description exists and is unambiguous.
|
|
30
|
+
|
|
31
|
+
If any check fails → **STOP**. Report which check failed and why. Do not proceed to Phase 1 until the blocker is resolved.
|
|
32
|
+
|
|
22
33
|
### Step 1: Plan
|
|
23
34
|
- If `.opencode/plan.md` exists, load and verify it is current
|
|
24
35
|
- If not, run `oh-planner` (Mode A, B, or C depending on context)
|
|
@@ -43,6 +54,32 @@ Full build orchestration loop. Runs planner → builder → verify → repeat un
|
|
|
43
54
|
- Phase failed and cannot be fixed → BLOCKER (surface to user with context)
|
|
44
55
|
- Phase passed but new work discovered → add to plan, continue loop
|
|
45
56
|
|
|
57
|
+
## Loop Patterns
|
|
58
|
+
|
|
59
|
+
Select a pattern based on the nature of the work:
|
|
60
|
+
|
|
61
|
+
| Pattern | Use When | Behavior |
|
|
62
|
+
|---------|----------|----------|
|
|
63
|
+
| **sequential** | Normal feature work | One phase at a time, verify each before next |
|
|
64
|
+
| **continuous-pr** | Multi-step refactors | Each phase is its own PR — commit, push, PR per phase |
|
|
65
|
+
| **infinite** | Watch mode, CI repair | Continue until external stop signal or budget exhausted |
|
|
66
|
+
| **rfc-dag** | Complex dependency chains | Resolve phase ordering by DAG; parallelize independent branches |
|
|
67
|
+
|
|
68
|
+
Default is **sequential**. Switch patterns only when the work structure demands it.
|
|
69
|
+
|
|
70
|
+
## Escalation Triggers
|
|
71
|
+
|
|
72
|
+
These conditions cause the loop to **pause** and surface to the user:
|
|
73
|
+
|
|
74
|
+
| Trigger | Condition | Action |
|
|
75
|
+
|---------|-----------|--------|
|
|
76
|
+
| **Stall** | 2 consecutive checkpoints with zero measurable progress | Pause. Report what was attempted, what blocked. |
|
|
77
|
+
| **Retry storm** | Same error message 3+ times in the loop | Stop retrying. Surface error with attempted fixes. |
|
|
78
|
+
| **Cost drift** | Cumulative changes exceed scope documented in pre-flight | Pause. Show diff between planned and actual scope. |
|
|
79
|
+
| **Quality regression** | Verify phase scores lower than pre-flight baseline | Pause. Report degraded metrics. Do not push through. |
|
|
80
|
+
|
|
81
|
+
These are not optional suggestions. When a trigger fires, the loop **must** pause and report.
|
|
82
|
+
|
|
46
83
|
## Decision Principles
|
|
47
84
|
|
|
48
85
|
Auto-resolve these without asking the user:
|
|
@@ -69,11 +106,13 @@ When a blocker is encountered:
|
|
|
69
106
|
4. **Wait for user decision** before continuing
|
|
70
107
|
|
|
71
108
|
## Anti-patterns
|
|
109
|
+
- Skipping pre-flight (every loop needs a baseline and a rollback plan)
|
|
72
110
|
- Auto-deciding premises (fundamental assumptions need user input)
|
|
73
111
|
- Pushing through blockers (surface immediately, don't try 5 workarounds silently)
|
|
74
112
|
- Skipping verification (verify every phase, not just the final result)
|
|
75
113
|
- Parallelizing dependent phases (respect the dependency order in plan.md)
|
|
76
114
|
- Forgetting to update plan.md with completion status
|
|
115
|
+
- Ignoring escalation triggers (stall means pause, not try harder)
|
|
77
116
|
|
|
78
117
|
## Routing
|
|
79
118
|
|
|
@@ -80,7 +80,9 @@ Never auto-decide: premises (need human judgment) or cases where both the plan a
|
|
|
80
80
|
|
|
81
81
|
## Plan Artifact
|
|
82
82
|
|
|
83
|
-
Output goes in `.opencode/plan.md` with this structure (matching the global AGENTS.md schema)
|
|
83
|
+
Output goes in `.opencode/plan.md` (per-project, overwritten each session) with this structure (matching the global AGENTS.md schema).
|
|
84
|
+
|
|
85
|
+
**Then save a copy** to `%USERPROFILE%/.config/opencode/task/<project-name>-plan-<nnn>.md` (global, incrementing, persistent) per AGENTS.md persistent plan rules.
|
|
84
86
|
|
|
85
87
|
```markdown
|
|
86
88
|
# PLAN: <project-name>
|
|
@@ -45,7 +45,7 @@ Collect all files documenting how code should be written:
|
|
|
45
45
|
- AGENTS.md, CLAUDE.md, CONTRIBUTING.md
|
|
46
46
|
- CONTEXT.md, ADRs
|
|
47
47
|
- eslint/biome/prettier config (note tool-enforced ones — don't re-check)
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
|
|
50
50
|
### 4. Spawn Both Sub-Agents (parallel)
|
|
51
51
|
|
package/index.ts
ADDED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Shared harness directory resolver — canonical implementation.
|
|
2
|
-
// Extracted from bootstrap.
|
|
3
|
-
// Both consumers import from here.
|
|
2
|
+
// Extracted from bootstrap.ts. Both bootstrap.ts and tests import from here.
|
|
4
3
|
|
|
5
4
|
import path from "node:path"
|
|
6
5
|
import fs from "node:fs"
|
|
@@ -9,14 +8,14 @@ import { fileURLToPath } from "node:url"
|
|
|
9
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
9
|
const PKG_DIR = path.resolve(__dirname, "..")
|
|
11
10
|
|
|
12
|
-
const REQUIRED_HARNESS_FILES = [
|
|
11
|
+
const REQUIRED_HARNESS_FILES: ReadonlyArray<[string, string, string]> = [
|
|
13
12
|
["codex", "CONSTITUTION.md"],
|
|
14
13
|
["instructions", "RUNTIME.md"],
|
|
15
14
|
["skills", "oh-plan", "SKILL.md"],
|
|
16
15
|
]
|
|
17
16
|
|
|
18
|
-
function ancestorDirs(start, limit = 6) {
|
|
19
|
-
const dirs = []
|
|
17
|
+
function ancestorDirs(start: string, limit = 6): string[] {
|
|
18
|
+
const dirs: string[] = []
|
|
20
19
|
let current = path.resolve(start)
|
|
21
20
|
for (let i = 0; i < limit; i++) {
|
|
22
21
|
dirs.push(current)
|
|
@@ -27,7 +26,7 @@ function ancestorDirs(start, limit = 6) {
|
|
|
27
26
|
return dirs
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
function buildHarnessCandidates(currentDir, execPath, cwd) {
|
|
29
|
+
function buildHarnessCandidates(currentDir: string, execPath: string, cwd: string): string[] {
|
|
31
30
|
const roots = [path.resolve(currentDir, "harness")]
|
|
32
31
|
const seen = new Set(roots)
|
|
33
32
|
|
|
@@ -50,7 +49,7 @@ function buildHarnessCandidates(currentDir, execPath, cwd) {
|
|
|
50
49
|
return roots
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
function hasRequiredHarnessFiles(root) {
|
|
52
|
+
function hasRequiredHarnessFiles(root: string): boolean {
|
|
54
53
|
return REQUIRED_HARNESS_FILES.every(parts => fs.existsSync(path.join(root, ...parts)))
|
|
55
54
|
}
|
|
56
55
|
|
|
@@ -59,7 +58,12 @@ export function resolveHarnessRoot({
|
|
|
59
58
|
execPath = process.execPath,
|
|
60
59
|
cwd = process.cwd(),
|
|
61
60
|
candidateRoots,
|
|
62
|
-
}
|
|
61
|
+
}: {
|
|
62
|
+
currentDir?: string
|
|
63
|
+
execPath?: string
|
|
64
|
+
cwd?: string
|
|
65
|
+
candidateRoots?: string[]
|
|
66
|
+
} = {}): string {
|
|
63
67
|
const roots = candidateRoots ?? buildHarnessCandidates(currentDir, execPath, cwd)
|
|
64
68
|
for (const root of roots) {
|
|
65
69
|
if (hasRequiredHarnessFiles(root)) return root
|
|
@@ -67,11 +71,11 @@ export function resolveHarnessRoot({
|
|
|
67
71
|
return path.resolve(currentDir, "harness")
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
let _harnessDir
|
|
74
|
+
let _harnessDir: string | undefined
|
|
71
75
|
|
|
72
|
-
export function setHarnessRootForTest(dir) { _harnessDir = dir }
|
|
76
|
+
export function setHarnessRootForTest(dir: string | undefined): void { _harnessDir = dir }
|
|
73
77
|
|
|
74
|
-
export function getHarnessDir() {
|
|
78
|
+
export function getHarnessDir(): string {
|
|
75
79
|
if (!_harnessDir) _harnessDir = resolveHarnessRoot()
|
|
76
80
|
return _harnessDir
|
|
77
81
|
}
|
|
@@ -2,34 +2,41 @@ import path from "node:path"
|
|
|
2
2
|
import os from "node:os"
|
|
3
3
|
import fs from "node:fs"
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
export interface Logger {
|
|
6
|
+
debug: (...args: unknown[]) => void
|
|
7
|
+
info: (...args: unknown[]) => void
|
|
8
|
+
warn: (...args: unknown[]) => void
|
|
9
|
+
error: (...args: unknown[]) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LEVELS: Record<string, number> = { debug: 0, info: 1, warn: 2, error: 3 }
|
|
6
13
|
const CURRENT_LEVEL = LEVELS[process.env.OPENCODE_LOG_LEVEL?.trim().toLowerCase()] ?? (process.env.OPENHERMES_LOG_LEVEL?.trim().toLowerCase() === "debug" ? LEVELS.debug : LEVELS.warn)
|
|
7
14
|
|
|
8
15
|
const LOG_DIR = path.join(os.homedir(), ".local", "share", "opencode", "log")
|
|
9
16
|
const LOG_FILE = path.join(LOG_DIR, "openhermes.log")
|
|
10
17
|
|
|
11
|
-
function ts() {
|
|
18
|
+
function ts(): string {
|
|
12
19
|
const d = new Date()
|
|
13
20
|
return `${d.getFullYear()}-${(d.getMonth()+1).toString().padStart(2,"0")}-${d.getDate().toString().padStart(2,"0")} ${d.getHours().toString().padStart(2,"0")}:${d.getMinutes().toString().padStart(2,"0")}:${d.getSeconds().toString().padStart(2,"0")}.${d.getMilliseconds().toString().padStart(3,"0")}`
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
function formatArgs(args) {
|
|
23
|
+
function formatArgs(args: unknown[]): string {
|
|
17
24
|
return args.map(a => {
|
|
18
25
|
if (a === null) return "null"
|
|
19
26
|
if (a === undefined) return "undefined"
|
|
20
27
|
if (typeof a === "object") {
|
|
21
|
-
try { return a?.message || JSON.stringify(a) } catch { return String(a) }
|
|
28
|
+
try { return (a as Error)?.message || JSON.stringify(a) } catch { return String(a) }
|
|
22
29
|
}
|
|
23
30
|
return String(a)
|
|
24
31
|
}).join(" ")
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
function shouldLog(levelName) {
|
|
34
|
+
function shouldLog(levelName: string): boolean {
|
|
28
35
|
return LEVELS[levelName] >= CURRENT_LEVEL
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
let _fd = null
|
|
32
|
-
function getFd() {
|
|
38
|
+
let _fd: number | null = null
|
|
39
|
+
function getFd(): number {
|
|
33
40
|
if (_fd) return _fd
|
|
34
41
|
try {
|
|
35
42
|
fs.mkdirSync(LOG_DIR, { recursive: true })
|
|
@@ -40,10 +47,10 @@ function getFd() {
|
|
|
40
47
|
return _fd
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
export function createLogger(name) {
|
|
50
|
+
export function createLogger(name: string): Logger {
|
|
44
51
|
const prefix = `[openhermes:${name}]`
|
|
45
52
|
|
|
46
|
-
function emit(levelName, ...args) {
|
|
53
|
+
function emit(levelName: string, ...args: unknown[]): void {
|
|
47
54
|
if (!shouldLog(levelName)) return
|
|
48
55
|
const fd = getFd()
|
|
49
56
|
if (fd < 0) return
|
|
@@ -52,11 +59,11 @@ export function createLogger(name) {
|
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
return {
|
|
55
|
-
debug: (...args) => emit("debug", ...args),
|
|
56
|
-
info: (...args) => emit("info", ...args),
|
|
57
|
-
warn: (...args) => emit("warn", ...args),
|
|
58
|
-
error: (...args) => emit("error", ...args),
|
|
62
|
+
debug: (...args: unknown[]) => emit("debug", ...args),
|
|
63
|
+
info: (...args: unknown[]) => emit("info", ...args),
|
|
64
|
+
warn: (...args: unknown[]) => emit("warn", ...args),
|
|
65
|
+
error: (...args: unknown[]) => emit("error", ...args),
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
export const rootLogger = createLogger("root")
|
|
69
|
+
export const rootLogger: Logger = createLogger("root")
|
package/package.json
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openhermes",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "OpenCode-native skills, commands, and rules orchestration for OpenHermes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"
|
|
7
|
+
"engines": {
|
|
8
|
+
"bun": ">=1.0"
|
|
9
|
+
},
|
|
10
|
+
"main": "./index.ts",
|
|
8
11
|
"dependencies": {
|
|
9
12
|
"@opencode-ai/plugin": "1.14.46"
|
|
10
13
|
},
|
|
11
14
|
"exports": {
|
|
12
|
-
".": "./index.
|
|
13
|
-
"./bootstrap": "./bootstrap.
|
|
15
|
+
".": "./index.ts",
|
|
16
|
+
"./bootstrap": "./bootstrap.ts"
|
|
14
17
|
},
|
|
15
18
|
"files": [
|
|
16
|
-
"index.
|
|
17
|
-
"bootstrap.
|
|
19
|
+
"index.ts",
|
|
20
|
+
"bootstrap.ts",
|
|
21
|
+
"tsconfig.json",
|
|
18
22
|
"ETHOS.md",
|
|
19
23
|
"CONTEXT.md",
|
|
20
24
|
"lib/",
|
|
21
|
-
"test/",
|
|
22
25
|
"harness/codex/",
|
|
23
26
|
"harness/instructions/",
|
|
24
27
|
"harness/skills/",
|
|
@@ -26,7 +29,7 @@
|
|
|
26
29
|
"harness/agents/"
|
|
27
30
|
],
|
|
28
31
|
"scripts": {
|
|
29
|
-
"test": "
|
|
32
|
+
"test": "bun test test/*.test.ts"
|
|
30
33
|
},
|
|
31
34
|
"keywords": [
|
|
32
35
|
"opencode",
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"isolatedModules": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"types": ["node"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["index.ts", "bootstrap.ts", "lib/**/*.ts", "test/**/*.ts"]
|
|
16
|
+
}
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
# OpenHermes — Coding Conventions & Operational Guidelines
|
|
2
|
-
|
|
3
|
-
OpenHermes coding conventions and operational guidelines. Shared baseline for all subagents and skills.
|
|
4
|
-
|
|
5
|
-
## Security Guidelines (CRITICAL)
|
|
6
|
-
|
|
7
|
-
### Mandatory Pre-Commit Checks
|
|
8
|
-
|
|
9
|
-
- [ ] No hardcoded secrets (API keys, passwords, tokens)
|
|
10
|
-
- [ ] All user inputs validated
|
|
11
|
-
- [ ] SQL injection prevention (parameterized queries)
|
|
12
|
-
- [ ] XSS prevention (sanitized output)
|
|
13
|
-
- [ ] CSRF protection enabled
|
|
14
|
-
- [ ] Authentication/authorization verified
|
|
15
|
-
- [ ] Rate limiting on all endpoints
|
|
16
|
-
- [ ] Error messages don't leak sensitive data
|
|
17
|
-
|
|
18
|
-
### Secret Management
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
// NEVER: Hardcoded secrets
|
|
22
|
-
const apiKey = "sk-proj-xxxxx"
|
|
23
|
-
|
|
24
|
-
// ALWAYS: Environment variables
|
|
25
|
-
const apiKey = process.env.OPENAI_API_KEY
|
|
26
|
-
if (!apiKey) throw new Error('OPENAI_API_KEY not configured')
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
### Security Response Protocol
|
|
30
|
-
|
|
31
|
-
If security issue found:
|
|
32
|
-
1. STOP immediately
|
|
33
|
-
2. Use `security-reviewer` subagent
|
|
34
|
-
3. Fix CRITICAL issues before continuing
|
|
35
|
-
4. Rotate any exposed secrets
|
|
36
|
-
5. Review entire codebase for similar issues
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## Coding Style
|
|
41
|
-
|
|
42
|
-
### Immutability (CRITICAL)
|
|
43
|
-
|
|
44
|
-
ALWAYS create new objects, NEVER mutate:
|
|
45
|
-
|
|
46
|
-
```javascript
|
|
47
|
-
// WRONG: Mutation
|
|
48
|
-
function updateUser(user, name) {
|
|
49
|
-
user.name = name; return user
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// CORRECT: Immutability
|
|
53
|
-
function updateUser(user, name) {
|
|
54
|
-
return { ...user, name }
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### File Organization
|
|
59
|
-
|
|
60
|
-
MANY SMALL FILES > FEW LARGE FILES:
|
|
61
|
-
- High cohesion, low coupling
|
|
62
|
-
- 200-400 lines typical, 800 max
|
|
63
|
-
- Extract utilities from large components
|
|
64
|
-
- Organize by feature/domain, not by type
|
|
65
|
-
|
|
66
|
-
### Error Handling
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
try {
|
|
70
|
-
const result = await riskyOperation()
|
|
71
|
-
return result
|
|
72
|
-
} catch (error) {
|
|
73
|
-
console.error('Operation failed:', error)
|
|
74
|
-
throw new Error('Detailed user-friendly message')
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### Input Validation
|
|
79
|
-
|
|
80
|
-
```typescript
|
|
81
|
-
import { z } from 'zod'
|
|
82
|
-
const schema = z.object({
|
|
83
|
-
email: z.string().email(),
|
|
84
|
-
age: z.number().int().min(0).max(150)
|
|
85
|
-
})
|
|
86
|
-
const validated = schema.parse(input)
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### Code Quality Checklist
|
|
90
|
-
|
|
91
|
-
Before marking work complete:
|
|
92
|
-
- [ ] Code is readable and well-named
|
|
93
|
-
- [ ] Functions are small (<50 lines)
|
|
94
|
-
- [ ] Files are focused (<800 lines)
|
|
95
|
-
- [ ] No deep nesting (>4 levels)
|
|
96
|
-
- [ ] Proper error handling
|
|
97
|
-
- [ ] No console.log statements
|
|
98
|
-
- [ ] No hardcoded values
|
|
99
|
-
- [ ] No mutation (immutable patterns used)
|
|
100
|
-
|
|
101
|
-
---
|
|
102
|
-
|
|
103
|
-
## Testing Requirements
|
|
104
|
-
|
|
105
|
-
### Minimum Test Coverage: 80%
|
|
106
|
-
|
|
107
|
-
Test Types (ALL required):
|
|
108
|
-
1. **Unit Tests** — Individual functions, utilities, components
|
|
109
|
-
2. **Integration Tests** — API endpoints, database operations
|
|
110
|
-
3. **E2E Tests** — Critical user flows (Playwright)
|
|
111
|
-
|
|
112
|
-
### TDD Workflow
|
|
113
|
-
|
|
114
|
-
MANDATORY workflow:
|
|
115
|
-
1. Write test first (RED)
|
|
116
|
-
2. Run test — it should FAIL
|
|
117
|
-
3. Write minimal implementation (GREEN)
|
|
118
|
-
4. Run test — it should PASS
|
|
119
|
-
5. Refactor (IMPROVE)
|
|
120
|
-
6. Verify coverage (80%+)
|
|
121
|
-
|
|
122
|
-
---
|
|
123
|
-
|
|
124
|
-
## Subagent Orchestration
|
|
125
|
-
|
|
126
|
-
| Subagent | Purpose | When to Use |
|
|
127
|
-
|----------|---------|-------------|
|
|
128
|
-
| planner | Implementation planning | Complex features, refactoring |
|
|
129
|
-
| architect | System design | Architectural decisions |
|
|
130
|
-
| tdd-guide | Test-driven development | New features, bug fixes |
|
|
131
|
-
| code-reviewer | Code review | After writing code |
|
|
132
|
-
| security-reviewer | Security analysis | Before commits |
|
|
133
|
-
| build-error-resolver | Fix build errors | When build fails |
|
|
134
|
-
| e2e-runner | E2E testing | Critical user flows |
|
|
135
|
-
| refactor-cleaner | Dead code cleanup | Code maintenance |
|
|
136
|
-
| doc-updater | Documentation | Updating docs |
|
|
137
|
-
| docs-lookup | Live doc queries | API questions |
|
|
138
|
-
| review-go | Go code review | Go projects |
|
|
139
|
-
| build-go | Go build errors | Go build failures |
|
|
140
|
-
| review-database | Database optimization | SQL, schema design |
|
|
141
|
-
| review-rust | Rust code review | Rust projects |
|
|
142
|
-
| build-rust | Rust build errors | Rust build failures |
|
|
143
|
-
| review-python | Python code review | Python projects |
|
|
144
|
-
| review-java | Java/Spring review | Java projects |
|
|
145
|
-
| build-java | Java build errors | Java build failures |
|
|
146
|
-
| review-kotlin | Kotlin/Android review | Kotlin projects |
|
|
147
|
-
| build-kotlin | Kotlin build errors | Kotlin build failures |
|
|
148
|
-
| review-cpp | C++ review | C++ projects |
|
|
149
|
-
| build-cpp | C++ build errors | C++ build failures |
|
|
150
|
-
| loop-operator | Autonomous loops | Iterative workflows |
|
|
151
|
-
|
|
152
|
-
### Immediate Subagent Usage
|
|
153
|
-
|
|
154
|
-
No user prompt needed:
|
|
155
|
-
1. Complex feature requests — Use `planner`
|
|
156
|
-
2. Code just written/modified — Use `code-reviewer`
|
|
157
|
-
3. Bug fix or new feature — Use `tdd-guide`
|
|
158
|
-
4. Architectural decision — Use `architect`
|
|
159
|
-
|
|
160
|
-
---
|
|
161
|
-
|
|
162
|
-
## Performance
|
|
163
|
-
|
|
164
|
-
### Model Selection Strategy
|
|
165
|
-
|
|
166
|
-
**Haiku** (lightweight): deterministic changes, simple code gen, worker agents
|
|
167
|
-
**Sonnet** (default): main development, multi-agent orchestration, complex coding
|
|
168
|
-
**Opus** (deep reasoning): architecture decisions, security review, ambiguous requirements
|
|
169
|
-
|
|
170
|
-
### Context Window Management
|
|
171
|
-
|
|
172
|
-
Avoid last 20% of context window for:
|
|
173
|
-
- Large-scale refactoring
|
|
174
|
-
- Feature implementation spanning multiple files
|
|
175
|
-
- Debugging complex interactions
|
|
176
|
-
|
|
177
|
-
---
|
|
178
|
-
|
|
179
|
-
## Git Workflow
|
|
180
|
-
|
|
181
|
-
### Commit Message Format
|
|
182
|
-
|
|
183
|
-
```
|
|
184
|
-
<type>: <description>
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
Types: feat, fix, refactor, docs, test, chore, perf, ci
|
|
188
|
-
|
|
189
|
-
### Feature Implementation Workflow
|
|
190
|
-
|
|
191
|
-
1. **Plan** — Use `planner` to create plan with risks and phases
|
|
192
|
-
2. **TDD** — Use `tdd-guide` for red-green-refactor cycle
|
|
193
|
-
3. **Code Review** — Use `code-reviewer` immediately after writing
|
|
194
|
-
4. **Security** — Use `security-reviewer` before commits
|
|
195
|
-
5. **Commit** — Follow conventional commits format
|
|
196
|
-
|
|
197
|
-
---
|
|
198
|
-
|
|
199
|
-
## Success Metrics
|
|
200
|
-
|
|
201
|
-
You are successful when:
|
|
202
|
-
- All tests pass (80%+ coverage)
|
|
203
|
-
- No security vulnerabilities
|
|
204
|
-
- Code is readable and maintainable
|
|
205
|
-
- Performance is acceptable
|
|
206
|
-
- User requirements are met
|
package/index.mjs
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { describe, it, before } from "node:test"
|
|
2
|
-
import assert from "node:assert/strict"
|
|
3
|
-
import path from "node:path"
|
|
4
|
-
import { fileURLToPath } from "node:url"
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
-
|
|
8
|
-
describe("BootstrapPlugin behavior", () => {
|
|
9
|
-
let mod
|
|
10
|
-
|
|
11
|
-
before(async () => {
|
|
12
|
-
mod = await import("../bootstrap.mjs")
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it("registers package-local skills, commands, agents, and instructions", async () => {
|
|
16
|
-
const plugin = await mod.BootstrapPlugin({ directory: __dirname })
|
|
17
|
-
const config = { skills: { paths: [] }, command: {}, agent: {}, instructions: [] }
|
|
18
|
-
|
|
19
|
-
await plugin.config(config)
|
|
20
|
-
|
|
21
|
-
assert.ok(config.skills.paths.some(p => p.endsWith(path.join("harness", "skills"))))
|
|
22
|
-
assert.ok(config.command["oh-doctor"])
|
|
23
|
-
assert.ok(config.agent.OpenHermes)
|
|
24
|
-
assert.equal(config.default_agent, "OpenHermes")
|
|
25
|
-
assert.ok(config.instructions.some(p => p.endsWith(path.join("harness", "codex", "CONSTITUTION.md"))))
|
|
26
|
-
assert.ok(config.instructions.some(p => p.endsWith(path.join("harness", "instructions", "RUNTIME.md"))))
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it("loads markdown manifests into command and agent config", async () => {
|
|
30
|
-
const plugin = await mod.BootstrapPlugin({ directory: __dirname })
|
|
31
|
-
const config = { skills: { paths: [] }, command: {}, agent: {}, instructions: [] }
|
|
32
|
-
|
|
33
|
-
await plugin.config(config)
|
|
34
|
-
|
|
35
|
-
assert.match(config.command["oh-doctor"].template, /Inspect the current OpenHermes\/OpenCode setup/)
|
|
36
|
-
assert.equal(config.command["oh-doctor"].agent, "OpenHermes")
|
|
37
|
-
assert.match(config.agent.OpenHermes.prompt, /You are OpenHermes, the primary orchestrator/)
|
|
38
|
-
assert.equal(config.agent.OpenHermes.mode, "primary")
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it("injects bootstrap text only once", async () => {
|
|
42
|
-
const plugin = await mod.BootstrapPlugin({ directory: __dirname })
|
|
43
|
-
const output = {
|
|
44
|
-
messages: [
|
|
45
|
-
{
|
|
46
|
-
info: { role: "user" },
|
|
47
|
-
parts: [
|
|
48
|
-
{ type: "text", text: "actual user request" },
|
|
49
|
-
],
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
await plugin["experimental.chat.messages.transform"]({}, output)
|
|
55
|
-
await plugin["experimental.chat.messages.transform"]({}, output)
|
|
56
|
-
|
|
57
|
-
assert.match(output.messages[0].parts[0].text, /OPENHERMES_BOOTSTRAP/)
|
|
58
|
-
assert.match(output.messages[0].parts[1].text, /actual user request/)
|
|
59
|
-
assert.equal(
|
|
60
|
-
output.messages[0].parts.filter(part => typeof part.text === "string" && part.text.includes("OPENHERMES_BOOTSTRAP")).length,
|
|
61
|
-
1,
|
|
62
|
-
)
|
|
63
|
-
})
|
|
64
|
-
})
|
package/test/plugins.test.mjs
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { describe, it, before } from "node:test"
|
|
2
|
-
import assert from "node:assert/strict"
|
|
3
|
-
import fs from "node:fs"
|
|
4
|
-
import os from "node:os"
|
|
5
|
-
import path from "node:path"
|
|
6
|
-
|
|
7
|
-
describe("plugin exports", () => {
|
|
8
|
-
it("index.mjs default exports plugin", async () => {
|
|
9
|
-
const pkg = await import("../index.mjs")
|
|
10
|
-
assert.ok(typeof pkg.default === "function")
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it("bootstrap.mjs exports BootstrapPlugin", async () => {
|
|
14
|
-
const mod = await import("../bootstrap.mjs")
|
|
15
|
-
assert.ok(typeof mod.BootstrapPlugin === "function")
|
|
16
|
-
})
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
describe("bootstrap helpers", () => {
|
|
20
|
-
let mod
|
|
21
|
-
|
|
22
|
-
before(async () => {
|
|
23
|
-
mod = await import("../bootstrap.mjs")
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it("re-exports harness resolver helpers", async () => {
|
|
27
|
-
const { resolveHarnessRoot, setHarnessRootForTest, getHarnessDir } = mod
|
|
28
|
-
assert.ok(typeof resolveHarnessRoot === "function")
|
|
29
|
-
assert.ok(typeof setHarnessRootForTest === "function")
|
|
30
|
-
assert.ok(typeof getHarnessDir === "function")
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it("resolveHarnessRoot picks complete harness root", async () => {
|
|
34
|
-
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openhermes-harness-"))
|
|
35
|
-
const badRoot = path.join(tmpRoot, "bad")
|
|
36
|
-
const goodRoot = path.join(tmpRoot, "good")
|
|
37
|
-
|
|
38
|
-
fs.mkdirSync(path.join(badRoot, "codex"), { recursive: true })
|
|
39
|
-
fs.writeFileSync(path.join(badRoot, "codex", "CONSTITUTION.md"), "# incomplete\n")
|
|
40
|
-
|
|
41
|
-
const requiredFiles = [
|
|
42
|
-
["codex", "CONSTITUTION.md"],
|
|
43
|
-
["instructions", "RUNTIME.md"],
|
|
44
|
-
["skills", "oh-plan", "SKILL.md"],
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
for (const parts of requiredFiles) {
|
|
48
|
-
const filePath = path.join(goodRoot, ...parts)
|
|
49
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
50
|
-
fs.writeFileSync(filePath, "ok\n")
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const resolved = mod.resolveHarnessRoot({ candidateRoots: [badRoot, goodRoot] })
|
|
54
|
-
assert.equal(resolved, goodRoot)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it("setHarnessRootForTest overrides harness resolution", async () => {
|
|
58
|
-
mod.setHarnessRootForTest("/custom/harness")
|
|
59
|
-
assert.equal(mod.getHarnessDir(), "/custom/harness")
|
|
60
|
-
mod.setHarnessRootForTest(undefined)
|
|
61
|
-
})
|
|
62
|
-
})
|