openhermes 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/autorecall.mjs +34 -28
- package/bootstrap.mjs +1 -1
- package/curator.mjs +37 -26
- package/lib/memory-tools-plugin.mjs +4 -4
- package/lib/paths.mjs +52 -0
- package/lib/tools/_memory.mjs +2 -2
- package/package.json +1 -1
- package/skill-builder.mjs +2 -12
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 `~/.
|
|
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 —
|
|
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/ (
|
|
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,7 @@ 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
|
-
|
|
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
|
-
}
|
|
5
|
+
import { getDataRoot, getCacheRoot, getMemoryRoot, getRecallRoot, getRuntimeRoot } from "./lib/paths.mjs"
|
|
20
6
|
|
|
21
7
|
function readJson(fp, fallback) {
|
|
22
8
|
try { return JSON.parse(fs.readFileSync(fp, "utf8")) } catch { return fallback }
|
|
@@ -88,31 +74,52 @@ function formatMemoryWriteGap(memory) {
|
|
|
88
74
|
return `## Memory Write Gap\nThese memory classes are empty: ${gaps.join(", ")}. Write at least one ${gaps[0]} this session.`
|
|
89
75
|
}
|
|
90
76
|
|
|
91
|
-
async function loadMemoryAndWriteCache(
|
|
77
|
+
async function loadMemoryAndWriteCache(projectKey, directory) {
|
|
78
|
+
const OLD_MEMORY = path.join(os.homedir(), ".config", "opencode", "openhermes", "memory")
|
|
79
|
+
const OLD_CACHE = path.join(os.homedir(), ".config", "opencode", "openhermes", "memory", "recall")
|
|
80
|
+
const SENTINEL = path.join(getDataRoot(), ".migrated-from-v1")
|
|
81
|
+
if (!fs.existsSync(SENTINEL)) {
|
|
82
|
+
if (fs.existsSync(OLD_MEMORY)) {
|
|
83
|
+
fs.cpSync(OLD_MEMORY, getMemoryRoot(), { recursive: true })
|
|
84
|
+
fs.rmSync(OLD_MEMORY, { recursive: true, force: true })
|
|
85
|
+
}
|
|
86
|
+
if (fs.existsSync(OLD_CACHE)) {
|
|
87
|
+
fs.mkdirSync(getRecallRoot(), { recursive: true })
|
|
88
|
+
const files = fs.readdirSync(OLD_CACHE).filter(f => f.endsWith(".json"))
|
|
89
|
+
for (const f of files) fs.cpSync(path.join(OLD_CACHE, f), path.join(getRecallRoot(), f))
|
|
90
|
+
}
|
|
91
|
+
const oldRuntime = path.join(os.homedir(), ".config", "opencode", "openhermes", "runtime")
|
|
92
|
+
if (fs.existsSync(oldRuntime)) {
|
|
93
|
+
fs.cpSync(oldRuntime, getRuntimeRoot(), { recursive: true })
|
|
94
|
+
fs.rmSync(oldRuntime, { recursive: true, force: true })
|
|
95
|
+
}
|
|
96
|
+
fs.writeFileSync(SENTINEL, new Date().toISOString(), "utf8")
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
const memory = { constraints: [], decisions: [], mistakes: [], checkpoint: null, pendingSkillCandidates: [] }
|
|
93
|
-
const fingerprint = buildEnvironmentFingerprint(
|
|
100
|
+
const fingerprint = buildEnvironmentFingerprint(getDataRoot(), directory, projectKey)
|
|
94
101
|
|
|
95
|
-
const constraintsIndex = readJson(path.join(
|
|
102
|
+
const constraintsIndex = readJson(path.join(getMemoryRoot(), "constraints", "index.json"), [])
|
|
96
103
|
if (Array.isArray(constraintsIndex)) memory.constraints = constraintsIndex.filter(e => e.status === "active")
|
|
97
104
|
|
|
98
|
-
const decisionsIndex = readJson(path.join(
|
|
105
|
+
const decisionsIndex = readJson(path.join(getMemoryRoot(), "decisions", "index.json"), [])
|
|
99
106
|
if (Array.isArray(decisionsIndex)) {
|
|
100
107
|
memory.decisions = decisionsIndex
|
|
101
108
|
.filter(e => e.status === "active")
|
|
102
|
-
.map(entry => loadMemoryRecord(
|
|
109
|
+
.map(entry => loadMemoryRecord(getDataRoot(), "decisions", entry))
|
|
103
110
|
.filter(validateMemoryRecord)
|
|
104
111
|
}
|
|
105
112
|
|
|
106
|
-
const allMistakes = readJsonl(path.join(
|
|
113
|
+
const allMistakes = readJsonl(path.join(getMemoryRoot(), "mistakes", "mistakes.jsonl"))
|
|
107
114
|
if (allMistakes.length) memory.mistakes = allMistakes.filter(e => e.status === "active").slice(0, 5)
|
|
108
115
|
|
|
109
|
-
const checkpointIndex = readJson(path.join(
|
|
116
|
+
const checkpointIndex = readJson(path.join(getMemoryRoot(), "checkpoints", "index.json"), [])
|
|
110
117
|
if (Array.isArray(checkpointIndex) && checkpointIndex.length > 0) {
|
|
111
118
|
const latest = checkpointIndex.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at))[0]
|
|
112
|
-
memory.checkpoint = readJson(path.join(
|
|
119
|
+
memory.checkpoint = readJson(path.join(getMemoryRoot(), "checkpoints", `${latest.id}.json`), null)
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
const backlogIndex = readJson(path.join(
|
|
122
|
+
const backlogIndex = readJson(path.join(getMemoryRoot(), "backlog", "index.json"), [])
|
|
116
123
|
if (Array.isArray(backlogIndex)) {
|
|
117
124
|
memory.pendingSkillCandidates = backlogIndex.filter(e =>
|
|
118
125
|
e.status === "open" && (e.summary || "").includes("skill-candidate")
|
|
@@ -130,13 +137,13 @@ async function loadMemoryAndWriteCache(harnessRoot, projectKey, directory) {
|
|
|
130
137
|
const context = contextParts.join("\n\n")
|
|
131
138
|
const boundedContext = context ? truncateText(context, 12000) : null
|
|
132
139
|
|
|
133
|
-
const cacheDir =
|
|
140
|
+
const cacheDir = getRecallRoot()
|
|
134
141
|
fs.mkdirSync(cacheDir, { recursive: true })
|
|
135
142
|
atomicWriteJson(path.join(cacheDir, "cache.json"), sanitizeRecord({
|
|
136
143
|
context: boundedContext,
|
|
137
144
|
project: projectKey,
|
|
138
145
|
trust_mode: isTruthy(process.env.OPENCODE_ALLOW_PROJECT_HARNESS) ? "project" : "global",
|
|
139
|
-
harness_root:
|
|
146
|
+
harness_root: getDataRoot(),
|
|
140
147
|
project_root: directory,
|
|
141
148
|
updated_at: new Date().toISOString(),
|
|
142
149
|
fingerprint,
|
|
@@ -158,9 +165,8 @@ export const AutorecallPlugin = async ({ project, directory }) => {
|
|
|
158
165
|
return {
|
|
159
166
|
event: async ({ event }) => {
|
|
160
167
|
if (event.type === "session.created") {
|
|
161
|
-
const harnessRoot = getHarnessRoot(directory)
|
|
162
168
|
const projectKey = project?.name || path.basename(directory)
|
|
163
|
-
await loadMemoryAndWriteCache(
|
|
169
|
+
await loadMemoryAndWriteCache(projectKey, directory)
|
|
164
170
|
}
|
|
165
171
|
},
|
|
166
172
|
}
|
package/bootstrap.mjs
CHANGED
|
@@ -35,7 +35,7 @@ Snapshot before mutation. Never delete unrelated files. Never assume \`%USERPROF
|
|
|
35
35
|
| Category | Items |
|
|
36
36
|
|----------|-------|
|
|
37
37
|
| **Native tools** | \`read\`, \`write\`, \`edit\`, \`glob\`, \`grep\`, \`bash\`, \`task\`, \`webfetch\`, \`skill\`, \`todowrite\`, \`todoread\` |
|
|
38
|
-
| **
|
|
38
|
+
| **In-process tools** | \`hm_put\`, \`hm_get\`, \`hm_list\`, \`hm_latest\`, \`hm_search\` |
|
|
39
39
|
| **Memory recall cache** | \`openhermes/memory/recall/cache.json\` — read on session start, no MCP round-trip |
|
|
40
40
|
| **Subagents** | \`explore\` (read-only), \`general\` (multi-step), \`architect\`, \`planner\`, \`build-error-resolver\`, \`code-reviewer\`, \`security-reviewer\`, \`e2e-runner\` |
|
|
41
41
|
| **Plugins** | \`curator\` (checkpoints, mistakes, audit, compaction), \`autorecall\` (recall cache on \`session.created\`), \`skill-builder\` (complex session detection) |
|
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 { getConfigRoot, getDataRoot, getMemoryRoot, getRuntimeRoot, getArchiveRoot, getSchemaRoot } 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,8 +78,7 @@ function updateLoopState(root, patch) {
|
|
|
92
78
|
}
|
|
93
79
|
|
|
94
80
|
function loadSchema(classId) {
|
|
95
|
-
const
|
|
96
|
-
const fp = path.join(home, ".config", "opencode", "openhermes", "schemas", `${classId}.schema.json`)
|
|
81
|
+
const fp = path.join(getSchemaRoot(), `${classId}.schema.json`)
|
|
97
82
|
try { return JSON.parse(fs.readFileSync(fp, "utf8")) } catch {}
|
|
98
83
|
const bundled = path.join(__dirname, "schemas", `${classId}.schema.json`)
|
|
99
84
|
try { return JSON.parse(fs.readFileSync(bundled, "utf8")) } catch { return null }
|
|
@@ -242,11 +227,16 @@ function writeMistakeRecord(root, project, directory, error) {
|
|
|
242
227
|
project: project?.name || path.basename(directory),
|
|
243
228
|
environment_fingerprint: environmentFingerprint,
|
|
244
229
|
}
|
|
245
|
-
const dir = path.join(
|
|
230
|
+
const dir = path.join(getMemoryRoot(), "mistakes")
|
|
246
231
|
fs.mkdirSync(dir, { recursive: true })
|
|
247
232
|
const fp = path.join(dir, "mistakes.jsonl")
|
|
248
|
-
const
|
|
249
|
-
|
|
233
|
+
const safeRecord = sanitizeRecord(record, { maxStringLength: 4000 })
|
|
234
|
+
let entries = []
|
|
235
|
+
try { entries = fs.readFileSync(fp, "utf8").trim().split("\n").filter(Boolean).map(l => JSON.parse(l)) } catch {}
|
|
236
|
+
const idx = entries.findIndex(e => e.id === safeRecord.id)
|
|
237
|
+
if (idx >= 0) entries[idx] = safeRecord; else entries.push(safeRecord)
|
|
238
|
+
const text = entries.map(e => JSON.stringify(e)).join("\n")
|
|
239
|
+
fs.writeFileSync(fp, text ? text + "\n" : "", "utf8")
|
|
250
240
|
curatorLog(`[curator] mistake logged: ${id} - ${safeLogMessage(errorMsg, 80)}`)
|
|
251
241
|
return id
|
|
252
242
|
}
|
|
@@ -298,7 +288,7 @@ function writeVerificationReceipt(root, project, directory, checkpointId) {
|
|
|
298
288
|
|
|
299
289
|
async function handleSessionIdle(directory, project) {
|
|
300
290
|
try {
|
|
301
|
-
const root =
|
|
291
|
+
const root = getDataRoot()
|
|
302
292
|
const checkpointId = await writeCheckpoint(root, project, directory, "session.idle", null)
|
|
303
293
|
if (checkpointId) {
|
|
304
294
|
writeVerificationReceipt(root, project, directory, checkpointId)
|
|
@@ -310,7 +300,7 @@ async function handleSessionIdle(directory, project) {
|
|
|
310
300
|
|
|
311
301
|
async function handleSessionCompacted(directory, project) {
|
|
312
302
|
try {
|
|
313
|
-
const root =
|
|
303
|
+
const root = getDataRoot()
|
|
314
304
|
const ts = new Date().toISOString()
|
|
315
305
|
updateLoopState(root, {
|
|
316
306
|
status: "compacted",
|
|
@@ -326,7 +316,7 @@ async function handleSessionCompacted(directory, project) {
|
|
|
326
316
|
|
|
327
317
|
async function handleSessionError(directory, project, event) {
|
|
328
318
|
try {
|
|
329
|
-
const root =
|
|
319
|
+
const root = getDataRoot()
|
|
330
320
|
const ts = new Date().toISOString()
|
|
331
321
|
const errorMsg = typeof event?.error === "object" && event.error !== null
|
|
332
322
|
? (event.error.message || JSON.stringify(event.error).slice(0, 200))
|
|
@@ -347,7 +337,7 @@ async function handleSessionError(directory, project, event) {
|
|
|
347
337
|
|
|
348
338
|
async function handlePermissionReplied(directory, project, event) {
|
|
349
339
|
try {
|
|
350
|
-
const root =
|
|
340
|
+
const root = getDataRoot()
|
|
351
341
|
const ts = new Date().toISOString()
|
|
352
342
|
const id = `audit_perm_${ts.replace(/[:.]/g, "-")}`
|
|
353
343
|
const environmentFingerprint = buildEnvironmentFingerprint(root, directory, project)
|
|
@@ -387,7 +377,14 @@ async function handlePermissionReplied(directory, project, event) {
|
|
|
387
377
|
environment_fingerprint: environmentFingerprint,
|
|
388
378
|
}
|
|
389
379
|
const safeRecord = sanitizeRecord(record, { maxStringLength: 4000 })
|
|
390
|
-
|
|
380
|
+
const auditSchema = loadSchema("audit") || readJson(path.join(__dirname, "schemas", "audit.schema.json"), null)
|
|
381
|
+
if (auditSchema) {
|
|
382
|
+
const unsupported = findUnsupportedSchemaKeywords(auditSchema)
|
|
383
|
+
if (!unsupported.length) {
|
|
384
|
+
const errors = validateSchema(auditSchema, safeRecord, "$")
|
|
385
|
+
if (errors.length) return
|
|
386
|
+
}
|
|
387
|
+
}
|
|
391
388
|
const dir = path.join(root, "memory", "audits")
|
|
392
389
|
fs.mkdirSync(dir, { recursive: true })
|
|
393
390
|
atomicWriteJson(path.join(dir, `${id}.json`), safeRecord)
|
|
@@ -401,6 +398,20 @@ async function handlePermissionReplied(directory, project, event) {
|
|
|
401
398
|
export const CuratorPlugin = async ({ project, directory }) => {
|
|
402
399
|
return {
|
|
403
400
|
event: async ({ event }) => {
|
|
401
|
+
const OLD_DATA = path.join(os.homedir(), ".config", "opencode", "openhermes", "memory")
|
|
402
|
+
const SENTINEL = path.join(getDataRoot(), ".migrated-from-v1")
|
|
403
|
+
if (!fs.existsSync(SENTINEL)) {
|
|
404
|
+
if (fs.existsSync(OLD_DATA)) {
|
|
405
|
+
fs.cpSync(OLD_DATA, getMemoryRoot(), { recursive: true })
|
|
406
|
+
fs.rmSync(OLD_DATA, { recursive: true, force: true })
|
|
407
|
+
}
|
|
408
|
+
const oldRuntime = path.join(os.homedir(), ".config", "opencode", "openhermes", "runtime")
|
|
409
|
+
if (fs.existsSync(oldRuntime)) {
|
|
410
|
+
fs.cpSync(oldRuntime, getRuntimeRoot(), { recursive: true })
|
|
411
|
+
fs.rmSync(oldRuntime, { recursive: true, force: true })
|
|
412
|
+
}
|
|
413
|
+
fs.writeFileSync(SENTINEL, new Date().toISOString(), "utf8")
|
|
414
|
+
}
|
|
404
415
|
if (event.type === "session.idle") {
|
|
405
416
|
await handleSessionIdle(directory, project)
|
|
406
417
|
} else if (event.type === "session.compacted") {
|
|
@@ -415,7 +426,7 @@ export const CuratorPlugin = async ({ project, directory }) => {
|
|
|
415
426
|
},
|
|
416
427
|
"experimental.session.compacting": async (input, output) => {
|
|
417
428
|
try {
|
|
418
|
-
const root =
|
|
429
|
+
const root = getDataRoot()
|
|
419
430
|
const projectKey = project?.name || path.basename(directory)
|
|
420
431
|
const checkpointIndex = readJson(path.join(root, "memory", "checkpoints", "index.json"), [])
|
|
421
432
|
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, getConfigRoot, getDataRoot, getSchemaRoot, getRuntimeRoot } from "../lib/paths.mjs"
|
|
9
10
|
|
|
10
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
11
12
|
const PACKAGE_SCHEMAS = path.resolve(__dirname, "..", "schemas")
|
|
12
|
-
const
|
|
13
|
-
const MEMORY_DIR = path.join(ROOT, "memory")
|
|
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(getSchemaRoot(), `${cls}.schema.json`), null) || readJSON(path.join(PACKAGE_SCHEMAS, `${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(
|
|
212
|
+
fs.mkdirSync(getRuntimeRoot(), { recursive: true })
|
|
213
213
|
|
|
214
214
|
return {
|
|
215
215
|
tool: {
|
package/lib/paths.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import os from "node:os"
|
|
3
|
+
import fs from "node:fs"
|
|
4
|
+
|
|
5
|
+
const HOME = process.env.USERPROFILE || os.homedir()
|
|
6
|
+
const CONFIG_ROOT = path.join(HOME, ".config", "opencode", "openhermes")
|
|
7
|
+
const DATA_ROOT = path.join(HOME, ".local", "share", "opencode", "openhermes")
|
|
8
|
+
const CACHE_ROOT = path.join(HOME, ".cache", "opencode", "openhermes")
|
|
9
|
+
|
|
10
|
+
function resolveRoot(envVar, fallback) {
|
|
11
|
+
if (!isTruthy(process.env.OPENCODE_ALLOW_PROJECT_HARNESS)) return fallback
|
|
12
|
+
const cwd = process.cwd()
|
|
13
|
+
const project = path.join(cwd, ".opencode", "openhermes")
|
|
14
|
+
try { fs.accessSync(path.join(project, "memory")); return project } catch {}
|
|
15
|
+
return fallback
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getConfigRoot() {
|
|
19
|
+
return resolveRoot("OPENCODE_ALLOW_PROJECT_HARNESS", CONFIG_ROOT)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getDataRoot() {
|
|
23
|
+
return resolveRoot("OPENCODE_ALLOW_PROJECT_HARNESS", DATA_ROOT)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getCacheRoot() {
|
|
27
|
+
return CACHE_ROOT
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getMemoryRoot() {
|
|
31
|
+
return path.join(getDataRoot(), "memory")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getRuntimeRoot() {
|
|
35
|
+
return path.join(getDataRoot(), "runtime")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getRecallRoot() {
|
|
39
|
+
return path.join(getCacheRoot(), "recall")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getArchiveRoot() {
|
|
43
|
+
return path.join(getConfigRoot(), "archive")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getSchemaRoot() {
|
|
47
|
+
return path.join(getConfigRoot(), "schemas")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isTruthy(value) {
|
|
51
|
+
return /^(1|true|yes|on)$/i.test(String(value || ""))
|
|
52
|
+
}
|
package/lib/tools/_memory.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
3
|
+
"version": "1.3.0",
|
|
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 =
|
|
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")
|