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.
Files changed (85) hide show
  1. package/CONTEXT.md +7 -7
  2. package/ETHOS.md +2 -2
  3. package/README.md +34 -33
  4. package/bootstrap.ts +310 -160
  5. package/harness/agents/oh-planner.md +1 -1
  6. package/harness/agents/openhermes.md +27 -126
  7. package/harness/codex/AUTOPILOT.md +131 -23
  8. package/harness/codex/CHARTER.md +4 -5
  9. package/harness/lib/background/background.test.ts +216 -0
  10. package/harness/lib/background/index.ts +7 -0
  11. package/harness/lib/background/interfaces.ts +31 -0
  12. package/harness/lib/background/manager.ts +320 -0
  13. package/harness/lib/composer/compose.test.ts +179 -0
  14. package/harness/lib/composer/compose.ts +65 -0
  15. package/harness/lib/composer/fragments/01-identity.md +1 -0
  16. package/harness/lib/composer/fragments/02-delegation.md +7 -0
  17. package/harness/lib/composer/fragments/03-permissions.md +13 -0
  18. package/harness/lib/composer/fragments/04-task-flow.md +55 -0
  19. package/harness/lib/composer/fragments/05-confidence.md +5 -0
  20. package/harness/lib/composer/fragments/06-parallelization.md +17 -0
  21. package/harness/lib/composer/fragments/07-shell.md +41 -0
  22. package/harness/lib/composer/fragments/08-routing.md +8 -0
  23. package/harness/lib/composer/fragments/09-guardrails.md +25 -0
  24. package/harness/lib/composer/index.ts +1 -0
  25. package/harness/lib/guards/guard-config.ts +72 -0
  26. package/harness/lib/hooks/builtins/confidence-gate-hook.ts +68 -0
  27. package/harness/lib/hooks/builtins/delegation-depth-hook.ts +78 -0
  28. package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -0
  29. package/harness/lib/hooks/builtins/error-recovery-hook.ts +107 -0
  30. package/harness/lib/hooks/builtins/memory-sync-hook.ts +73 -0
  31. package/harness/lib/hooks/builtins/next-route-hook.ts +24 -0
  32. package/harness/lib/hooks/builtins/plan-check-hook.ts +43 -0
  33. package/harness/lib/hooks/builtins/route-tracking-hook.ts +201 -0
  34. package/harness/lib/hooks/builtins/sanity-check-hook.ts +52 -0
  35. package/harness/lib/hooks/builtins/shell-detect-hook.ts +96 -0
  36. package/harness/lib/hooks/builtins/subagent-failure-hook.ts +93 -0
  37. package/harness/lib/hooks/hooks.test.ts +1092 -0
  38. package/harness/lib/hooks/index.ts +42 -0
  39. package/harness/lib/hooks/registry.ts +416 -0
  40. package/harness/lib/hooks/types.ts +119 -0
  41. package/harness/lib/memory/index.ts +18 -0
  42. package/harness/lib/memory/interfaces.ts +53 -0
  43. package/harness/lib/memory/memory-manager.ts +205 -0
  44. package/harness/lib/memory/memory.test.ts +485 -0
  45. package/harness/lib/memory/plan-store.ts +346 -0
  46. package/harness/lib/plans/plan-location.ts +134 -0
  47. package/harness/lib/recovery/handler.ts +243 -0
  48. package/harness/lib/recovery/index.ts +14 -0
  49. package/harness/lib/recovery/interfaces.ts +48 -0
  50. package/harness/lib/recovery/patterns.ts +149 -0
  51. package/harness/lib/recovery/recovery.test.ts +312 -0
  52. package/harness/lib/routing/index.ts +21 -0
  53. package/harness/lib/routing/route-guidance.ts +147 -0
  54. package/harness/lib/routing/route-resolver.ts +58 -0
  55. package/harness/lib/routing/routing.test.ts +195 -0
  56. package/harness/lib/routing/skill-frontmatter.ts +125 -0
  57. package/harness/lib/routing/types.ts +52 -0
  58. package/harness/lib/sanity/anomaly-tracker.ts +127 -0
  59. package/harness/lib/sanity/checker.ts +189 -0
  60. package/harness/lib/sanity/index.ts +13 -0
  61. package/harness/lib/sanity/interfaces.ts +24 -0
  62. package/harness/lib/sanity/sanity.test.ts +472 -0
  63. package/harness/lib/sync/file-watcher.ts +175 -0
  64. package/harness/lib/sync/index.ts +11 -0
  65. package/harness/lib/sync/interfaces.ts +27 -0
  66. package/harness/lib/sync/plan-sync.ts +533 -0
  67. package/harness/lib/sync/sync.test.ts +858 -0
  68. package/harness/skills/oh-fusion/DEEP.md +109 -86
  69. package/harness/skills/oh-fusion/SKILL.md +47 -33
  70. package/harness/skills/oh-init/DEEP.md +2 -2
  71. package/harness/skills/oh-manifest/SKILL.md +2 -1
  72. package/harness/skills/oh-plan-review/DEEP.md +1 -1
  73. package/harness/skills/oh-planner/DEEP.md +3 -3
  74. package/harness/skills/oh-review/DEEP.md +5 -3
  75. package/harness/skills/oh-review/SKILL.md +1 -0
  76. package/harness/skills/oh-ship/SKILL.md +1 -1
  77. package/harness/skills/oh-skill-craft/SKILL.md +1 -4
  78. package/package.json +53 -55
  79. package/tsconfig.json +1 -1
  80. package/harness/commands/oh-doctor.md +0 -205
  81. package/harness/commands/oh-log.md +0 -18
  82. package/harness/skills/oh-learn/DEEP.md +0 -44
  83. package/harness/skills/oh-learn/SKILL.md +0 -30
  84. package/scripts/count-tokens.mjs +0 -158
  85. 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
- You are OpenHermes, an OpenCode-native orchestrator: pragmatic, task-focused, concise.
7
-
8
- ## Core Behaviors
9
-
10
- 1. **Enforced delegation.** OpenHermes CANNOT write code, run commands, or edit files (bash=deny, edit=deny). ALL execution happens through sub-agents spawned via the task tool.
11
- 2. **Load skills on demand.** Use the `skill()` tool when a task matches a skill description.
12
- 3. **Verify before claim.** Read files, run commands, confirm output before stating completion.
13
- 4. **Default voice is situational.** Be direct for clear requests. Use brief conversational framing for ambiguous ones. Concise by default, conversational when calibrating. Always bounded to 1 exchange. Even HIGH confidence inputs get a quick injection scan — if instruction tokens are detected, escalate to MEDIUM before delegating.
14
-
15
- ## Permissions
16
-
17
- These are MECHANICAL, not instructional. OpenCode enforces them.
18
-
19
- - `bash`: DENIED cannot execute shell commands
20
- - `edit`: DENIED cannot write or modify files
21
- - `read`: ALLOWED can inspect files for classification
22
- - `glob/grep`: ALLOWED can search for files and content
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/opencode/openhermes/plans/<project-name>-plan-<nnn>.md`:
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. Read the skill's `route:` frontmatter (`route.pass`, `route.fail`, `route.blocker`)
105
- 3. Route immediately by outcome do not ask
106
- 4. Repeat until blocker, completion (`done`), or surface (`surface`)
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
- | `mode` | Mode switch — return to caller after toggle |
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. The only true terminal is `oh-handoff`.
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
- If the same skill is visited 5+ times in one chain, or 8+ hops pass without producing a new artifact STOP. Write OptiRoute report to plan file (routing chain, trigger, current state, blocker). Surface to user. Do not keep looping.
156
+ ### Loop Guard (Mechanical)
157
+ Enforced by the `route-tracking`, `delegation-depth`, and `subagent-failure` hooksno 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 (built-in `route.pass` pointing to `oh-deploy` routes there)
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
@@ -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 deferred** — Intentional absence for this pass.
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 with findings, options, what is needed
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/opencode/openhermes/plans/<project-name>-plan-<nnn>.md`.
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/opencode/openhermes/plans/<project-name>-plan-<nnn>.md`
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,7 @@
1
+ export type {
2
+ BackgroundTask,
3
+ BackgroundTaskStatus,
4
+ BackgroundRunOptions,
5
+ } from "./interfaces.ts";
6
+
7
+ export { BackgroundManager } from "./manager.ts";
@@ -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
+ }