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 +11 -3
- package/autorecall.mjs +39 -27
- package/bootstrap.mjs +19 -32
- package/curator.mjs +43 -29
- package/lib/memory-tools-plugin.mjs +5 -5
- package/lib/paths.mjs +53 -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,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
|
-
|
|
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(
|
|
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(
|
|
106
|
+
const fingerprint = buildEnvironmentFingerprint(getDataRoot(), directory, projectKey)
|
|
94
107
|
|
|
95
|
-
const constraintsIndex = readJson(path.join(
|
|
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(
|
|
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(
|
|
115
|
+
.map(entry => loadMemoryRecord(getDataRoot(), "decisions", entry))
|
|
103
116
|
.filter(validateMemoryRecord)
|
|
104
117
|
}
|
|
105
118
|
|
|
106
|
-
const allMistakes = readJsonl(path.join(
|
|
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(
|
|
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(
|
|
125
|
+
memory.checkpoint = readJson(path.join(getMemoryRoot(), "checkpoints", `${latest.id}.json`), null)
|
|
113
126
|
}
|
|
114
127
|
|
|
115
|
-
const backlogIndex = readJson(path.join(
|
|
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 =
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
| **
|
|
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
|
|
96
|
-
|
|
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(
|
|
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
|
|
249
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
12
|
-
const
|
|
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(
|
|
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(
|
|
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
|
+
}
|
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.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 =
|
|
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")
|