openhermes 1.2.2 → 1.4.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/README.md CHANGED
@@ -97,7 +97,7 @@ The LLM reads rules on demand via the injected paths. Memory directories auto-cr
97
97
 
98
98
  ## Memory Architecture
99
99
 
100
- Nine memory classes, all schema-validated before persistence, stored at `~/.config/opencode/openhermes/memory/`:
100
+ Nine memory classes, all schema-validated before persistence, stored at `~/.local/share/opencode/openhermes/memory/`:
101
101
 
102
102
  | Class | Format | Purpose |
103
103
  |-------|--------|---------|
@@ -111,6 +111,14 @@ Nine memory classes, all schema-validated before persistence, stored at `~/.conf
111
111
  | `verification_receipt` | JSON | Cached verification results keyed by artifact fingerprint |
112
112
  | `recall` | JSON | Session-start cache aggregating active state for compaction injection |
113
113
 
114
+ OpenHermes follows the same storage contract as OpenCode itself — see [OpenCode docs on storage](https://opencode.ai/docs/troubleshooting/#storage):
115
+
116
+ | What | Where |
117
+ |---|---|
118
+ | Config (schemas, archive) | `~/.config/opencode/openhermes/` |
119
+ | Durable memory + runtime state | `~/.local/share/opencode/openhermes/` |
120
+ | Derived recall cache | `~/.cache/opencode/openhermes/recall/` |
121
+
114
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.
115
123
 
116
124
  ---
@@ -153,7 +161,7 @@ permission.replied
153
161
 
154
162
  ## Bundled Harness
155
163
 
156
- The full OpenHermes framework ships inside the package — 45 files across 6 directories:
164
+ The full OpenHermes framework ships inside the package — 60 files across 6 directories:
157
165
 
158
166
  ```
159
167
  harness/
@@ -190,7 +198,7 @@ harness/
190
198
  ├── prompts/ (7 files)
191
199
  │ # Subagent prompt templates: architect, build-error-resolver,
192
200
  │ # code-reviewer, e2e-runner, explore, planner, security-reviewer
193
- └── commands/ (8 files)
201
+ └── commands/ (7 files)
194
202
  # Slash command templates: build-fix, code-review, doctor,
195
203
  # learn, memory-search, plan, security
196
204
  ```
package/autorecall.mjs CHANGED
@@ -2,21 +2,9 @@ import path from "node:path"
2
2
  import os from "node:os"
3
3
  import fs from "node:fs"
4
4
  import { atomicWriteJson, fingerprintEnvironment, isTruthy, sanitizeRecord, truncateText } from "./lib/hardening.mjs"
5
+ import { getDataRoot, getCacheRoot, getMemoryRoot, getRecallRoot, getRuntimeRoot } from "./lib/paths.mjs"
5
6
 
6
- function getHarnessRoot(directory) {
7
- const home = process.env.USERPROFILE || os.homedir()
8
- const configRoot = path.join(home, ".config", "opencode")
9
- const projectHarness = path.join(directory, ".opencode", "openhermes")
10
- const projectMemory = path.join(projectHarness, "memory")
11
- if (isTruthy(process.env.OPENCODE_ALLOW_PROJECT_HARNESS)) {
12
- try {
13
- fs.accessSync(projectMemory)
14
- return projectHarness
15
- } catch {
16
- }
17
- }
18
- return path.join(configRoot, "openhermes")
19
- }
7
+ const OLD_BASE = path.join(os.homedir(), ".config", "opencode", "openhermes")
20
8
 
21
9
  function readJson(fp, fallback) {
22
10
  try { return JSON.parse(fs.readFileSync(fp, "utf8")) } catch { return fallback }
@@ -88,31 +76,56 @@ function formatMemoryWriteGap(memory) {
88
76
  return `## Memory Write Gap\nThese memory classes are empty: ${gaps.join(", ")}. Write at least one ${gaps[0]} this session.`
89
77
  }
90
78
 
91
- async function loadMemoryAndWriteCache(harnessRoot, projectKey, directory) {
79
+ async function loadMemoryAndWriteCache(projectKey, directory) {
80
+ const SENTINEL = path.join(getDataRoot(), ".migrated-from-v1")
81
+ if (!fs.existsSync(SENTINEL)) {
82
+ const oldMemory = path.join(OLD_BASE, "memory")
83
+ if (fs.existsSync(oldMemory)) {
84
+ fs.cpSync(oldMemory, getMemoryRoot(), { recursive: true })
85
+ fs.rmSync(oldMemory, { recursive: true, force: true })
86
+ }
87
+ const oldCache = path.join(OLD_BASE, "memory", "recall")
88
+ if (fs.existsSync(oldCache)) {
89
+ fs.mkdirSync(getRecallRoot(), { recursive: true })
90
+ const files = fs.readdirSync(oldCache).filter(f => f.endsWith(".json"))
91
+ for (const f of files) fs.cpSync(path.join(oldCache, f), path.join(getRecallRoot(), f))
92
+ }
93
+ const oldRuntime = path.join(OLD_BASE, "runtime")
94
+ if (fs.existsSync(oldRuntime)) {
95
+ fs.cpSync(oldRuntime, getRuntimeRoot(), { recursive: true })
96
+ fs.rmSync(oldRuntime, { recursive: true, force: true })
97
+ }
98
+ const oldArchive = path.join(OLD_BASE, "archive")
99
+ if (fs.existsSync(oldArchive)) {
100
+ fs.rmSync(oldArchive, { recursive: true, force: true })
101
+ }
102
+ fs.writeFileSync(SENTINEL, new Date().toISOString(), "utf8")
103
+ }
104
+
92
105
  const memory = { constraints: [], decisions: [], mistakes: [], checkpoint: null, pendingSkillCandidates: [] }
93
- const fingerprint = buildEnvironmentFingerprint(harnessRoot, directory, projectKey)
106
+ const fingerprint = buildEnvironmentFingerprint(getDataRoot(), directory, projectKey)
94
107
 
95
- const constraintsIndex = readJson(path.join(harnessRoot, "memory", "constraints", "index.json"), [])
108
+ const constraintsIndex = readJson(path.join(getMemoryRoot(), "constraints", "index.json"), [])
96
109
  if (Array.isArray(constraintsIndex)) memory.constraints = constraintsIndex.filter(e => e.status === "active")
97
110
 
98
- const decisionsIndex = readJson(path.join(harnessRoot, "memory", "decisions", "index.json"), [])
111
+ const decisionsIndex = readJson(path.join(getMemoryRoot(), "decisions", "index.json"), [])
99
112
  if (Array.isArray(decisionsIndex)) {
100
113
  memory.decisions = decisionsIndex
101
114
  .filter(e => e.status === "active")
102
- .map(entry => loadMemoryRecord(harnessRoot, "decisions", entry))
115
+ .map(entry => loadMemoryRecord(getDataRoot(), "decisions", entry))
103
116
  .filter(validateMemoryRecord)
104
117
  }
105
118
 
106
- const allMistakes = readJsonl(path.join(harnessRoot, "memory", "mistakes", "mistakes.jsonl"))
119
+ const allMistakes = readJsonl(path.join(getMemoryRoot(), "mistakes", "mistakes.jsonl"))
107
120
  if (allMistakes.length) memory.mistakes = allMistakes.filter(e => e.status === "active").slice(0, 5)
108
121
 
109
- const checkpointIndex = readJson(path.join(harnessRoot, "memory", "checkpoints", "index.json"), [])
122
+ const checkpointIndex = readJson(path.join(getMemoryRoot(), "checkpoints", "index.json"), [])
110
123
  if (Array.isArray(checkpointIndex) && checkpointIndex.length > 0) {
111
124
  const latest = checkpointIndex.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at))[0]
112
- memory.checkpoint = readJson(path.join(harnessRoot, "memory", "checkpoints", `${latest.id}.json`), null)
125
+ memory.checkpoint = readJson(path.join(getMemoryRoot(), "checkpoints", `${latest.id}.json`), null)
113
126
  }
114
127
 
115
- const backlogIndex = readJson(path.join(harnessRoot, "memory", "backlog", "index.json"), [])
128
+ const backlogIndex = readJson(path.join(getMemoryRoot(), "backlog", "index.json"), [])
116
129
  if (Array.isArray(backlogIndex)) {
117
130
  memory.pendingSkillCandidates = backlogIndex.filter(e =>
118
131
  e.status === "open" && (e.summary || "").includes("skill-candidate")
@@ -130,13 +143,13 @@ async function loadMemoryAndWriteCache(harnessRoot, projectKey, directory) {
130
143
  const context = contextParts.join("\n\n")
131
144
  const boundedContext = context ? truncateText(context, 12000) : null
132
145
 
133
- const cacheDir = path.join(harnessRoot, "memory", "recall")
146
+ const cacheDir = getRecallRoot()
134
147
  fs.mkdirSync(cacheDir, { recursive: true })
135
148
  atomicWriteJson(path.join(cacheDir, "cache.json"), sanitizeRecord({
136
149
  context: boundedContext,
137
150
  project: projectKey,
138
151
  trust_mode: isTruthy(process.env.OPENCODE_ALLOW_PROJECT_HARNESS) ? "project" : "global",
139
- harness_root: harnessRoot,
152
+ harness_root: getDataRoot(),
140
153
  project_root: directory,
141
154
  updated_at: new Date().toISOString(),
142
155
  fingerprint,
@@ -158,9 +171,8 @@ export const AutorecallPlugin = async ({ project, directory }) => {
158
171
  return {
159
172
  event: async ({ event }) => {
160
173
  if (event.type === "session.created") {
161
- const harnessRoot = getHarnessRoot(directory)
162
174
  const projectKey = project?.name || path.basename(directory)
163
- await loadMemoryAndWriteCache(harnessRoot, projectKey, directory)
175
+ await loadMemoryAndWriteCache(projectKey, directory)
164
176
  }
165
177
  },
166
178
  }
package/bootstrap.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  import path from "node:path"
2
2
  import fs from "node:fs"
3
- import os from "node:os"
4
3
  import { fileURLToPath } from "node:url"
5
4
 
6
5
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -9,8 +8,7 @@ const RULES_DIR = path.join(HARNESS_DIR, "rules")
9
8
  const SKILLS_DIR = path.join(HARNESS_DIR, "skills")
10
9
  const CONSTITUTION_FILE = path.join(HARNESS_DIR, "constitution", "soul.md")
11
10
  const RUNTIME_FILE = path.join(HARNESS_DIR, "instructions", "RUNTIME.md")
12
- const TOOLS_SOURCE_DIR = path.resolve(__dirname, "lib", "tools")
13
- const USER_TOOLS_DIR = path.join(os.homedir(), ".config", "opencode", "tools")
11
+
14
12
 
15
13
  let _bootstrapCache = undefined
16
14
 
@@ -35,7 +33,7 @@ Snapshot before mutation. Never delete unrelated files. Never assume \`%USERPROF
35
33
  | Category | Items |
36
34
  |----------|-------|
37
35
  | **Native tools** | \`read\`, \`write\`, \`edit\`, \`glob\`, \`grep\`, \`bash\`, \`task\`, \`webfetch\`, \`skill\`, \`todowrite\`, \`todoread\` |
38
- | **MCP: openhermes-memory** | \`hm_put\`, \`hm_get\`, \`hm_list\`, \`hm_latest\`, \`hm_search\` |
36
+ | **In-process tools** | \`hm_put\`, \`hm_get\`, \`hm_list\`, \`hm_latest\`, \`hm_search\` |
39
37
  | **Memory recall cache** | \`openhermes/memory/recall/cache.json\` — read on session start, no MCP round-trip |
40
38
  | **Subagents** | \`explore\` (read-only), \`general\` (multi-step), \`architect\`, \`planner\`, \`build-error-resolver\`, \`code-reviewer\`, \`security-reviewer\`, \`e2e-runner\` |
41
39
  | **Plugins** | \`curator\` (checkpoints, mistakes, audit, compaction), \`autorecall\` (recall cache on \`session.created\`), \`skill-builder\` (complex session detection) |
@@ -116,35 +114,7 @@ function getOwnVersion() {
116
114
  } catch { return "1.0.0" }
117
115
  }
118
116
 
119
- function installToolFiles() {
120
- try {
121
- if (!fs.existsSync(TOOLS_SOURCE_DIR)) return
122
- const files = fs.readdirSync(TOOLS_SOURCE_DIR).filter(f => f.endsWith(".mjs") && f !== "_memory.mjs")
123
- if (!files.length) return
124
- fs.mkdirSync(USER_TOOLS_DIR, { recursive: true })
125
- const pkgVersion = getOwnVersion()
126
- const markerPath = path.join(USER_TOOLS_DIR, ".openhermes-version")
127
- let installedVersion = ""
128
- try { installedVersion = fs.readFileSync(markerPath, "utf8").trim() } catch {}
129
- if (installedVersion === pkgVersion) {
130
- const existing = fs.readdirSync(USER_TOOLS_DIR).filter(f => f.endsWith(".mjs"))
131
- const needed = [...files, "_memory.mjs"]
132
- if (needed.every(f => existing.includes(f))) return
133
- }
134
- for (const f of ["_memory.mjs", ...files]) {
135
- const src = path.join(TOOLS_SOURCE_DIR, f)
136
- const dst = path.join(USER_TOOLS_DIR, f)
137
- if (fs.existsSync(src)) fs.copyFileSync(src, dst)
138
- }
139
- fs.writeFileSync(markerPath, pkgVersion, "utf8")
140
- process.stderr.write(`[openhermes-bootstrap] installed ${files.length + 1} tool files (v${pkgVersion})\n`)
141
- } catch (err) {
142
- process.stderr.write(`[openhermes-bootstrap] tool install error: ${err.message}\n`)
143
- }
144
- }
145
-
146
117
  export const BootstrapPlugin = async ({ client, directory }) => {
147
- installToolFiles()
148
118
 
149
119
  const getContent = () => {
150
120
  if (_bootstrapCache !== undefined) return _bootstrapCache
@@ -220,6 +190,23 @@ export const BootstrapPlugin = async ({ client, directory }) => {
220
190
  if (!config.agent[name]) config.agent[name] = def
221
191
  }
222
192
 
193
+ if (!config.agent["OpenHermes"]) {
194
+ config.agent["OpenHermes"] = {
195
+ description: "Fully autonomous primary coding agent (all tools allowed)",
196
+ mode: "primary",
197
+ color: "#F59E0B",
198
+ permission: {
199
+ bash: { "*": "allow" },
200
+ edit: "allow",
201
+ read: "allow",
202
+ task: { "*": "allow" }
203
+ }
204
+ }
205
+ }
206
+ if (!config.default_agent) {
207
+ config.default_agent = "OpenHermes"
208
+ }
209
+
223
210
  config.command = config.command || {}
224
211
  const COMMANDS_DIR = path.join(HARNESS_DIR, "commands")
225
212
  const ct = (file) => `{file:${path.join(COMMANDS_DIR, file)}}\n\n$ARGUMENTS`
package/curator.mjs CHANGED
@@ -5,6 +5,7 @@ import { findUnsupportedSchemaKeywords, validateSchema } from "./lib/schema-vali
5
5
  import { atomicWriteJson, fingerprintEnvironment, fingerprintFile, isTruthy, redactSensitiveText, sanitizeRecord, truncateText } from "./lib/hardening.mjs"
6
6
  import { fileURLToPath } from "node:url"
7
7
  import { dirname } from "node:path"
8
+ import { getDataRoot, getMemoryRoot, getRuntimeRoot, getArchiveRoot } from "./lib/paths.mjs"
8
9
 
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url))
10
11
 
@@ -19,21 +20,6 @@ function curatorLog(message) {
19
20
  process.stderr.write(`${message}\n`)
20
21
  }
21
22
 
22
- function getHarnessRoot(directory) {
23
- const home = process.env.USERPROFILE || os.homedir()
24
- const configRoot = path.join(home, ".config", "opencode")
25
- const projectHarness = path.join(directory, ".opencode", "openhermes")
26
- const projectMemory = path.join(projectHarness, "memory")
27
- if (isTruthy(process.env.OPENCODE_ALLOW_PROJECT_HARNESS)) {
28
- try {
29
- fs.accessSync(projectMemory)
30
- return projectHarness
31
- } catch {
32
- }
33
- }
34
- return path.join(configRoot, "openhermes")
35
- }
36
-
37
23
  function readJson(fp, fallback) {
38
24
  try { return JSON.parse(fs.readFileSync(fp, "utf8")) } catch { return fallback }
39
25
  }
@@ -92,11 +78,8 @@ function updateLoopState(root, patch) {
92
78
  }
93
79
 
94
80
  function loadSchema(classId) {
95
- const home = process.env.USERPROFILE || os.homedir()
96
- const fp = path.join(home, ".config", "opencode", "openhermes", "schemas", `${classId}.schema.json`)
97
- try { return JSON.parse(fs.readFileSync(fp, "utf8")) } catch {}
98
- const bundled = path.join(__dirname, "schemas", `${classId}.schema.json`)
99
- try { return JSON.parse(fs.readFileSync(bundled, "utf8")) } catch { return null }
81
+ const fp = path.join(__dirname, "schemas", `${classId}.schema.json`)
82
+ try { return JSON.parse(fs.readFileSync(fp, "utf8")) } catch { return null }
100
83
  }
101
84
 
102
85
  function validateRecordAgainstSchema(record) {
@@ -242,11 +225,16 @@ function writeMistakeRecord(root, project, directory, error) {
242
225
  project: project?.name || path.basename(directory),
243
226
  environment_fingerprint: environmentFingerprint,
244
227
  }
245
- const dir = path.join(root, "memory", "mistakes")
228
+ const dir = path.join(getMemoryRoot(), "mistakes")
246
229
  fs.mkdirSync(dir, { recursive: true })
247
230
  const fp = path.join(dir, "mistakes.jsonl")
248
- const line = JSON.stringify(sanitizeRecord(record, { maxStringLength: 4000 }))
249
- try { fs.appendFileSync(fp, line + "\n") } catch { fs.writeFileSync(fp, line + "\n") }
231
+ const safeRecord = sanitizeRecord(record, { maxStringLength: 4000 })
232
+ let entries = []
233
+ try { entries = fs.readFileSync(fp, "utf8").trim().split("\n").filter(Boolean).map(l => JSON.parse(l)) } catch {}
234
+ const idx = entries.findIndex(e => e.id === safeRecord.id)
235
+ if (idx >= 0) entries[idx] = safeRecord; else entries.push(safeRecord)
236
+ const text = entries.map(e => JSON.stringify(e)).join("\n")
237
+ fs.writeFileSync(fp, text ? text + "\n" : "", "utf8")
250
238
  curatorLog(`[curator] mistake logged: ${id} - ${safeLogMessage(errorMsg, 80)}`)
251
239
  return id
252
240
  }
@@ -298,7 +286,7 @@ function writeVerificationReceipt(root, project, directory, checkpointId) {
298
286
 
299
287
  async function handleSessionIdle(directory, project) {
300
288
  try {
301
- const root = getHarnessRoot(directory)
289
+ const root = getDataRoot()
302
290
  const checkpointId = await writeCheckpoint(root, project, directory, "session.idle", null)
303
291
  if (checkpointId) {
304
292
  writeVerificationReceipt(root, project, directory, checkpointId)
@@ -310,7 +298,7 @@ async function handleSessionIdle(directory, project) {
310
298
 
311
299
  async function handleSessionCompacted(directory, project) {
312
300
  try {
313
- const root = getHarnessRoot(directory)
301
+ const root = getDataRoot()
314
302
  const ts = new Date().toISOString()
315
303
  updateLoopState(root, {
316
304
  status: "compacted",
@@ -326,7 +314,7 @@ async function handleSessionCompacted(directory, project) {
326
314
 
327
315
  async function handleSessionError(directory, project, event) {
328
316
  try {
329
- const root = getHarnessRoot(directory)
317
+ const root = getDataRoot()
330
318
  const ts = new Date().toISOString()
331
319
  const errorMsg = typeof event?.error === "object" && event.error !== null
332
320
  ? (event.error.message || JSON.stringify(event.error).slice(0, 200))
@@ -347,7 +335,7 @@ async function handleSessionError(directory, project, event) {
347
335
 
348
336
  async function handlePermissionReplied(directory, project, event) {
349
337
  try {
350
- const root = getHarnessRoot(directory)
338
+ const root = getDataRoot()
351
339
  const ts = new Date().toISOString()
352
340
  const id = `audit_perm_${ts.replace(/[:.]/g, "-")}`
353
341
  const environmentFingerprint = buildEnvironmentFingerprint(root, directory, project)
@@ -387,7 +375,14 @@ async function handlePermissionReplied(directory, project, event) {
387
375
  environment_fingerprint: environmentFingerprint,
388
376
  }
389
377
  const safeRecord = sanitizeRecord(record, { maxStringLength: 4000 })
390
- if (!validateRecordAgainstSchema(safeRecord)) return
378
+ const auditSchema = loadSchema("audit")
379
+ if (auditSchema) {
380
+ const unsupported = findUnsupportedSchemaKeywords(auditSchema)
381
+ if (!unsupported.length) {
382
+ const errors = validateSchema(auditSchema, safeRecord, "$")
383
+ if (errors.length) return
384
+ }
385
+ }
391
386
  const dir = path.join(root, "memory", "audits")
392
387
  fs.mkdirSync(dir, { recursive: true })
393
388
  atomicWriteJson(path.join(dir, `${id}.json`), safeRecord)
@@ -401,6 +396,25 @@ async function handlePermissionReplied(directory, project, event) {
401
396
  export const CuratorPlugin = async ({ project, directory }) => {
402
397
  return {
403
398
  event: async ({ event }) => {
399
+ const OLD_BASE = path.join(os.homedir(), ".config", "opencode", "openhermes")
400
+ const SENTINEL = path.join(getDataRoot(), ".migrated-from-v1")
401
+ if (!fs.existsSync(SENTINEL)) {
402
+ const oldMemory = path.join(OLD_BASE, "memory")
403
+ if (fs.existsSync(oldMemory)) {
404
+ fs.cpSync(oldMemory, getMemoryRoot(), { recursive: true })
405
+ fs.rmSync(oldMemory, { recursive: true, force: true })
406
+ }
407
+ const oldRuntime = path.join(OLD_BASE, "runtime")
408
+ if (fs.existsSync(oldRuntime)) {
409
+ fs.cpSync(oldRuntime, getRuntimeRoot(), { recursive: true })
410
+ fs.rmSync(oldRuntime, { recursive: true, force: true })
411
+ }
412
+ const oldArchive = path.join(OLD_BASE, "archive")
413
+ if (fs.existsSync(oldArchive)) {
414
+ fs.rmSync(oldArchive, { recursive: true, force: true })
415
+ }
416
+ fs.writeFileSync(SENTINEL, new Date().toISOString(), "utf8")
417
+ }
404
418
  if (event.type === "session.idle") {
405
419
  await handleSessionIdle(directory, project)
406
420
  } else if (event.type === "session.compacted") {
@@ -415,7 +429,7 @@ export const CuratorPlugin = async ({ project, directory }) => {
415
429
  },
416
430
  "experimental.session.compacting": async (input, output) => {
417
431
  try {
418
- const root = getHarnessRoot(directory)
432
+ const root = getDataRoot()
419
433
  const projectKey = project?.name || path.basename(directory)
420
434
  const checkpointIndex = readJson(path.join(root, "memory", "checkpoints", "index.json"), [])
421
435
  const constraintsIndex = readJson(path.join(root, "memory", "constraints", "index.json"), [])
@@ -6,11 +6,11 @@ import os from "os"
6
6
 
7
7
  import { atomicWriteJson, fingerprintEnvironment, sanitizeRecord, truncateText } from "./hardening.mjs"
8
8
  import { findUnsupportedSchemaKeywords, validateSchema } from "./schema-validator.mjs"
9
+ import { getMemoryRoot, getRuntimeRoot } from "../lib/paths.mjs"
9
10
 
10
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
11
- const PACKAGE_SCHEMAS = path.resolve(__dirname, "..", "schemas")
12
- const ROOT = path.join(os.homedir(), ".config", "opencode", "openhermes")
13
- const MEMORY_DIR = path.join(ROOT, "memory")
12
+ const SCHEMAS_DIR = path.resolve(__dirname, "..", "schemas")
13
+ const MEMORY_DIR = getMemoryRoot()
14
14
 
15
15
  const CLASSES = ["audit", "checkpoint", "mistake", "instinct", "decision", "constraint", "backlog", "verification_receipt"]
16
16
  const PLURALS = { audit: "audits", checkpoint: "checkpoints", mistake: "mistakes", instinct: "instincts", decision: "decisions", constraint: "constraints", backlog: "backlog", verification_receipt: "verification_receipts" }
@@ -141,7 +141,7 @@ function handlePut(cls, id, dataStr) {
141
141
  const now = new Date().toISOString()
142
142
  const record = { ...parsed, id, class: cls, source: parsed.source ?? "agent", status: parsed.status ?? "active", created_at: parsed.created_at ?? now, updated_at: now }
143
143
 
144
- const schema = readJSON(path.join(PACKAGE_SCHEMAS, `${cls}.schema.json`), null)
144
+ const schema = readJSON(path.join(SCHEMAS_DIR, `${cls}.schema.json`), null)
145
145
  if (schema) {
146
146
  const unsupported = findUnsupportedSchemaKeywords(schema)
147
147
  if (unsupported.length) return `Unsupported schema keywords: ${unsupported.join(", ")}`
@@ -209,7 +209,7 @@ function handleSearch(query, scope, classes, project, limit) {
209
209
 
210
210
  export const MemoryToolsPlugin = async () => {
211
211
  fs.mkdirSync(MEMORY_DIR, { recursive: true })
212
- fs.mkdirSync(path.join(ROOT, "runtime"), { recursive: true })
212
+ fs.mkdirSync(getRuntimeRoot(), { recursive: true })
213
213
 
214
214
  return {
215
215
  tool: {
package/lib/paths.mjs ADDED
@@ -0,0 +1,53 @@
1
+ import path from "node:path"
2
+ import os from "node:os"
3
+ import fs from "node:fs"
4
+ import { fileURLToPath } from "node:url"
5
+
6
+ const HOME = process.env.USERPROFILE || os.homedir()
7
+ const DATA_ROOT = path.join(HOME, ".local", "share", "opencode", "openhermes")
8
+ const CACHE_ROOT = path.join(HOME, ".cache", "opencode", "openhermes")
9
+ const PKG_DIR = path.dirname(fileURLToPath(import.meta.url))
10
+
11
+ function resolveRoot(envVar, fallback) {
12
+ if (!isTruthy(process.env.OPENCODE_ALLOW_PROJECT_HARNESS)) return fallback
13
+ const cwd = process.cwd()
14
+ const project = path.join(cwd, ".opencode", "openhermes")
15
+ try { fs.accessSync(path.join(project, "memory")); return project } catch {}
16
+ return fallback
17
+ }
18
+
19
+ export function getConfigRoot() {
20
+ return null // legacy — no longer used
21
+ }
22
+
23
+ export function getDataRoot() {
24
+ return resolveRoot("OPENCODE_ALLOW_PROJECT_HARNESS", DATA_ROOT)
25
+ }
26
+
27
+ export function getCacheRoot() {
28
+ return CACHE_ROOT
29
+ }
30
+
31
+ export function getMemoryRoot() {
32
+ return path.join(getDataRoot(), "memory")
33
+ }
34
+
35
+ export function getRuntimeRoot() {
36
+ return path.join(getDataRoot(), "runtime")
37
+ }
38
+
39
+ export function getRecallRoot() {
40
+ return path.join(getCacheRoot(), "recall")
41
+ }
42
+
43
+ export function getArchiveRoot() {
44
+ return path.join(getDataRoot(), "archive")
45
+ }
46
+
47
+ export function getSchemaRoot() {
48
+ return path.resolve(PKG_DIR, "..", "schemas")
49
+ }
50
+
51
+ function isTruthy(value) {
52
+ return /^(1|true|yes|on)$/i.test(String(value || ""))
53
+ }
@@ -1,9 +1,9 @@
1
1
  import fs from "fs"
2
2
  import path from "path"
3
3
  import os from "os"
4
+ import { getMemoryRoot, getDataRoot } from "../../lib/paths.mjs"
4
5
 
5
- const ROOT = path.join(os.homedir(), ".config", "opencode", "openhermes")
6
- const MEMORY_DIR = path.join(ROOT, "memory")
6
+ const MEMORY_DIR = getMemoryRoot()
7
7
 
8
8
  const CLASSES = ["audit", "checkpoint", "mistake", "instinct", "decision", "constraint", "backlog", "verification_receipt"]
9
9
  const PLURALS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openhermes",
3
- "version": "1.2.2",
3
+ "version": "1.4.1",
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",
package/skill-builder.mjs CHANGED
@@ -2,17 +2,7 @@ import path from "node:path"
2
2
  import fs from "node:fs"
3
3
  import os from "node:os"
4
4
  import { atomicWriteJson, fingerprintEnvironment, isTruthy, sanitizeRecord } from "./lib/hardening.mjs"
5
-
6
- function getHarnessRoot(directory) {
7
- const home = process.env.USERPROFILE || os.homedir()
8
- const configRoot = path.join(home, ".config", "opencode")
9
- const projectHarness = path.join(directory, ".opencode", "openhermes")
10
- const projectMemory = path.join(projectHarness, "memory")
11
- if (isTruthy(process.env.OPENCODE_ALLOW_PROJECT_HARNESS)) {
12
- try { fs.accessSync(projectMemory); return projectHarness } catch {}
13
- }
14
- return path.join(configRoot, "openhermes")
15
- }
5
+ import { getConfigRoot, getDataRoot, getMemoryRoot } from "./lib/paths.mjs"
16
6
 
17
7
  function readJson(fp, fallback) {
18
8
  try { return JSON.parse(fs.readFileSync(fp, "utf8")) } catch { return fallback }
@@ -50,7 +40,7 @@ export const SkillBuilderPlugin = async ({ project, directory }) => {
50
40
 
51
41
  if (isComplex) {
52
42
  try {
53
- const root = getHarnessRoot(directory)
43
+ const root = getDataRoot()
54
44
  const ts = new Date().toISOString()
55
45
  const id = `bl_skill_candidate_${ts.replace(/[:.]/g, "-")}`
56
46
  const backlogIndexPath = path.join(root, "memory", "backlog", "index.json")