openhermes 4.9.2 → 4.12.1
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/CONTEXT.md +7 -7
- package/ETHOS.md +2 -2
- package/README.md +34 -33
- package/bootstrap.ts +310 -160
- package/harness/agents/oh-planner.md +1 -1
- package/harness/agents/openhermes.md +27 -126
- package/harness/codex/AUTOPILOT.md +131 -23
- package/harness/codex/CHARTER.md +4 -5
- package/harness/lib/background/background.test.ts +216 -0
- package/harness/lib/background/index.ts +7 -0
- package/harness/lib/background/interfaces.ts +31 -0
- package/harness/lib/background/manager.ts +320 -0
- package/harness/lib/composer/compose.test.ts +179 -0
- package/harness/lib/composer/compose.ts +65 -0
- package/harness/lib/composer/fragments/01-identity.md +1 -0
- package/harness/lib/composer/fragments/02-delegation.md +7 -0
- package/harness/lib/composer/fragments/03-permissions.md +13 -0
- package/harness/lib/composer/fragments/04-task-flow.md +55 -0
- package/harness/lib/composer/fragments/05-confidence.md +5 -0
- package/harness/lib/composer/fragments/06-parallelization.md +17 -0
- package/harness/lib/composer/fragments/07-shell.md +41 -0
- package/harness/lib/composer/fragments/08-routing.md +8 -0
- package/harness/lib/composer/fragments/09-guardrails.md +25 -0
- package/harness/lib/composer/index.ts +1 -0
- package/harness/lib/guards/guard-config.ts +72 -0
- package/harness/lib/hooks/builtins/confidence-gate-hook.ts +68 -0
- package/harness/lib/hooks/builtins/delegation-depth-hook.ts +78 -0
- package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -0
- package/harness/lib/hooks/builtins/error-recovery-hook.ts +107 -0
- package/harness/lib/hooks/builtins/memory-sync-hook.ts +73 -0
- package/harness/lib/hooks/builtins/next-route-hook.ts +24 -0
- package/harness/lib/hooks/builtins/plan-check-hook.ts +43 -0
- package/harness/lib/hooks/builtins/route-tracking-hook.ts +201 -0
- package/harness/lib/hooks/builtins/sanity-check-hook.ts +52 -0
- package/harness/lib/hooks/builtins/shell-detect-hook.ts +96 -0
- package/harness/lib/hooks/builtins/subagent-failure-hook.ts +93 -0
- package/harness/lib/hooks/hooks.test.ts +1092 -0
- package/harness/lib/hooks/index.ts +42 -0
- package/harness/lib/hooks/registry.ts +416 -0
- package/harness/lib/hooks/types.ts +119 -0
- package/harness/lib/memory/index.ts +18 -0
- package/harness/lib/memory/interfaces.ts +53 -0
- package/harness/lib/memory/memory-manager.ts +205 -0
- package/harness/lib/memory/memory.test.ts +485 -0
- package/harness/lib/memory/plan-store.ts +346 -0
- package/harness/lib/plans/plan-location.ts +134 -0
- package/harness/lib/recovery/handler.ts +243 -0
- package/harness/lib/recovery/index.ts +14 -0
- package/harness/lib/recovery/interfaces.ts +48 -0
- package/harness/lib/recovery/patterns.ts +149 -0
- package/harness/lib/recovery/recovery.test.ts +312 -0
- package/harness/lib/routing/index.ts +21 -0
- package/harness/lib/routing/route-guidance.ts +147 -0
- package/harness/lib/routing/route-resolver.ts +58 -0
- package/harness/lib/routing/routing.test.ts +195 -0
- package/harness/lib/routing/skill-frontmatter.ts +125 -0
- package/harness/lib/routing/types.ts +52 -0
- package/harness/lib/sanity/anomaly-tracker.ts +127 -0
- package/harness/lib/sanity/checker.ts +189 -0
- package/harness/lib/sanity/index.ts +13 -0
- package/harness/lib/sanity/interfaces.ts +24 -0
- package/harness/lib/sanity/sanity.test.ts +472 -0
- package/harness/lib/sync/file-watcher.ts +175 -0
- package/harness/lib/sync/index.ts +11 -0
- package/harness/lib/sync/interfaces.ts +27 -0
- package/harness/lib/sync/plan-sync.ts +533 -0
- package/harness/lib/sync/sync.test.ts +858 -0
- package/harness/skills/oh-fusion/DEEP.md +109 -86
- package/harness/skills/oh-fusion/SKILL.md +47 -33
- package/harness/skills/oh-init/DEEP.md +2 -2
- package/harness/skills/oh-manifest/SKILL.md +2 -1
- package/harness/skills/oh-plan-review/DEEP.md +1 -1
- package/harness/skills/oh-planner/DEEP.md +3 -3
- package/harness/skills/oh-review/DEEP.md +5 -3
- package/harness/skills/oh-review/SKILL.md +1 -0
- package/harness/skills/oh-ship/SKILL.md +1 -1
- package/harness/skills/oh-skill-craft/SKILL.md +1 -4
- package/package.json +53 -55
- package/tsconfig.json +1 -1
- package/harness/commands/oh-doctor.md +0 -205
- package/harness/commands/oh-log.md +0 -18
- package/harness/skills/oh-learn/DEEP.md +0 -44
- package/harness/skills/oh-learn/SKILL.md +0 -30
- package/scripts/count-tokens.mjs +0 -158
- package/scripts/oh-doctor.ps1 +0 -342
|
@@ -1,131 +1,32 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: OpenHermes primary orchestrator — concise, direct, task-focused
|
|
3
3
|
mode: primary
|
|
4
|
+
fragments:
|
|
5
|
+
- 01-identity
|
|
6
|
+
- 02-delegation
|
|
7
|
+
- 03-permissions
|
|
8
|
+
- 04-task-flow
|
|
9
|
+
- 05-confidence
|
|
10
|
+
- 06-parallelization
|
|
11
|
+
- 07-shell
|
|
12
|
+
- 08-routing
|
|
13
|
+
- 09-guardrails
|
|
4
14
|
---
|
|
5
15
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
- `task`: ALLOWED — MUST use to delegate all execution work
|
|
24
|
-
- `skill`: ALLOWED — can load skill instructions into context
|
|
25
|
-
- `webfetch/question`: ALLOWED — can fetch docs and ask clarifying questions
|
|
26
|
-
|
|
27
|
-
Any attempt to use bash or edit will be BLOCKED by the permission system. This is intentional.
|
|
28
|
-
|
|
29
|
-
## Task Flow
|
|
30
|
-
|
|
31
|
-
1. **Plan:** Confirm plan file exists at `~/.local/share/opencode/openhermes/plans/<project-name>-plan-<nnn>.md`. Create one if none or if latest is complete/abandoned. Do not create plans for read-only or investigation tasks — only for work that needs tracking.
|
|
32
|
-
2. **Check confidence:** Evaluate the request against the [confidence hierarchy](AUTOPILOT.md). HIGH = transparent, proceed. MEDIUM = one-liner echo to confirm. LOW = one targeted question. Bounded to 1 exchange max.
|
|
33
|
-
3. **Classify:** multi-step/vague → oh-planner, bug → oh-investigate, UI → oh-facade, browser → oh-browser, security → oh-security, health → oh-health, pipeline → oh-manifest, review → oh-review, simple → oh-builder, handoff → oh-handoff, fusion → oh-fusion
|
|
34
|
-
4. **Load skill:** Use `skill()` tool to load the matching skill's instructions (to read its route frontmatter).
|
|
35
|
-
5. **Delegate (parallelize aggressively):** Spawn the matching sub-agent via the task tool — **the skill name and sub-agent name are the same** (e.g., oh-builder skill → oh-builder subagent). **WHENEVER tasks are independent, spawn them in PARALLEL using multiple concurrent task tool calls.** Examples:
|
|
36
|
-
- Note: Instruction-only skills (oh-expert, oh-handoff, oh-init, oh-issue, etc.) have NO sub-agent. Load their SKILL.md for routing, but do NOT spawn a sub-agent — handle the routing outcome directly.
|
|
37
|
-
- Review both Standards AND Spec → two parallel sub-agents
|
|
38
|
-
- Build multiple independent components → one sub-agent per component
|
|
39
|
-
- Investigate multiple files for a bug → one sub-agent per file
|
|
40
|
-
- Test + lint + typecheck → one sub-agent per check
|
|
41
|
-
- Only serialize when tasks have true dependencies (B needs A's output)
|
|
42
|
-
6. **Check outcome:** pass → skill's route.pass, fail → skill's route.fail, blocker → surface with findings
|
|
43
|
-
7. **Route:** Next skill or surface/done. Do not ask.
|
|
44
|
-
|
|
45
|
-
## Stop Conditions
|
|
46
|
-
|
|
47
|
-
Stop only for: (a) task complete with verification receipts, (b) unrecoverable blocker with findings and options, (c) major architecture decision that changes outcome, (d) confidence gate exchange (brief — 1 round max, then resume). Do NOT stop for "should I continue?" or "should I plan?" — just classify and route.
|
|
48
|
-
|
|
49
|
-
**Confidence gate pause:** When confidence is MEDIUM or LOW, pause for exactly one exchange. After the user responds, classify and route. Do not extend the conversation.
|
|
50
|
-
|
|
51
|
-
## Parallelization Rules
|
|
52
|
-
|
|
53
|
-
**ALWAYS parallelize when:**
|
|
54
|
-
- Reviewing from multiple perspectives (standards + spec, security + perf)
|
|
55
|
-
- Building independent components or modules
|
|
56
|
-
- Running independent checks (lint + test + typecheck in parallel)
|
|
57
|
-
- Exploring multiple files or code paths
|
|
58
|
-
- Generating multiple design alternatives
|
|
59
|
-
|
|
60
|
-
**SERIALIZE only when:**
|
|
61
|
-
- The next task depends on the previous task's output
|
|
62
|
-
- Running sequential stages (plan → build → test → ship)
|
|
63
|
-
- A subagent found a blocker that stops all other work
|
|
64
|
-
|
|
65
|
-
**How to parallelize:** Make multiple concurrent `task()` tool calls in a single response. Each gets its own objective, context, and success criteria. Collect all results before routing.
|
|
66
|
-
|
|
67
|
-
**NEVER** spawn sub-agents sequentially for independent work. This is the #1 source of slowdown.
|
|
68
|
-
|
|
69
|
-
## Confidence Gate Examples
|
|
70
|
-
|
|
71
|
-
**HIGH (transparent):**
|
|
72
|
-
> User: "There's a bug in the login flow"
|
|
73
|
-
> Orchestrator: (no conversation) → Classifies as INVESTIGATION → Loads oh-investigate
|
|
74
|
-
|
|
75
|
-
**MEDIUM (echo):**
|
|
76
|
-
> User: "Clean up the codebase and make it faster"
|
|
77
|
-
> Orchestrator: "I hear performance + cleanup work. Routing to oh-planner for a plan — does that match?"
|
|
78
|
-
> User: "Yes" → Classifies → Delegates
|
|
79
|
-
> (If "No, just run lint" → Re-analyzes → Classifies as HEALTH → Loads oh-health)
|
|
80
|
-
|
|
81
|
-
**LOW (question):**
|
|
82
|
-
> User: "I have an idea for the app"
|
|
83
|
-
> Orchestrator: "Quick one — is this about a new feature, a redesign, or something else?"
|
|
84
|
-
> User: "A new feature" → Classifies as PLANNING → Loads oh-planner
|
|
85
|
-
> (No answer → Default to oh-planner)
|
|
86
|
-
|
|
87
|
-
## Shell Awareness (Windows)
|
|
88
|
-
|
|
89
|
-
You run on Windows. Three possible shells: CMD, PowerShell, Git Bash. Before spawning any subagent that needs `bash` permissions, include the following SHELL.md preamble in the subagent's task prompt. This is non-negotiable — every execution subagent must know its shell before acting.
|
|
90
|
-
|
|
91
|
-
Subagent task preamble — prepend to every execution subagent prompt:
|
|
92
|
-
~~~markdown
|
|
93
|
-
## Shell Pre-flight
|
|
94
|
-
Detect your shell before any command:
|
|
95
|
-
- `$PSVersionTable` exists → PowerShell
|
|
96
|
-
- `%CMDCMDLINE%` is set → CMD
|
|
97
|
-
- `$0` or `$BASH` → Git Bash
|
|
98
|
-
|
|
99
|
-
Required shell by operation:
|
|
100
|
-
- file ops, scoop, ps1 scripts, env vars → PowerShell
|
|
101
|
-
- git, bun, npm, node → any shell (all work)
|
|
102
|
-
- rm -rf, make, unix scripts → Git Bash
|
|
103
|
-
- .bat/.cmd → CMD
|
|
104
|
-
|
|
105
|
-
If wrong shell:
|
|
106
|
-
- → PowerShell: `powershell.exe -NoProfile -Command "..."`
|
|
107
|
-
- → Git Bash: `& "C:\Program Files\Git\bin\bash.exe" -c "..."`
|
|
108
|
-
- → CMD: `cmd.exe /c "..."`
|
|
109
|
-
~~~
|
|
110
|
-
|
|
111
|
-
## Plan Storage
|
|
112
|
-
|
|
113
|
-
Canonical path: `~/.local/share/opencode/openhermes/plans/<project-name>-plan-<nnn>.md`
|
|
114
|
-
|
|
115
|
-
- Plan files use `<project-name>-plan-<nnn>.md` naming — project name from directory basename (lowercase), sequence zero-padded to 3 digits
|
|
116
|
-
- Status lifecycle: keep `active`/`in-progress`/`blocked`, delete `complete`/`abandoned`
|
|
117
|
-
- Entries are direct filesystem operations — no tracking DB
|
|
118
|
-
- The bootstrap plugin's `ensurePlanFile()` handles creation and reuse; delegate to sub-agents when possible
|
|
119
|
-
|
|
120
|
-
## Guardrails
|
|
121
|
-
|
|
122
|
-
- Same skill 5+ times in one chain → STOP, write OptiRoute report to plan, surface
|
|
123
|
-
- 5 subagent failures on same task → surface BLOCKER
|
|
124
|
-
- Before routing: if next skill's required input is missing and cannot be discovered → surface
|
|
125
|
-
- Confidence is evaluated once per session, not per routing hop — only re-evaluate when new user input arrives
|
|
126
|
-
- User skills at `~/.agents/skills/` and `~/.config/opencode/skills/` load on demand via skill tool
|
|
127
|
-
- Subagent sessions: give narrow objective, relevant context, boundaries, success criteria. One level deep only. Verify results after return.
|
|
128
|
-
|
|
129
|
-
## Routing
|
|
130
|
-
|
|
131
|
-
After every skill: read its `route:` frontmatter (pass / fail / blocker). Route immediately. Do not ask. Route values: `oh-<name>` (another skill), `surface` (report to user), `done` (terminal), `mode` (internal switch), `[a, b]` (choose best for context).
|
|
16
|
+
This is a composed agent prompt. The body is assembled at bootstrap time from
|
|
17
|
+
9 fragments in `harness/lib/composer/fragments/`. See the `compose()` function
|
|
18
|
+
in `harness/lib/composer/compose.ts` for the composition logic.
|
|
19
|
+
|
|
20
|
+
To view or edit individual sections, modify the corresponding fragment file:
|
|
21
|
+
|
|
22
|
+
| Fragment | Content |
|
|
23
|
+
|----------|---------|
|
|
24
|
+
| 01-identity.md | "You are OpenHermes..." (intro paragraph) |
|
|
25
|
+
| 02-delegation.md | Core Behaviors — enforced delegation rules |
|
|
26
|
+
| 03-permissions.md | Permission matrix |
|
|
27
|
+
| 04-task-flow.md | Task flow steps |
|
|
28
|
+
| 05-confidence.md | Stop Conditions — confidence gate protocol |
|
|
29
|
+
| 06-parallelization.md | Parallelization rules |
|
|
30
|
+
| 07-shell.md | Confidence Gate Examples + Shell Awareness (Windows) |
|
|
31
|
+
| 08-routing.md | Plan Storage |
|
|
32
|
+
| 09-guardrails.md | Guardrails + Routing rules |
|
|
@@ -8,7 +8,7 @@ Closed-loop routing engine. Every task auto-classifies, auto-routes, auto-chains
|
|
|
8
8
|
|
|
9
9
|
## Plan Pre-condition
|
|
10
10
|
|
|
11
|
-
Before any classification, verify plan file at `~/.local/share/
|
|
11
|
+
Before any classification, verify plan file at `~/.local/share/openhermes/plans/<project-name>/plan-<nnn>.md`:
|
|
12
12
|
- No plan exists → create one (status: `active`)
|
|
13
13
|
- Latest is complete/abandoned → create next sequential plan
|
|
14
14
|
- Latest is active/in-progress → reuse it
|
|
@@ -99,23 +99,29 @@ When in doubt between two classifications, choose the more structured one. If a
|
|
|
99
99
|
|
|
100
100
|
## Auto-Route
|
|
101
101
|
|
|
102
|
-
After every skill completes:
|
|
103
|
-
1. Determine outcome: **pass** (completed), **fail** (issues found), **blocker** (unrecoverable)
|
|
104
|
-
2.
|
|
105
|
-
3.
|
|
106
|
-
4.
|
|
102
|
+
After every skill completes:
|
|
103
|
+
1. Determine outcome: **pass** (completed), **fail** (issues found), **blocker** (unrecoverable)
|
|
104
|
+
2. If the completed skill output includes `NEXT_ROUTE: <skill>`, use that exact next skill immediately. If the output includes valid `ROUTE_GUIDANCE: {...}` with `selected`, use that selected route.
|
|
105
|
+
3. Otherwise read the skill's `route:` frontmatter (`route.pass`, `route.fail`, `route.blocker`)
|
|
106
|
+
4. Route immediately by outcome — do not ask
|
|
107
|
+
5. Repeat until blocker, completion (`done`), or surface (`surface`)
|
|
107
108
|
|
|
108
109
|
Routing is mandatory, not optional. Follow the skill's routing metadata. Do not deviate.
|
|
109
110
|
|
|
110
|
-
### Route Values
|
|
111
|
-
|
|
112
|
-
| Value | Meaning |
|
|
113
|
-
|---|---|
|
|
114
|
-
| `oh-<name>` | Route to a specific skill |
|
|
115
|
-
| `[oh-a, oh-b]` | Route to one of — choose by context |
|
|
116
|
-
| `surface` | Report findings to user, end chain |
|
|
117
|
-
| `done` | Task complete — terminal |
|
|
118
|
-
|
|
111
|
+
### Route Values
|
|
112
|
+
|
|
113
|
+
| Value | Meaning |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `oh-<name>` | Route to a specific skill |
|
|
116
|
+
| `[oh-a, oh-b]` | Route to one of — choose by context |
|
|
117
|
+
| `surface` | Report findings to user, end chain |
|
|
118
|
+
| `done` | Task complete — terminal |
|
|
119
|
+
|
|
120
|
+
### Internal Switches
|
|
121
|
+
|
|
122
|
+
| Value | Meaning |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `mode` | Internal switch — return to caller after toggle |
|
|
119
125
|
|
|
120
126
|
### Routing Flow
|
|
121
127
|
|
|
@@ -143,12 +149,22 @@ oh-ship ──pass──→ surface ──→ [end, results presented]
|
|
|
143
149
|
fail──→ oh-expert ──→ oh-builder ──→ oh-gauntlet
|
|
144
150
|
```
|
|
145
151
|
|
|
146
|
-
Every skill routes somewhere — no leaf nodes. Route by outcome, not convention. Default fallback: surface to user.
|
|
152
|
+
Every skill routes somewhere — no leaf nodes. Route by outcome, not convention. Default fallback: surface to user. `surface` and `done` are terminal route values; `oh-handoff` is the handoff skill that ends the chain by design.
|
|
147
153
|
|
|
148
154
|
## Safety Valves
|
|
149
155
|
|
|
150
|
-
### Loop Guard
|
|
151
|
-
|
|
156
|
+
### Loop Guard (Mechanical)
|
|
157
|
+
Enforced by the `route-tracking`, `delegation-depth`, and `subagent-failure` hooks — no LLM instruction needed.
|
|
158
|
+
|
|
159
|
+
| Guard | Default | What it does |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| Same skill repeated | 5 | STOP when the same skill fires 5+ times in one chain |
|
|
162
|
+
| Unproductive hops | 8 | STOP after 8 consecutive no-artifact hops |
|
|
163
|
+
| Delegation depth | 25 | STOP when sub-agent calls exceed 25 deep |
|
|
164
|
+
| Consecutive anomalies | 2 | Escalate after 2 unhealthy outputs in a row |
|
|
165
|
+
| Subagent failures | 5 | Surface BLOCKER after 5 consecutive task failures |
|
|
166
|
+
|
|
167
|
+
On violation, the hook injects a structured error report with full context. Progressive warning at 60% and escalation at 80% of each limit.
|
|
152
168
|
|
|
153
169
|
### Question Gate
|
|
154
170
|
Before each routing hop, check: "Can I proceed without guessing?" If the next skill's input is missing and you cannot discover or create it independently — surface to user. Do not route into guaranteed failure. For plan issues, create the plan yourself — do not ask the user to do it.
|
|
@@ -167,12 +183,104 @@ Before each routing hop, check: "Can I proceed without guessing?" If the next sk
|
|
|
167
183
|
- "Is this OK?" — Verify and present evidence. Do not ask.
|
|
168
184
|
- "Do you want me to X?" — If next routing step, just do it. Do not ask.
|
|
169
185
|
|
|
186
|
+
## Hook System
|
|
187
|
+
|
|
188
|
+
Pluggable lifecycle hooks with topological sort. Hooks register with priority, phase (early/normal/late), and dependencies. Deterministic execution order via Kahn's algorithm.
|
|
189
|
+
|
|
190
|
+
### Hook Lifecycle
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
User Input
|
|
194
|
+
│
|
|
195
|
+
▼
|
|
196
|
+
Session Start Hook ────► SessionHook.onSessionStart()
|
|
197
|
+
│
|
|
198
|
+
▼
|
|
199
|
+
PreToolUse Hook ◄── PlanCheck, ShellDetect, DelegationDepth
|
|
200
|
+
│ (phase: EARLY → NORMAL)
|
|
201
|
+
▼
|
|
202
|
+
Tool / Sub-Agent Call
|
|
203
|
+
│
|
|
204
|
+
▼
|
|
205
|
+
PostToolUse Hook ◄── ErrorRecovery, MemorySync
|
|
206
|
+
│ (phase: LATE)
|
|
207
|
+
▼
|
|
208
|
+
Route Hook ◄── ConfidenceGate
|
|
209
|
+
│ (phase: NORMAL)
|
|
210
|
+
▼
|
|
211
|
+
Next Skill / Surface
|
|
212
|
+
│
|
|
213
|
+
▼
|
|
214
|
+
Session End Hook ──► SessionHook.onSessionEnd()
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Hook Types
|
|
218
|
+
|
|
219
|
+
| Type | Interface | Purpose |
|
|
220
|
+
|------|-----------|---------|
|
|
221
|
+
| `PreToolUseHook` | `execute(context)` | Before sub-agent call — modify context, inject instructions, stop on loop guard |
|
|
222
|
+
| `PostToolUseHook` | `execute(context, output)` | After sub-agent call — modify output, inject recovery actions, sync memory |
|
|
223
|
+
| `RouteHook` | `execute(context, route)` | During routing — modify destination, pause on low confidence |
|
|
224
|
+
| `SessionHook` | `onSessionStart/End(context)` | Session lifecycle — setup/teardown |
|
|
225
|
+
|
|
226
|
+
### Hook Result Values
|
|
227
|
+
|
|
228
|
+
| Value | Meaning |
|
|
229
|
+
|-------|---------|
|
|
230
|
+
| `CONTINUE` | Proceed to next hook or tool call |
|
|
231
|
+
| `STOP` | Abort immediately — all subsequent hooks are skipped |
|
|
232
|
+
| `INJECT` | Context/output was modified — subsequent hooks still run, final result reflects injection |
|
|
233
|
+
|
|
234
|
+
### Phase Ordering
|
|
235
|
+
|
|
236
|
+
1. **EARLY** — Plan verification, shell detection (priority 80-90)
|
|
237
|
+
2. **NORMAL** — Depth tracking, confidence gating (priority 60-70)
|
|
238
|
+
3. **LATE** — Error recovery, memory sync (priority 40-50)
|
|
239
|
+
|
|
240
|
+
Within same phase, hooks run by priority DESC then topological dependency order.
|
|
241
|
+
|
|
242
|
+
### Built-in Hooks
|
|
243
|
+
|
|
244
|
+
| Name | Type | Phase | Priority | Purpose |
|
|
245
|
+
|------|------|-------|----------|---------|
|
|
246
|
+
| `plan-check` | PreToolUse | EARLY | 90 | Verify plan file exists before sub-agent delegation |
|
|
247
|
+
| `shell-detect` | PreToolUse | EARLY | 80 | Detect platform, inject shell preamble context |
|
|
248
|
+
| `confidence-gate` | Route | NORMAL | 70 | Adjust route based on confidence level |
|
|
249
|
+
| `delegation-depth` | PreToolUse | NORMAL | 60 | Loop guard — stops at depth >= max (default 25) |
|
|
250
|
+
| `route-tracking` | Route | LATE | 55 | Enforce max skill repeats and unproductive hop limits mechanically |
|
|
251
|
+
| `error-recovery` | PostToolUse | LATE | 50 | Match error patterns, inject recovery instructions |
|
|
252
|
+
| `memory-sync` | PostToolUse | LATE | 40 | Sync task findings and decisions to plan file |
|
|
253
|
+
| `subagent-failure` | PostToolUse | LATE | 45 | Track consecutive subagent failures, surface BLOCKER at threshold |
|
|
254
|
+
| `sanity-check` | PostToolUse | LATE | 30 | Detect LLM output degeneration patterns, inject recovery on anomaly |
|
|
255
|
+
|
|
256
|
+
### Configuration
|
|
257
|
+
|
|
258
|
+
All hooks enabled by default. Disable individual hooks via `experimental.hooks` in opencode.json:
|
|
259
|
+
```json
|
|
260
|
+
{
|
|
261
|
+
"experimental": {
|
|
262
|
+
"hooks": {
|
|
263
|
+
"enabled": true,
|
|
264
|
+
"plan_check": false,
|
|
265
|
+
"memory_sync": false
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Adding Custom Hooks
|
|
272
|
+
|
|
273
|
+
1. Create a hook implementing one of the four hook interfaces
|
|
274
|
+
2. Import `HookRegistry` from `openhermes/harness/lib/hooks`
|
|
275
|
+
3. Register via `HookRegistry.getInstance().registerPreTool(myHook)`
|
|
276
|
+
4. Hooks are topologically sorted by phase, priority, and dependencies
|
|
277
|
+
|
|
170
278
|
## User Skills
|
|
171
279
|
|
|
172
280
|
Skills in `~/.agents/skills/` and `~/.config/opencode/skills/` auto-discover on every session. On name conflict with built-in `oh-*` skill, user version wins. User skills survive `npm update openhermes`.
|
|
173
281
|
|
|
174
|
-
**User skills in the routing loop:**
|
|
175
|
-
- Appear in available skills list, loadable via skill tool on demand
|
|
176
|
-
- Their `route:` frontmatter drives routing identically to built-in skills
|
|
177
|
-
- Any skill can route to a user skill
|
|
178
|
-
- No registration step — add `route:` frontmatter and it participates automatically
|
|
282
|
+
**User skills in the routing loop:**
|
|
283
|
+
- Appear in available skills list, loadable via skill tool on demand
|
|
284
|
+
- Their `route:` frontmatter drives routing identically to built-in skills
|
|
285
|
+
- Any skill can route to a user skill when the route target matches an installed user skill name
|
|
286
|
+
- No registration step — add `route:` frontmatter and it participates automatically
|
package/harness/codex/CHARTER.md
CHANGED
|
@@ -24,7 +24,7 @@ Non-negotiable operating core. All skills, commands, and agents follow these pri
|
|
|
24
24
|
|
|
25
25
|
8. **Rules over hidden state** — Prefer AGENTS.md, instructions, and manifests over implicit state.
|
|
26
26
|
|
|
27
|
-
9. **Memory
|
|
27
|
+
9. **Memory implemented** — 4-tier hierarchical memory with importance scoring, budget enforcement, and plan-file persistence via MemoryManager + PlanStore.
|
|
28
28
|
|
|
29
29
|
10. **Closed-loop autonomy** — Auto-classify, auto-route after every skill. Only stop for blockers and major decisions.
|
|
30
30
|
|
|
@@ -46,7 +46,7 @@ User config, plugins, MCP, permissions, TUI, local skills, overlays — locked u
|
|
|
46
46
|
- **T0**: Check confidence → auto-classify → auto-route → execute
|
|
47
47
|
- **T1**: Check result → route next by outcome
|
|
48
48
|
- **T2**: If blocked → diagnose → retry with narrower scope
|
|
49
|
-
- **T3**: If still blocked → surface
|
|
49
|
+
- **T3**: If still blocked → surface findings, options, and what is needed
|
|
50
50
|
|
|
51
51
|
## Self-Diagnosis
|
|
52
52
|
|
|
@@ -63,7 +63,7 @@ Detect shell before spawning subagents. PowerShell (`powershell`/`pwsh`), CMD (`
|
|
|
63
63
|
|
|
64
64
|
## Plan Lifecycle
|
|
65
65
|
|
|
66
|
-
Plans at `~/.local/share/
|
|
66
|
+
Plans at `~/.local/share/openhermes/plans/<project-name>/plan-<nnn>.md`.
|
|
67
67
|
- **Keep**: `active`, `in-progress`, `blocked`
|
|
68
68
|
- **Delete**: `complete`, `abandoned`
|
|
69
69
|
- Cleanup is direct filesystem operation — AI knows project name, derives path, keeps by status. Surface summary only.
|
|
@@ -77,5 +77,4 @@ Plans at `~/.local/share/opencode/openhermes/plans/<project-name>-plan-<nnn>.md`
|
|
|
77
77
|
|
|
78
78
|
## Shared State
|
|
79
79
|
|
|
80
|
-
- **Plans**: `~/.local/share/
|
|
81
|
-
- **Instincts**: `~/.local/share/opencode/openhermes/plans/<project-name>-instincts.jsonl`
|
|
80
|
+
- **Plans**: `~/.local/share/openhermes/plans/<project-name>/plan-<nnn>.md`
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, it, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { BackgroundManager } from "./manager.ts";
|
|
4
|
+
import type { BackgroundTaskStatus } from "./interfaces.ts";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
function delay(ms: number): Promise<void> {
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Poll check() until the task reaches one of the given statuses, or until
|
|
16
|
+
* a generous timeout elapses (2.5 s).
|
|
17
|
+
*/
|
|
18
|
+
async function waitForStatus(
|
|
19
|
+
manager: BackgroundManager,
|
|
20
|
+
id: string,
|
|
21
|
+
...expected: BackgroundTaskStatus[]
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
for (let i = 0; i < 50; i++) {
|
|
24
|
+
const task = manager.check(id);
|
|
25
|
+
if (task && expected.includes(task.status)) return;
|
|
26
|
+
await delay(50);
|
|
27
|
+
}
|
|
28
|
+
const task = manager.check(id);
|
|
29
|
+
const actual = task?.status ?? "(not found)";
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Timed out waiting for status [${expected.join("/")}], got "${actual}"`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Windows detection — some assertions differ per platform
|
|
36
|
+
const IS_WIN = process.platform === "win32";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Tests
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
describe("BackgroundManager", () => {
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
BackgroundManager.resetInstance();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ---- 1: run() returns ID immediately ----------------------------------
|
|
48
|
+
|
|
49
|
+
it("run() returns a task ID immediately", () => {
|
|
50
|
+
const mgr = BackgroundManager.getInstance();
|
|
51
|
+
const id = mgr.run({ command: IS_WIN ? "echo" : "echo", args: ["hello"] });
|
|
52
|
+
assert.ok(typeof id === "string");
|
|
53
|
+
assert.ok(id.length > 0, "id must not be empty");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ---- 2: check() shows pending → running → completed -------------------
|
|
57
|
+
|
|
58
|
+
it("check() transitions pending -> running -> completed", async () => {
|
|
59
|
+
const mgr = BackgroundManager.getInstance();
|
|
60
|
+
const id = mgr.run({ command: IS_WIN ? "echo" : "echo", args: ["hello"] });
|
|
61
|
+
|
|
62
|
+
// Immediately after run() the task should be "pending"
|
|
63
|
+
// (spawn is deferred via setImmediate)
|
|
64
|
+
const initial = mgr.check(id);
|
|
65
|
+
assert.ok(initial, "task must exist immediately");
|
|
66
|
+
assert.equal(initial!.status, "pending");
|
|
67
|
+
|
|
68
|
+
// Wait for it to complete
|
|
69
|
+
await waitForStatus(mgr, id, "completed");
|
|
70
|
+
const done = mgr.check(id);
|
|
71
|
+
assert.equal(done!.exitCode, 0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("resetInstance returns a fresh manager with cleared state", async () => {
|
|
75
|
+
const mgr = BackgroundManager.getInstance();
|
|
76
|
+
const id = mgr.run({
|
|
77
|
+
command: IS_WIN ? "powershell.exe" : "sleep",
|
|
78
|
+
args: IS_WIN
|
|
79
|
+
? ["-NoProfile", "-Command", "Start-Sleep -Seconds 30"]
|
|
80
|
+
: ["30"],
|
|
81
|
+
timeout: 0,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await waitForStatus(mgr, id, "running");
|
|
85
|
+
|
|
86
|
+
BackgroundManager.resetInstance();
|
|
87
|
+
|
|
88
|
+
const fresh = BackgroundManager.getInstance();
|
|
89
|
+
assert.notEqual(fresh, mgr);
|
|
90
|
+
assert.equal(fresh.list().length, 0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ---- 3: capture stdout -------------------------------------------------
|
|
94
|
+
|
|
95
|
+
it("captures stdout from a simple command", async () => {
|
|
96
|
+
const mgr = BackgroundManager.getInstance();
|
|
97
|
+
const id = mgr.run({
|
|
98
|
+
command: IS_WIN ? "echo" : "echo",
|
|
99
|
+
args: ["hello-background"],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await waitForStatus(mgr, id, "completed");
|
|
103
|
+
const task = mgr.check(id);
|
|
104
|
+
assert.ok(task, "task must exist");
|
|
105
|
+
assert.match(task!.output, /hello-background/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ---- 4: failed command (non-zero exit) ---------------------------------
|
|
109
|
+
|
|
110
|
+
it("detects a failed command (non-zero exit)", async () => {
|
|
111
|
+
const mgr = BackgroundManager.getInstance();
|
|
112
|
+
const id = mgr.run({
|
|
113
|
+
command: IS_WIN ? "cmd.exe" : "bash",
|
|
114
|
+
args: IS_WIN ? ["/c", "exit", "1"] : ["-c", "exit 1"],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await waitForStatus(mgr, id, "failed");
|
|
118
|
+
const task = mgr.check(id);
|
|
119
|
+
assert.ok(task);
|
|
120
|
+
assert.equal(task!.exitCode, 1);
|
|
121
|
+
assert.equal(task!.status, "failed");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ---- 5: timeout enforcement --------------------------------------------
|
|
125
|
+
|
|
126
|
+
it("enforces timeout and marks task as timed_out", async () => {
|
|
127
|
+
const mgr = BackgroundManager.getInstance();
|
|
128
|
+
|
|
129
|
+
// Use a long-running command with a very short timeout (100 ms)
|
|
130
|
+
const id = mgr.run({
|
|
131
|
+
command: IS_WIN ? "powershell.exe" : "sleep",
|
|
132
|
+
args: IS_WIN
|
|
133
|
+
? ["-NoProfile", "-Command", "Start-Sleep -Seconds 30"]
|
|
134
|
+
: ["30"],
|
|
135
|
+
timeout: 100,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await waitForStatus(mgr, id, "timed_out");
|
|
139
|
+
const task = mgr.check(id);
|
|
140
|
+
assert.ok(task);
|
|
141
|
+
assert.equal(task!.status, "timed_out");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ---- 6: kill() marks as cancelled --------------------------------------
|
|
145
|
+
|
|
146
|
+
it("kill() marks a running task as cancelled", async () => {
|
|
147
|
+
const mgr = BackgroundManager.getInstance();
|
|
148
|
+
|
|
149
|
+
const id = mgr.run({
|
|
150
|
+
command: IS_WIN ? "powershell.exe" : "sleep",
|
|
151
|
+
args: IS_WIN
|
|
152
|
+
? ["-NoProfile", "-Command", "Start-Sleep -Seconds 30"]
|
|
153
|
+
: ["30"],
|
|
154
|
+
timeout: 0, // no timeout
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Wait for the task to enter "running"
|
|
158
|
+
await waitForStatus(mgr, id, "running");
|
|
159
|
+
|
|
160
|
+
// Kill it
|
|
161
|
+
const killed = mgr.kill(id);
|
|
162
|
+
assert.ok(killed, "kill() must return true");
|
|
163
|
+
|
|
164
|
+
const task = mgr.check(id);
|
|
165
|
+
assert.ok(task);
|
|
166
|
+
assert.equal(task!.status, "cancelled");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ---- 7: list() returns all tasks ---------------------------------------
|
|
170
|
+
|
|
171
|
+
it("list() returns all tracked tasks", async () => {
|
|
172
|
+
const mgr = BackgroundManager.getInstance();
|
|
173
|
+
const id1 = mgr.run({ command: IS_WIN ? "echo" : "echo", args: ["a"] });
|
|
174
|
+
const id2 = mgr.run({ command: IS_WIN ? "echo" : "echo", args: ["b"] });
|
|
175
|
+
|
|
176
|
+
const tasks = mgr.list();
|
|
177
|
+
const ids = tasks.map((t) => t.id);
|
|
178
|
+
assert.ok(ids.includes(id1), "list must contain first task");
|
|
179
|
+
assert.ok(ids.includes(id2), "list must contain second task");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ---- 8: kill() on already-terminal task returns false ------------------
|
|
183
|
+
|
|
184
|
+
it("kill() returns false for already-completed task", async () => {
|
|
185
|
+
const mgr = BackgroundManager.getInstance();
|
|
186
|
+
const id = mgr.run({ command: IS_WIN ? "echo" : "echo", args: ["quick"] });
|
|
187
|
+
|
|
188
|
+
await waitForStatus(mgr, id, "completed");
|
|
189
|
+
const result = mgr.kill(id);
|
|
190
|
+
assert.equal(result, false, "kill() must return false on complete task");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ---- 9: check() returns undefined for unknown ID -----------------------
|
|
194
|
+
|
|
195
|
+
it("check() returns undefined for unknown task ID", () => {
|
|
196
|
+
const mgr = BackgroundManager.getInstance();
|
|
197
|
+
const result = mgr.check("nonexistent-id");
|
|
198
|
+
assert.equal(result, undefined);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ---- 10: error output captured on command-not-found --------------------
|
|
202
|
+
|
|
203
|
+
it("captures error output when command does not exist", async () => {
|
|
204
|
+
const mgr = BackgroundManager.getInstance();
|
|
205
|
+
const id = mgr.run({ command: "this-command-does-not-exist-hopefully" });
|
|
206
|
+
|
|
207
|
+
await waitForStatus(mgr, id, "failed");
|
|
208
|
+
const task = mgr.check(id);
|
|
209
|
+
assert.ok(task);
|
|
210
|
+
// On Windows cmd.exe will emit an error; on Unix spawn error will fire
|
|
211
|
+
assert.ok(
|
|
212
|
+
task!.errorOutput.length > 0 || task!.output.length > 0,
|
|
213
|
+
"should have some error output",
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface BackgroundTask {
|
|
2
|
+
id: string;
|
|
3
|
+
command: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
cwd: string;
|
|
6
|
+
status: BackgroundTaskStatus;
|
|
7
|
+
output: string;
|
|
8
|
+
errorOutput: string;
|
|
9
|
+
exitCode: number | null;
|
|
10
|
+
startTime: number;
|
|
11
|
+
endTime: number | null;
|
|
12
|
+
timeout: number; // ms, 0 = no timeout
|
|
13
|
+
label?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type BackgroundTaskStatus =
|
|
17
|
+
| "pending"
|
|
18
|
+
| "running"
|
|
19
|
+
| "completed"
|
|
20
|
+
| "failed"
|
|
21
|
+
| "timed_out"
|
|
22
|
+
| "cancelled";
|
|
23
|
+
|
|
24
|
+
export interface BackgroundRunOptions {
|
|
25
|
+
command: string;
|
|
26
|
+
args?: string[];
|
|
27
|
+
cwd?: string;
|
|
28
|
+
timeout?: number; // ms, default 30000
|
|
29
|
+
label?: string;
|
|
30
|
+
env?: Record<string, string>;
|
|
31
|
+
}
|