openhermes 1.5.2 → 1.5.6

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 CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  ---
14
14
 
15
- **Your OpenCode agent, leveled up.** Add it to your plugins your agent gains a personality, a memory, a conscience, 7 specialist subagents, 7 slash commands, 5 native memory tools, 10 procedural skills, and the discipline to self-improve.
15
+ **Your OpenCode agent, leveled up.** Add it to your plugins and your agent gains a personality, a memory, a conscience, 7 specialist subagents, 7 slash commands, 5 native memory tools, 10 procedural skills, autonomous checkpointing, and the discipline to self-improve.
16
16
 
17
17
  ```bash
18
18
  npm i openhermes
@@ -29,21 +29,25 @@ npm i openhermes
29
29
  ## What OpenHermes Does For Your Agent
30
30
 
31
31
  <table>
32
- <tr><td width="160"><b>&#129302; Personality Layer</b></td><td>A 11-principle constitution (soul.md) injected into every session — pragmatic, concise, subagent-first, verify-don't-claim. Your agent stops rambling and starts delivering.</td></tr>
32
+ <tr><td width="160"><b>&#129302; Personality Layer</b></td><td>An 11-principle constitution (<code>soul.md</code>) injected into every session — pragmatic, concise, subagent-first, verify-don't-claim. Your agent stops rambling and starts delivering.</td></tr>
33
33
  <tr><td><b>&#128204; Delegation Discipline</b></td><td>Mandated routing table — every non-trivial task goes to the right specialist subagent. Main context stays clean, coordination-only. No more bloated chat logs.</td></tr>
34
- <tr><td><b>&#128190; 9-Class Durable Memory</b></td><td>Checkpoints, decisions, constraints, instincts, mistakes, backlog items, audit reports, verification receipts, and session recall — all schema-validated, fingerprint-verified, persisted to disk.</td></tr>
34
+ <tr><td><b>&#128190; 9-Class Durable Memory</b></td><td>Checkpoints, decisions, constraints, instincts, mistakes, backlog items, audit reports, verification receipts, and session recall — all schema-validated, fingerprint-aware, persisted to disk.</td></tr>
35
35
  <tr><td><b>&#129520; Precision-First Retrieval</b></td><td>Gated retrieval with anti-spam controls. <code>hm_latest</code> → <code>hm_search</code> → <code>hm_get</code> → <code>hm_list</code>. No full-index reads unless explicitly scoped. Memory stays lean.</td></tr>
36
- <tr><td><b>&#128293; Autonomous Checkpointing</b></td><td>Pre-compaction snapshots capture mission, current state, next actions, active decisions, blockers, and risk notes so compaction never loses the plot.</td></tr>
36
+ <tr><td><b>&#128293; Autonomous Checkpointing</b></td><td>Pre-compaction snapshots capture mission, current state, next actions, blockers, and risk notes so compaction never loses the plot.</td></tr>
37
37
  <tr><td><b>&#128260; Closed Learning Loop</b></td><td>Mistakes are logged with root cause + prevention rule. Complex sessions auto-generate skill-candidate backlogs. Strike tracking escalates repeat failures. The agent gets better — you don't have to teach it twice.</td></tr>
38
- <tr><td><b>&#128736; 10 Bundled Procedural Skills</b></td><td>Pre-built skills for API design, backend patterns, coding standards, E2E testing, frontend patterns, security reviews, strategic compaction, TDD workflow, verification loops, and presentation building. Discovered automatically — use <code>skill</code> tool to list and load.</td></tr>
39
- <tr><td><b>&#129513; Zero Infrastructure</b></td><td>No Python. No uv. No Docker. No PostgreSQL. No gateway. No cron daemon. Just Node.js which OpenCode's Bun runtime already bundles. Everything runs inside your existing OpenCode session.</td></tr>
38
+ <tr><td><b>&#128736; 10 Bundled Procedural Skills</b></td><td>Pre-built skills for API design, backend patterns, coding standards, E2E testing, frontend patterns, frontend slides, security reviews, strategic compaction, TDD workflow, and verification loops. Discovered automatically — use <code>skill</code> to list and load.</td></tr>
39
+ <tr><td><b>&#129513; Zero Infrastructure</b></td><td>No Python. No uv. No Docker. No PostgreSQL. No gateway. No cron daemon. Just Node.js and your existing OpenCode runtime.</td></tr>
40
40
  </table>
41
41
 
42
42
  ---
43
43
 
44
44
  ## Setup
45
45
 
46
- Add one line to your `opencode.json`:
46
+ Add one line to your `opencode.json`. Two good options:
47
+
48
+ ### Published Release
49
+
50
+ Use npm when you want a stable release boundary.
47
51
 
48
52
  ```json
49
53
  {
@@ -51,7 +55,17 @@ Add one line to your `opencode.json`:
51
55
  }
52
56
  ```
53
57
 
54
- That's it. **No other config needed.** The plugin auto-registers:
58
+ ### Git-Backed
59
+
60
+ Use GitHub when you want the latest pushed changes immediately.
61
+
62
+ ```json
63
+ {
64
+ "plugin": ["openhermes@git+https://github.com/nathwn12/openhermes.git"]
65
+ }
66
+ ```
67
+
68
+ Either way, **no other config needed.** The plugin auto-registers:
55
69
 
56
70
  | What | Details |
57
71
  |------|---------|
@@ -67,17 +81,17 @@ You only need to define primary agents (like `build` or `OpenHermes`) in `openco
67
81
  <summary><b>What happens on your next session</b></summary>
68
82
 
69
83
  1. **Config hook** — BootstrapPlugin registers auto-config: 7 subagents, 7 commands, 10 skill dirs.
70
- 2. **Chat transform hook** — ~12KB of context injected into the first user message:
71
- - &#9733; **Constitution** (soul.md) — 11 immutable principles
72
- - &#9733; **Runtime** (RUNTIME.md) — gather → delegate → verify → compress
73
- - &#9733; **Router** (AGENTS.md) — delegation table, memory policy, escalation, with absolute paths to every rule
74
- 3. **Session created** — AutorecallPlugin builds recall cache from prior session memory
75
- 4. **Tools execute** — SkillBuilderPlugin watches tool calls and subagent spawns; MemoryToolsPlugin provides 5 native tools immediately
76
- 5. **Session idle** — CuratorPlugin snapshots checkpoint + verification receipt
77
- 6. **Session error** — CuratorPlugin logs mistake with root cause + prevention rule
78
- 7. **Compaction** — CuratorPlugin force-writes pre-compaction checkpoint, injects state into buffer
79
-
80
- The LLM reads rules on demand via the injected paths. Memory directories auto-create. Everything Just Works™.
84
+ 2. **Chat transform hook** — bootstrap content is injected into the first user message:
85
+ - &#9733; **Constitution** (`soul.md`) — 11 immutable principles
86
+ - &#9733; **Runtime** (`RUNTIME.md`) — gather → delegate → verify → compress
87
+ - &#9733; **Router** (`AGENTS.md`) — delegation table, memory policy, escalation, and rule paths
88
+ 3. **Session created** — AutorecallPlugin builds recall cache from prior session memory.
89
+ 4. **Tools execute** — SkillBuilderPlugin watches tool calls and subagent spawns; MemoryToolsPlugin provides 5 native tools immediately.
90
+ 5. **Session idle** — CuratorPlugin snapshots checkpoint + verification receipt.
91
+ 6. **Session error** — CuratorPlugin logs mistake with root cause + prevention rule.
92
+ 7. **Compaction** — CuratorPlugin force-writes a pre-compaction checkpoint and injects state into the compaction buffer.
93
+
94
+ The LLM reads rules on demand via the injected paths. Memory directories auto-create. Everything Just Works.
81
95
 
82
96
  </details>
83
97
 
@@ -91,13 +105,13 @@ The LLM reads rules on demand via the injected paths. Memory directories auto-cr
91
105
  | **MemoryToolsPlugin** | — | Registers 5 native tools: `hm_put`, `hm_get`, `hm_list`, `hm_latest`, `hm_search`. Runs in-process — no MCP server needed. |
92
106
  | **CuratorPlugin** | `session.idle`, `.compacted`, `.error`, `.compacting`, `permission.replied` | Writes checkpoints, logs mistakes, records audits, injects state into compaction. |
93
107
  | **AutorecallPlugin** | `session.created` | Loads memory from disk, builds session recall cache. |
94
- | **SkillBuilderPlugin** | `session.idle`, `.created`, `tool.execute.after` | Detects complex sessions (8+ tool calls or 2+ subagent spawns) creates skill-candidate backlogs. |
108
+ | **SkillBuilderPlugin** | `session.idle`, `.created`, `tool.execute.after` | Detects complex sessions (8+ tool calls or 2+ subagent spawns) and creates skill-candidate backlogs. |
95
109
 
96
110
  ---
97
111
 
98
112
  ## Memory Architecture
99
113
 
100
- Nine memory classes, all schema-validated before persistence, stored at `~/.local/share/opencode/openhermes/memory/`:
114
+ Nine memory classes, all schema-validated before persistence, stored under the standard OpenCode storage roots:
101
115
 
102
116
  | Class | Format | Purpose |
103
117
  |-------|--------|---------|
@@ -107,11 +121,11 @@ Nine memory classes, all schema-validated before persistence, stored at `~/.loca
107
121
  | `instinct` | JSON | Reusable trigger→action patterns with success/failure tracking |
108
122
  | `backlog` | JSON | Evidence-backed self-improvement items with acceptance criteria |
109
123
  | `mistake` | JSONL | Failure registry: type, root cause, fix, prevention rule, strike count |
110
- | `audit` | JSON | Structured quality/integrity evaluations with health scores |
124
+ | `audit` | JSON | Structured quality and integrity evaluations with health scores |
111
125
  | `verification_receipt` | JSON | Cached verification results keyed by artifact fingerprint |
112
126
  | `recall` | JSON | Session-start cache aggregating active state for compaction injection |
113
127
 
114
- OpenHermes follows the same storage contract as OpenCode itself — see [OpenCode docs on storage](https://opencode.ai/docs/troubleshooting/#storage):
128
+ OpenHermes follows the same storage contract as OpenCode itself:
115
129
 
116
130
  | What | Where |
117
131
  |---|---|
@@ -119,7 +133,7 @@ OpenHermes follows the same storage contract as OpenCode itself — see [OpenCod
119
133
  | Durable memory + runtime state | `~/.local/share/opencode/openhermes/` |
120
134
  | Derived recall cache | `~/.cache/opencode/openhermes/recall/` |
121
135
 
122
- **Runtime hardening**: All records pass through `sanitizeRecord()` (strips `__proto__`, `constructor`, `prototype`), `redactSensitiveText()` (strips bearer tokens, API keys, passwords), and `truncateText()` before persistence. Schema validation gate runs before every write.
136
+ **Runtime hardening**: records pass through `sanitizeRecord()` (strips `__proto__`, `constructor`, `prototype`), `redactSensitiveText()` (strips bearer tokens, API keys, passwords), and `truncateText()` before persistence. Schema validation runs before every write.
123
137
 
124
138
  ---
125
139
 
@@ -159,50 +173,36 @@ permission.replied
159
173
 
160
174
  ---
161
175
 
176
+ ## Verification Discipline
177
+
178
+ OpenHermes pushes the agent toward one core habit: **verify before claiming success**.
179
+
180
+ - read before editing
181
+ - run commands before announcing success
182
+ - cache verification receipts by artifact fingerprint
183
+ - re-verify when files change
184
+ - flag contradictions instead of smoothing over them
185
+
186
+ That is why verification receipts are a first-class memory type rather than an afterthought.
187
+
188
+ ---
189
+
162
190
  ## Bundled Harness
163
191
 
164
- The full OpenHermes framework ships inside the package — 60 files across 6 directories:
192
+ The full OpenHermes framework ships inside the package — prompts, rules, commands, constitution, and procedural skills included:
165
193
 
166
194
  ```
167
195
  harness/
168
196
  ├── constitution/soul.md # 11 immutable personality principles
169
- ├── instructions/RUNTIME.md # Session workflow: gather → delegate → verify
170
- ├── rules/ (16 files)
171
- ├── delegation.md # Mandatory subagent routing
172
- ├── retrieval.md # Gated precision-first memory retrieval
173
- │ ├── session-start.md # Session-start checklist and memory hydration
174
- │ ├── credential-exposure.md # Secret redaction and credential exposure guard
175
- │ ├── self-heal.md # T0→T3 escalation tiers
176
- │ ├── verification.md # Skeptical evidence protocol
177
- │ ├── memory-management.md # Dual-target memory + anti-spam
178
- │ ├── precedence.md # 9-level conflict resolution
179
- │ ├── checkpointing.md # Compaction snapshot discipline
180
- │ ├── audit.md # Structured health checks
181
- │ ├── skills-management.md # Progressive disclosure loading
182
- │ ├── context-loading.md # Priority chain + size limits
183
- │ ├── promotion.md # High-signal-only promotion
184
- │ ├── ranking.md # Metadata-first retrieval
185
- │ ├── runtime-guards.md # Stale assumption prevention
186
- │ └── state-drift.md # Hash-based fingerprinting
187
- ├── skills/ (10 directories)
188
- │ ├── api-design/SKILL.md # REST API patterns (523 lines)
189
- │ ├── backend-patterns/SKILL.md # Backend architecture (598 lines)
190
- │ ├── coding-standards/SKILL.md # Baseline conventions (549 lines)
191
- │ ├── e2e-testing/SKILL.md # Playwright E2E patterns (326 lines)
192
- │ ├── frontend-patterns/SKILL.md # React/Next.js patterns (642 lines)
193
- │ ├── frontend-slides/SKILL.md # HTML presentation builder (184 lines)
194
- │ ├── security-review/SKILL.md # OWASP Top 10 checklist (495 lines)
195
- │ ├── strategic-compact/SKILL.md # Context compaction strategy (131 lines)
196
- │ ├── tdd-workflow/SKILL.md # Red-green-refactor discipline (463 lines)
197
- │ └── verification-loop/SKILL.md # Pre-PR quality gates (126 lines)
198
- ├── prompts/ (7 files)
199
- │ # Subagent prompt templates: architect, build-error-resolver,
200
- │ # code-reviewer, e2e-runner, explore, planner, security-reviewer
201
- └── commands/ (7 files)
202
- # Slash command templates: build-fix, code-review, doctor,
203
- # learn, memory-search, plan, security
197
+ ├── instructions/RUNTIME.md # Session workflow: gather → delegate → verify
198
+ ├── rules/ # retrieval, verification, checkpointing, audit, self-heal, etc.
199
+ ├── skills/ # 10 bundled procedural skills
200
+ ├── prompts/ # subagent prompt templates
201
+ └── commands/ # slash command templates
204
202
  ```
205
203
 
204
+ This matters because OpenHermes is not just a runtime shim. The operational doctrine ships with it.
205
+
206
206
  ---
207
207
 
208
208
  ## Self-Healing Escalation
@@ -233,17 +233,17 @@ No self-termination. No grandstanding. Narrow, log, recover, improve.
233
233
 
234
234
  ```
235
235
  openhermes/
236
- ├── index.mjs # Re-exports all 5 plugins
237
- ├── bootstrap.mjs # Config hook (agents/commands/skills) + chat.transform
238
- ├── autorecall.mjs # Recall cache builder
239
- ├── curator.mjs # Lifecycle hooks engine (~470 lines)
240
- ├── skill-builder.mjs # Complexity detection engine
236
+ ├── index.mjs # Re-exports all 5 plugins
237
+ ├── bootstrap.mjs # Config hook (agents/commands/skills) + chat.transform
238
+ ├── autorecall.mjs # Recall cache builder
239
+ ├── curator.mjs # Lifecycle hooks engine
240
+ ├── skill-builder.mjs # Complexity detection engine
241
241
  ├── lib/
242
242
  │ ├── memory-tools-plugin.mjs # 5 native memory tools (hm_put/get/list/latest/search)
243
243
  │ ├── hardening.mjs # atomicWriteJson, fingerprint, sanitize, redact
244
244
  │ └── schema-validator.mjs # Draft-07 subset validator
245
- ├── schemas/ # 9 JSON schemas for memory validation
246
- ├── harness/ # Full framework (44 files)
245
+ ├── schemas/ # Memory schemas for validation
246
+ ├── harness/ # Full framework bundle
247
247
  └── package.json
248
248
  ```
249
249
 
@@ -251,13 +251,6 @@ openhermes/
251
251
 
252
252
  ---
253
253
 
254
- ## Dependencies
255
-
256
- - **Node.js >= 18** — `node:path`, `node:fs`, `node:os`, `node:url`, `node:crypto`
257
- - **OpenCode** — provides the Bun runtime, plugin loader, hook dispatcher, and `skill` tool
258
-
259
- ---
260
-
261
254
  ## Why OpenHermes ≠ Hermes Agent
262
255
 
263
256
  | | Hermes Agent | OpenHermes |
@@ -132,6 +132,18 @@ function enforceAuditEvidence(record) {
132
132
  return ["db_refs", "file_refs", "log_refs"].some(k => Array.isArray(prov[k]) && prov[k].some(i => typeof i === "string" && i.trim()))
133
133
  }
134
134
 
135
+ function setToolTitle(context, title, metadata = {}) {
136
+ if (!context || typeof context.metadata !== "function") return
137
+ context.metadata({ title, metadata })
138
+ }
139
+
140
+ function classifyPutTitle(cls, id) {
141
+ const label = String(id || "").trim() || "unnamed"
142
+ if (cls === "checkpoint") return `Save checkpoint: ${label}`
143
+ if (cls === "verification_receipt") return `Save verification receipt: ${label}`
144
+ return `Save ${cls}: ${label}`
145
+ }
146
+
135
147
  function handlePut(cls, id, dataStr) {
136
148
  let parsed
137
149
  try { parsed = JSON.parse(dataStr) } catch (e) { return `data must be valid JSON: ${e.message}` }
@@ -220,7 +232,10 @@ export const MemoryToolsPlugin = async () => {
220
232
  id: tool.schema.string(),
221
233
  data: tool.schema.string(),
222
234
  },
223
- async execute(args) { return handlePut(args.class, args.id, args.data) },
235
+ async execute(args, context) {
236
+ setToolTitle(context, classifyPutTitle(args.class, args.id), { action: "put", class: args.class, id: args.id })
237
+ return handlePut(args.class, args.id, args.data)
238
+ },
224
239
  }),
225
240
 
226
241
  hm_get: tool({
@@ -229,7 +244,10 @@ export const MemoryToolsPlugin = async () => {
229
244
  class: tool.schema.enum(CLASSES).describe("Memory class"),
230
245
  id: tool.schema.string().describe("Record ID"),
231
246
  },
232
- async execute(args) { return handleGet(args.class, args.id) },
247
+ async execute(args, context) {
248
+ setToolTitle(context, `Open ${args.class}: ${args.id}`, { action: "get", class: args.class, id: args.id })
249
+ return handleGet(args.class, args.id)
250
+ },
233
251
  }),
234
252
 
235
253
  hm_list: tool({
@@ -238,7 +256,10 @@ export const MemoryToolsPlugin = async () => {
238
256
  class: tool.schema.enum(CLASSES).describe("Memory class"),
239
257
  limit: tool.schema.number().optional().default(10).describe("Max results (max 100)"),
240
258
  },
241
- async execute(args) { return handleList(args.class, args.limit) },
259
+ async execute(args, context) {
260
+ setToolTitle(context, `List ${args.class} records`, { action: "list", class: args.class, limit: args.limit })
261
+ return handleList(args.class, args.limit)
262
+ },
242
263
  }),
243
264
 
244
265
  hm_latest: tool({
@@ -246,7 +267,10 @@ export const MemoryToolsPlugin = async () => {
246
267
  args: {
247
268
  class: tool.schema.enum(CLASSES).describe("Memory class"),
248
269
  },
249
- async execute(args) { return handleLatest(args.class) },
270
+ async execute(args, context) {
271
+ setToolTitle(context, `Latest ${args.class}`, { action: "latest", class: args.class })
272
+ return handleLatest(args.class)
273
+ },
250
274
  }),
251
275
 
252
276
  hm_search: tool({
@@ -258,8 +282,18 @@ export const MemoryToolsPlugin = async () => {
258
282
  project: tool.schema.string().optional().describe("Project filter"),
259
283
  limit: tool.schema.number().optional().default(10).describe("Max results (max 50)"),
260
284
  },
261
- async execute(args) { return handleSearch(args.query, args.scope, args.classes, args.project, args.limit) },
285
+ async execute(args, context) {
286
+ setToolTitle(context, `Search memory: ${truncateText(args.query, 48)}`, {
287
+ action: "search",
288
+ scope: args.scope,
289
+ classes: args.classes?.length ? args.classes : CLASSES,
290
+ project: args.project ?? null,
291
+ limit: args.limit,
292
+ })
293
+ return handleSearch(args.query, args.scope, args.classes, args.project, args.limit)
294
+ },
262
295
  }),
296
+
263
297
  },
264
298
  }
265
299
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openhermes",
3
- "version": "1.5.2",
3
+ "version": "1.5.6",
4
4
  "description": "OpenHermes plugin suite for OpenCode — autonomous checkpointing, native memory tools, subagent routing, slash commands, and skill-candidate detection.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,230 +0,0 @@
1
- import fs from "fs"
2
- import path from "path"
3
- import os from "os"
4
- import { getMemoryRoot, getDataRoot } from "../../lib/paths.mjs"
5
-
6
- const MEMORY_DIR = getMemoryRoot()
7
-
8
- const CLASSES = ["audit", "checkpoint", "mistake", "instinct", "decision", "constraint", "backlog", "verification_receipt"]
9
- const PLURALS = {
10
- audit: "audits", checkpoint: "checkpoints", mistake: "mistakes",
11
- instinct: "instincts", decision: "decisions", constraint: "constraints",
12
- backlog: "backlog", verification_receipt: "verification_receipts",
13
- }
14
-
15
- function isPlainObject(v) { return !!v && typeof v === "object" && !Array.isArray(v) }
16
-
17
- function stableStringify(v, space = 0) {
18
- function sort(o) {
19
- if (Array.isArray(o)) return o.map(sort)
20
- if (!isPlainObject(o)) return o
21
- const r = {}
22
- for (const k of Object.keys(o).sort()) r[k] = sort(o[k])
23
- return r
24
- }
25
- return JSON.stringify(sort(v), null, space)
26
- }
27
-
28
- function classDir(cls) { return path.join(MEMORY_DIR, PLURALS[cls]) }
29
-
30
- function atomicWriteJson(fp, data) {
31
- const tmp = fp + ".tmp"
32
- fs.writeFileSync(tmp, stableStringify(data, 2), "utf8")
33
- fs.renameSync(tmp, fp)
34
- }
35
-
36
- function readJSON(fp, fallback) {
37
- try { return JSON.parse(fs.readFileSync(fp, "utf8")) } catch { return fallback }
38
- }
39
-
40
- function readJSONL(fp) {
41
- try {
42
- return fs.readFileSync(fp, "utf8").split(/\r?\n/).map(l => l.trim()).filter(Boolean).map(l => JSON.parse(l))
43
- } catch { return [] }
44
- }
45
-
46
- function buildEntry(cls, r) {
47
- const e = {
48
- id: r.id, summary: r.summary, status: r.status,
49
- updated_at: r.updated_at ?? r.created_at,
50
- path: path.join("openhermes", "memory", PLURALS[cls], `${r.id}.json`),
51
- scope: r.scope ?? null, project: r.project ?? null,
52
- }
53
- if (cls === "audit") { e.target = r.target; e.overall_score = r.overall_score }
54
- if (cls === "backlog") { e.priority = r.priority; e.trigger = r.trigger }
55
- return e
56
- }
57
-
58
- function hasExpired(r) {
59
- if (r?.status === "expired" || r?.status === "decayed") return true
60
- if (r?.decay_at && Date.parse(r.decay_at) < Date.now()) return true
61
- if (r?.expires_at && Date.parse(r.expires_at) < Date.now()) return true
62
- return false
63
- }
64
-
65
- function sortRecent(entries) {
66
- function ts(e) { return e?.updated_at ?? e?.created_at ?? "" }
67
- return [...entries].sort((a, b) => {
68
- const at = Date.parse(ts(a)), bt = Date.parse(ts(b))
69
- if (!Number.isNaN(at) && !Number.isNaN(bt) && at !== bt) return bt - at
70
- return String(ts(b)).localeCompare(String(ts(a)))
71
- })
72
- }
73
-
74
- function filterActive(entries) { return entries.filter(e => !hasExpired(e)) }
75
-
76
- function validateSchemaSimple(record) {
77
- const required = {
78
- audit: ["id", "class", "summary", "target", "overall_score", "checks"],
79
- checkpoint: ["id", "class", "summary", "mission", "current_state", "provenance"],
80
- mistake: ["id", "class", "summary", "failure", "root_cause", "type", "strike"],
81
- instinct: ["id", "class", "summary", "claim", "provenance"],
82
- decision: ["id", "class", "summary", "decision", "rationale", "provenance"],
83
- constraint: ["id", "class", "summary", "constraint", "provenance"],
84
- backlog: ["id", "class", "summary", "priority"],
85
- verification_receipt: ["id", "class", "summary", "fingerprint", "method", "result"],
86
- }
87
- const cls = record.class
88
- const reqs = required[cls]
89
- if (!reqs) return { ok: true }
90
- const missing = reqs.filter(r => !record[r] && record[r] !== null && record[r] !== undefined)
91
- if (missing.length) return { ok: false, errors: missing.map(m => `$.${m} is required`) }
92
- return { ok: true }
93
- }
94
-
95
- function queryList(cls, limit = 10) {
96
- if (cls === "mistake") {
97
- return sortRecent(filterActive(readJSONL(path.join(classDir(cls), "mistakes.jsonl")))).slice(0, limit)
98
- }
99
- const dir = classDir(cls)
100
- let files = []
101
- try { files = fs.readdirSync(dir).filter(f => f.endsWith(".json") && f !== "index.json").map(f => path.join(dir, f)) } catch { return [] }
102
- const entries = files.map(f => readJSON(f, null)).filter(Boolean).map(r => buildEntry(cls, r))
103
- return sortRecent(filterActive(entries)).slice(0, limit)
104
- }
105
-
106
- function queryGet(cls, id) {
107
- if (cls === "mistake") return readJSONL(path.join(classDir(cls), "mistakes.jsonl")).find(e => e?.id === id) ?? null
108
- return readJSON(path.join(classDir(cls), `${id}.json`), null)
109
- }
110
-
111
- function scoreRelevance(r, query, project) {
112
- const q = query.toLowerCase()
113
- let score = 0
114
- const fields = [r.summary, r.id, r.description, r.mission, r.current_state, r.failure, r.root_cause, r.fix, r.prevention, r.command, r.project, r.scope, ...(Array.isArray(r.tags) ? r.tags : []), ...(Array.isArray(r.next_actions) ? r.next_actions : []), ...(Array.isArray(r.refs) ? r.refs : [])].filter(Boolean)
115
- for (const f of fields) {
116
- const str = String(f).toLowerCase()
117
- let idx = 0; let count = 0
118
- while ((idx = str.indexOf(q, idx)) !== -1) { count++; idx += q.length }
119
- score += count * 10
120
- if (str.startsWith(q)) score += 5
121
- if (str.includes(q)) score += 2
122
- }
123
- if (r.project && r.project.toLowerCase() === (project || "").toLowerCase()) score += 20
124
- if (r.project && project && r.project.toLowerCase().includes(project.toLowerCase())) score += 10
125
- const age = Date.now() - Date.parse(r.updated_at || r.created_at || 0)
126
- if (!Number.isNaN(age)) score += Math.max(0, 10 - age / 604800000)
127
- if (r.status === "active") score += 3
128
- if (r.status === "closed") score -= 2
129
- return score
130
- }
131
-
132
- function writeObject(cls, record) {
133
- const dir = classDir(cls)
134
- fs.mkdirSync(dir, { recursive: true })
135
- const fp = path.join(dir, `${record.id}.json`)
136
- atomicWriteJson(fp, record)
137
- const indexPath = path.join(dir, "index.json")
138
- let index = readJSON(indexPath, [])
139
- if (!Array.isArray(index)) index = []
140
- const idx = index.findIndex(e => e?.id === record.id)
141
- const entry = buildEntry(cls, record)
142
- if (idx >= 0) index[idx] = entry; else index.push(entry)
143
- atomicWriteJson(indexPath, index)
144
- }
145
-
146
- function upsertMistake(record) {
147
- const dir = classDir("mistake")
148
- fs.mkdirSync(dir, { recursive: true })
149
- const fp = path.join(dir, "mistakes.jsonl")
150
- let entries = readJSONL(fp)
151
- const idx = entries.findIndex(e => e?.id === record.id)
152
- if (idx >= 0) entries[idx] = record; else entries.push(record)
153
- const text = entries.map(e => stableStringify(e)).join("\n")
154
- fs.writeFileSync(fp, text ? `${text}\n` : "", "utf8")
155
- }
156
-
157
- export function handlePut(cls, id, dataStr) {
158
- let parsed
159
- try { parsed = JSON.parse(dataStr) } catch (e) { return `data must be valid JSON: ${e.message}` }
160
- if (!isPlainObject(parsed)) return "data must be a JSON object"
161
- if (!id?.trim()) return "non-blank id is required"
162
-
163
- const now = new Date().toISOString()
164
- const record = {
165
- ...parsed, id, class: cls,
166
- source: parsed.source ?? "agent",
167
- status: parsed.status ?? "active",
168
- created_at: parsed.created_at ?? now,
169
- updated_at: now,
170
- }
171
-
172
- const validation = validateSchemaSimple(record)
173
- if (!validation.ok) return `Validation errors: ${validation.errors.join("; ")}`
174
-
175
- if (cls === "mistake") upsertMistake(record)
176
- else writeObject(cls, record)
177
-
178
- return stableStringify({ ok: true, id })
179
- }
180
-
181
- export function handleGet(cls, id) {
182
- if (!id?.trim()) return "non-blank id is required"
183
- const record = queryGet(cls, id.trim())
184
- if (!record) return stableStringify({ ok: false, found: false })
185
- return stableStringify({ ok: true, record })
186
- }
187
-
188
- export function handleList(cls, limit = 10) {
189
- const entries = queryList(cls, Math.min(limit, 100))
190
- return stableStringify({ ok: true, count: entries.length, entries })
191
- }
192
-
193
- export function handleLatest(cls) {
194
- const list = queryList(cls, 100)
195
- const active = filterActive(list)
196
- if (!active[0]?.id) return stableStringify({ ok: false, found: false })
197
- const record = queryGet(cls, active[0].id)
198
- if (!record) return stableStringify({ ok: false, found: false })
199
- return stableStringify({ ok: true, record })
200
- }
201
-
202
- export function handleSearch(query, scope, classes, project, limit) {
203
- const q = (query || "").trim()
204
- if (!q) return "non-blank query is required"
205
- const clsList = Array.isArray(classes) && classes.length ? classes : CLASSES
206
- const lim = Math.min(limit ?? 10, 50)
207
- let records = []
208
- for (const cls of clsList) {
209
- if (cls === "mistake") {
210
- for (const m of readJSONL(path.join(classDir(cls), "mistakes.jsonl"))) {
211
- if (!hasExpired(m)) records.push(m)
212
- }
213
- } else {
214
- const dir = classDir(cls)
215
- let files = []
216
- try { files = fs.readdirSync(dir).filter(f => f.endsWith(".json") && f !== "index.json") } catch { continue }
217
- for (const f of files) {
218
- const r = readJSON(path.join(dir, f), null)
219
- if (r && !hasExpired(r)) records.push(r)
220
- }
221
- }
222
- }
223
- if (scope === "global") records = records.filter(r => r.scope === "global" || !r.scope)
224
- else if (scope === "local") records = records.filter(r => r.scope === "project" || r.scope === "session")
225
- const scored = records.map(r => ({ ...buildEntry(r.class || "verification_receipt", r), score: scoreRelevance(r, q, project || "") }))
226
- .filter(e => e.score > 0)
227
- .sort((a, b) => b.score - a.score)
228
- .slice(0, lim)
229
- return stableStringify({ ok: true, count: scored.length, query: q, results: scored })
230
- }
@@ -1,13 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin"
2
- import { handleGet } from "./_memory.mjs"
3
-
4
- export default tool({
5
- description: "Get a specific OpenHermes memory record by class and ID",
6
- args: {
7
- class: tool.schema.enum(["audit", "checkpoint", "mistake", "instinct", "decision", "constraint", "backlog", "verification_receipt"]).describe("Memory class"),
8
- id: tool.schema.string().describe("Record ID to retrieve"),
9
- },
10
- async execute(args) {
11
- return handleGet(args.class, args.id)
12
- },
13
- })
@@ -1,12 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin"
2
- import { handleLatest } from "./_memory.mjs"
3
-
4
- export default tool({
5
- description: "Get the latest active OpenHermes memory record by class (returns the full record, not just metadata)",
6
- args: {
7
- class: tool.schema.enum(["audit", "checkpoint", "mistake", "instinct", "decision", "constraint", "backlog", "verification_receipt"]).describe("Memory class to get the latest from"),
8
- },
9
- async execute(args) {
10
- return handleLatest(args.class)
11
- },
12
- })
@@ -1,13 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin"
2
- import { handleList } from "./_memory.mjs"
3
-
4
- export default tool({
5
- description: "List OpenHermes memory records by class, sorted by recency (newest first)",
6
- args: {
7
- class: tool.schema.enum(["audit", "checkpoint", "mistake", "instinct", "decision", "constraint", "backlog", "verification_receipt"]).describe("Memory class to list"),
8
- limit: tool.schema.number().optional().default(10).describe("Max results (max 100)"),
9
- },
10
- async execute(args) {
11
- return handleList(args.class, args.limit)
12
- },
13
- })
@@ -1,14 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin"
2
- import { handlePut } from "./_memory.mjs"
3
-
4
- export default tool({
5
- description: "Create or update an OpenHermes memory record (checkpoints, mistakes, decisions, constraints, instincts, audits, backlog items, verification receipts)",
6
- args: {
7
- class: tool.schema.enum(["audit", "checkpoint", "mistake", "instinct", "decision", "constraint", "backlog", "verification_receipt"]).describe("Memory class to write to"),
8
- id: tool.schema.string().describe("Unique record ID (e.g. 'chk_2026-01-01T00-00-00-000Z')"),
9
- data: tool.schema.string().describe("JSON string of the record fields (summary, scope, provenance, etc.)"),
10
- },
11
- async execute(args) {
12
- return handlePut(args.class, args.id, args.data)
13
- },
14
- })
@@ -1,16 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin"
2
- import { handleSearch } from "./_memory.mjs"
3
-
4
- export default tool({
5
- description: "Search OpenHermes memory records with keyword matching and relevance ranking. Searches across all classes by default.",
6
- args: {
7
- query: tool.schema.string().describe("Search query string (matches against summary, id, description, tags, etc.)"),
8
- scope: tool.schema.enum(["global", "local", "auto"]).optional().default("auto").describe("Search scope: global (only global-scope records), local (only project/session-scope), auto (all)"),
9
- classes: tool.schema.array(tool.schema.enum(["audit", "checkpoint", "mistake", "instinct", "decision", "constraint", "backlog", "verification_receipt"])).optional().describe("Memory classes to search (default: all)"),
10
- project: tool.schema.string().optional().describe("Project name filter for boosted relevance scoring"),
11
- limit: tool.schema.number().optional().default(10).describe("Max results (max 50)"),
12
- },
13
- async execute(args) {
14
- return handleSearch(args.query, args.scope, args.classes, args.project, args.limit)
15
- },
16
- })