openhermes 1.13.1 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +123 -208
  2. package/autorecall.mjs +79 -12
  3. package/bootstrap.mjs +122 -25
  4. package/curator.mjs +4 -40
  5. package/harness/commands/harness-audit.md +1 -1
  6. package/harness/commands/learn.md +2 -2
  7. package/harness/commands/memory-search.md +2 -2
  8. package/harness/constitution/soul.md +16 -4
  9. package/harness/instructions/RUNTIME.md +6 -3
  10. package/harness/prompts/architect.txt +14 -0
  11. package/harness/prompts/build-cpp.md +15 -1
  12. package/harness/prompts/build-error-resolver.md +15 -9
  13. package/harness/prompts/build-go.md +14 -0
  14. package/harness/prompts/build-java.md +15 -1
  15. package/harness/prompts/build-kotlin.md +15 -1
  16. package/harness/prompts/build-rust.md +14 -0
  17. package/harness/prompts/code-reviewer.md +15 -9
  18. package/harness/prompts/doc-updater.md +13 -0
  19. package/harness/prompts/docs-lookup.md +11 -0
  20. package/harness/prompts/e2e-runner.txt +12 -0
  21. package/harness/prompts/explore.md +16 -4
  22. package/harness/prompts/harness-optimizer.md +12 -0
  23. package/harness/prompts/loop-operator.md +11 -0
  24. package/harness/prompts/planner.md +15 -9
  25. package/harness/prompts/refactor-cleaner.md +14 -0
  26. package/harness/prompts/review-cpp.md +14 -1
  27. package/harness/prompts/review-database.md +13 -0
  28. package/harness/prompts/review-go.md +13 -0
  29. package/harness/prompts/review-java.md +14 -1
  30. package/harness/prompts/review-kotlin.md +13 -0
  31. package/harness/prompts/review-python.md +14 -1
  32. package/harness/prompts/review-rust.md +13 -0
  33. package/harness/prompts/security-reviewer.md +15 -9
  34. package/harness/prompts/tdd-guide.md +14 -0
  35. package/harness/rules/audit.md +2 -2
  36. package/harness/rules/delegation.md +0 -2
  37. package/harness/rules/handoff.md +267 -0
  38. package/harness/rules/memory-management.md +4 -4
  39. package/harness/rules/precedence.md +1 -1
  40. package/harness/rules/retrieval.md +5 -5
  41. package/harness/rules/runtime-guards.md +1 -1
  42. package/harness/rules/self-heal.md +1 -1
  43. package/harness/rules/session-start.md +5 -5
  44. package/harness/rules/skills-management.md +2 -2
  45. package/harness/rules/verification.md +4 -4
  46. package/index.mjs +6 -2
  47. package/lib/ambient-memory.mjs +167 -0
  48. package/lib/handoff.mjs +176 -0
  49. package/lib/hardening.mjs +13 -8
  50. package/lib/memory-tools-plugin.mjs +107 -54
  51. package/lib/ohc/block-sync.mjs +69 -0
  52. package/lib/ohc/compress/search.mjs +152 -0
  53. package/lib/ohc/compress/state.mjs +76 -0
  54. package/lib/ohc/config.mjs +172 -16
  55. package/lib/ohc/message-ids.mjs +168 -0
  56. package/lib/ohc/notify.mjs +150 -0
  57. package/lib/ohc/protected-patterns.mjs +54 -0
  58. package/lib/ohc/prune-apply.mjs +134 -0
  59. package/lib/ohc/pruner.mjs +406 -55
  60. package/lib/ohc/reaper.mjs +12 -3
  61. package/lib/ohc/state.mjs +246 -15
  62. package/lib/ohc/strategies/deduplication.mjs +72 -0
  63. package/lib/ohc/strategies/index.mjs +2 -0
  64. package/lib/ohc/strategies/purge-errors.mjs +43 -0
  65. package/lib/ohc/token-utils.mjs +26 -0
  66. package/lib/ohc/updater.mjs +36 -13
  67. package/lib/paths.mjs +0 -3
  68. package/lib/search.mjs +48 -0
  69. package/package.json +1 -1
  70. package/schemas/audit.schema.json +22 -1
  71. package/schemas/backlog.schema.json +23 -2
  72. package/schemas/checkpoint.schema.json +23 -2
  73. package/schemas/constraint.schema.json +23 -2
  74. package/schemas/decision.schema.json +23 -2
  75. package/schemas/instinct.schema.json +23 -2
  76. package/schemas/mistake.schema.json +23 -2
  77. package/schemas/verification_receipt.schema.json +23 -2
  78. package/skill-builder.mjs +12 -23
package/lib/ohc/state.mjs CHANGED
@@ -2,31 +2,262 @@ import fs from "node:fs"
2
2
  import path from "node:path"
3
3
  import os from "node:os"
4
4
 
5
- const STATE_DIR = path.join(os.homedir(), ".local", "share", "opencode")
6
- const STATE_FILE = path.join(STATE_DIR, "ohc-state.json")
5
+ const STATE_DIR = path.join(os.homedir(), ".local", "share", "opencode", "ohc")
6
+ const LEGACY_FILE = path.join(os.homedir(), ".local", "share", "opencode", "ohc-state.json")
7
7
 
8
- function readAll() {
9
- try {
10
- return JSON.parse(fs.readFileSync(STATE_FILE, "utf8"))
11
- } catch {
12
- return {}
13
- }
8
+ function sessionPath(sessionId) {
9
+ const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")
10
+ return path.join(STATE_DIR, `${safe}.json`)
14
11
  }
15
12
 
16
- function writeAll(data) {
13
+ function ensureDir() {
17
14
  fs.mkdirSync(STATE_DIR, { recursive: true })
18
- fs.writeFileSync(STATE_FILE, JSON.stringify(data, null, 2), "utf8")
19
15
  }
20
16
 
17
+ function migrateLegacy() {
18
+ try {
19
+ if (!fs.existsSync(LEGACY_FILE)) return
20
+ const raw = JSON.parse(fs.readFileSync(LEGACY_FILE, "utf8"))
21
+ if (typeof raw !== "object") return
22
+ ensureDir()
23
+ for (const [sid, data] of Object.entries(raw)) {
24
+ const sp = sessionPath(sid)
25
+ if (!fs.existsSync(sp)) {
26
+ fs.writeFileSync(sp, JSON.stringify({ ...data, migratedFrom: "legacy" }, null, 2), "utf8")
27
+ }
28
+ }
29
+ fs.renameSync(LEGACY_FILE, LEGACY_FILE + ".bak")
30
+ } catch {}
31
+ }
32
+
33
+ migrateLegacy()
34
+
21
35
  export function loadOhcState(sessionId) {
22
36
  if (!sessionId) return null
23
- const all = readAll()
24
- return all[sessionId] || null
37
+ try {
38
+ return JSON.parse(fs.readFileSync(sessionPath(sessionId), "utf8"))
39
+ } catch {
40
+ return null
41
+ }
25
42
  }
26
43
 
27
44
  export function saveOhcState(sessionId, data) {
28
45
  if (!sessionId) return
29
- const all = readAll()
30
- all[sessionId] = { ...data, updatedAt: new Date().toISOString() }
31
- writeAll(all)
46
+ ensureDir()
47
+ fs.writeFileSync(sessionPath(sessionId), JSON.stringify({ ...data, updatedAt: new Date().toISOString() }, null, 2), "utf8")
48
+ }
49
+
50
+
51
+ export function createSessionState() {
52
+ return {
53
+ sessionId: null,
54
+ isSubAgent: false,
55
+ manualMode: false,
56
+ pendingManualTrigger: null,
57
+ prune: {
58
+ tools: new Map(),
59
+ messages: {
60
+ byMessageId: new Map(),
61
+ blocksById: new Map(),
62
+ activeBlockIds: new Set(),
63
+ activeByAnchorMessageId: new Map(),
64
+ nextBlockId: 1,
65
+ nextRunId: 1,
66
+ },
67
+ },
68
+ nudges: {
69
+ contextLimitAnchors: new Set(),
70
+ turnNudgeAnchors: new Set(),
71
+ iterationNudgeAnchors: new Set(),
72
+ },
73
+ stats: {
74
+ pruneTokenCounter: 0,
75
+ totalPruneTokens: 0,
76
+ },
77
+ toolParameters: new Map(),
78
+ toolIdList: [],
79
+ messageIds: { byRawId: new Map(), byRef: new Map(), nextRef: 1 },
80
+ lastCompaction: 0,
81
+ currentTurn: 0,
82
+ modelContextLimit: undefined,
83
+ systemPromptTokens: undefined,
84
+ protectedTurns: { enabled: false, turns: 0 },
85
+ compressionTiming: { starts: new Map(), pendingByCallId: new Map(), lastDurationMs: 0, totalDurationMs: 0 },
86
+ lastNudgePct: 0,
87
+ lastAutoPruneAt: null,
88
+ prunedIds: new Set(),
89
+ summary: null,
90
+ anchorMessageId: null,
91
+ totalTokensSaved: 0,
92
+ blockCount: 0,
93
+ }
94
+ }
95
+
96
+ function pruneMapToObj(map) {
97
+ return Object.fromEntries(map)
98
+ }
99
+
100
+ function pruneMapFromObj(obj) {
101
+ if (!obj || typeof obj !== "object") return new Map()
102
+ return new Map(Object.entries(obj))
103
+ }
104
+
105
+ function setToArr(s) {
106
+ return [...s]
107
+ }
108
+
109
+ function setFromArr(a) {
110
+ return new Set(Array.isArray(a) ? a : [])
111
+ }
112
+
113
+ export function serializeState(state) {
114
+ return {
115
+ sessionId: state.sessionId,
116
+ manualMode: state.manualMode,
117
+ lastCompaction: state.lastCompaction,
118
+ currentTurn: state.currentTurn,
119
+ modelContextLimit: state.modelContextLimit,
120
+ systemPromptTokens: state.systemPromptTokens,
121
+ stats: { ...state.stats },
122
+ nudges: {
123
+ contextLimitAnchors: setToArr(state.nudges.contextLimitAnchors),
124
+ turnNudgeAnchors: setToArr(state.nudges.turnNudgeAnchors),
125
+ iterationNudgeAnchors: setToArr(state.nudges.iterationNudgeAnchors),
126
+ },
127
+ prune: {
128
+ tools: pruneMapToObj(state.prune.tools),
129
+ messages: {
130
+ nextBlockId: state.prune?.messages?.nextBlockId || 1,
131
+ nextRunId: state.prune?.messages?.nextRunId || 1,
132
+ blocksById: pruneMapToObj(state.prune?.messages?.blocksById),
133
+ byMessageId: pruneMapToObj(state.prune?.messages?.byMessageId),
134
+ activeBlockIds: setToArr(state.prune?.messages?.activeBlockIds),
135
+ },
136
+ },
137
+ lastAutoPruneAt: state.lastAutoPruneAt,
138
+ totalTokensSaved: state.totalTokensSaved,
139
+ blockCount: state.blockCount,
140
+ summary: state.summary,
141
+ anchorMessageId: state.anchorMessageId,
142
+ prunedIds: setToArr(state.prunedIds),
143
+ isSubAgent: state.isSubAgent || false,
144
+ messageIds: {
145
+ byRawId: pruneMapToObj(state.messageIds?.byRawId),
146
+ byRef: pruneMapToObj(state.messageIds?.byRef),
147
+ nextRef: state.messageIds?.nextRef || 1,
148
+ },
149
+ compressionTiming: {
150
+ starts: Object.fromEntries(state.compressionTiming?.starts || new Map()),
151
+ lastDurationMs: state.compressionTiming?.lastDurationMs || 0,
152
+ totalDurationMs: state.compressionTiming?.totalDurationMs || 0,
153
+ },
154
+ }
155
+ }
156
+
157
+ export function deserializeState(saved) {
158
+ const state = createSessionState()
159
+ if (!saved) return state
160
+ state.sessionId = saved.sessionId || null
161
+ state.manualMode = saved.manualMode || false
162
+ state.lastCompaction = saved.lastCompaction || 0
163
+ state.currentTurn = saved.currentTurn || 0
164
+ state.modelContextLimit = saved.modelContextLimit
165
+ state.systemPromptTokens = saved.systemPromptTokens
166
+ if (saved.stats) Object.assign(state.stats, saved.stats)
167
+ if (saved.nudges) {
168
+ state.nudges.contextLimitAnchors = setFromArr(saved.nudges.contextLimitAnchors)
169
+ state.nudges.turnNudgeAnchors = setFromArr(saved.nudges.turnNudgeAnchors)
170
+ state.nudges.iterationNudgeAnchors = setFromArr(saved.nudges.iterationNudgeAnchors)
171
+ }
172
+ if (saved.prune?.tools) state.prune.tools = pruneMapFromObj(saved.prune.tools)
173
+ if (saved.prune?.messages) {
174
+ const pm = saved.prune.messages
175
+ state.prune.messages.nextBlockId = pm.nextBlockId || 1
176
+ state.prune.messages.nextRunId = pm.nextRunId || 1
177
+ state.prune.messages.blocksById = pruneMapFromObj(pm.blocksById)
178
+ state.prune.messages.byMessageId = pruneMapFromObj(pm.byMessageId)
179
+ state.prune.messages.activeBlockIds = setFromArr(pm.activeBlockIds)
180
+ }
181
+ state.lastAutoPruneAt = saved.lastAutoPruneAt || null
182
+ state.totalTokensSaved = saved.totalTokensSaved || 0
183
+ state.blockCount = saved.blockCount || 0
184
+ state.summary = saved.summary || null
185
+ state.anchorMessageId = saved.anchorMessageId || null
186
+ state.prunedIds = setFromArr(saved.prunedIds)
187
+ if (saved.compressionTiming) {
188
+ state.compressionTiming.starts = pruneMapFromObj(saved.compressionTiming.starts)
189
+ state.compressionTiming.lastDurationMs = saved.compressionTiming.lastDurationMs || 0
190
+ state.compressionTiming.totalDurationMs = saved.compressionTiming.totalDurationMs || 0
191
+ }
192
+ if (saved.messageIds) {
193
+ state.messageIds.byRawId = pruneMapFromObj(saved.messageIds.byRawId)
194
+ state.messageIds.byRef = pruneMapFromObj(saved.messageIds.byRef)
195
+ state.messageIds.nextRef = saved.messageIds.nextRef || 1
196
+ }
197
+ if (saved.isSubAgent !== undefined) state.isSubAgent = saved.isSubAgent
198
+ return state
199
+ }
200
+
201
+ export function buildToolIdList(state, messages) {
202
+ const ids = []
203
+ for (const msg of messages) {
204
+ if (!Array.isArray(msg.parts)) continue
205
+ for (const part of msg.parts) {
206
+ if (part.type === "tool" && part.callID) {
207
+ ids.push(part.callID)
208
+ }
209
+ }
210
+ }
211
+ state.toolIdList = ids
212
+ return ids
213
+ }
214
+
215
+ export function syncToolCache(state, messages) {
216
+ let maxTurn = 0
217
+ for (const msg of messages) {
218
+ if (msg.info?.role === "user") {
219
+ const lastUser = state.toolIdList.length > 0
220
+ if (lastUser) maxTurn++
221
+ }
222
+ if (!Array.isArray(msg.parts)) continue
223
+ for (const part of msg.parts) {
224
+ if (part.type !== "tool" || !part.callID) continue
225
+ const existing = state.toolParameters.get(part.callID)
226
+ if (existing) {
227
+ existing.status = part.state?.status || existing.status
228
+ continue
229
+ }
230
+ state.toolParameters.set(part.callID, {
231
+ tool: part.tool || "unknown",
232
+ parameters: part.state?.input || {},
233
+ status: part.state?.status || "pending",
234
+ turn: maxTurn,
235
+ tokenCount: estimateToolTokens(part),
236
+ lastSeen: Date.now(),
237
+ })
238
+ }
239
+ }
240
+ state.currentTurn = Math.max(state.currentTurn, maxTurn)
241
+ }
242
+
243
+ function estimateToolTokens(part) {
244
+ if (!part.state) return 0
245
+ let t = 0
246
+ if (part.state.input) t += JSON.stringify(part.state.input).length / 4
247
+ if (part.state.output) {
248
+ t += (typeof part.state.output === "string" ? part.state.output : JSON.stringify(part.state.output ?? "")).length / 4
249
+ }
250
+ return Math.ceil(t)
251
+ }
252
+
253
+ export function countTurns(state, messages) {
254
+ let userCount = 0
255
+ for (const msg of messages) {
256
+ if (msg.info?.role === "user") {
257
+ const parts = Array.isArray(msg.parts) ? msg.parts : []
258
+ const hasText = parts.some(p => p.type === "text" && p.text?.trim())
259
+ if (hasText) userCount++
260
+ }
261
+ }
262
+ return userCount
32
263
  }
@@ -0,0 +1,72 @@
1
+ import { isToolNameProtected, getFilePathsFromParameters, isFilePathProtected } from "../protected-patterns.mjs"
2
+ import { getTotalToolTokens } from "../token-utils.mjs"
3
+
4
+ export function deduplicate(state, config, messages) {
5
+ if (state.manualMode && !config.manualMode?.automaticStrategies) return
6
+
7
+ if (!config.strategies?.deduplication?.enabled) return
8
+
9
+ const allIds = state.toolIdList
10
+ if (!allIds?.length) return
11
+
12
+ const unprunedIds = allIds.filter(id => !state.prune.tools.has(id))
13
+ if (!unprunedIds.length) return
14
+
15
+ const protectedTools = config.strategies.deduplication.protectedTools || []
16
+
17
+ const sigMap = new Map()
18
+
19
+ for (const id of unprunedIds) {
20
+ const meta = state.toolParameters.get(id)
21
+ if (!meta) continue
22
+
23
+ if (isToolNameProtected(meta.tool, protectedTools)) continue
24
+
25
+ const fps = getFilePathsFromParameters(meta.tool, meta.parameters)
26
+ if (isFilePathProtected(fps, config.protectedFilePatterns)) continue
27
+
28
+ const sig = createToolSignature(meta.tool, meta.parameters)
29
+ if (!sigMap.has(sig)) sigMap.set(sig, [])
30
+ sigMap.get(sig).push(id)
31
+ }
32
+
33
+ const toPrune = []
34
+ for (const ids of sigMap.values()) {
35
+ if (ids.length > 1) {
36
+ toPrune.push(...ids.slice(0, -1))
37
+ }
38
+ }
39
+
40
+ if (!toPrune.length) return
41
+
42
+ state.stats.totalPruneTokens += getTotalToolTokens(state, toPrune)
43
+ for (const id of toPrune) {
44
+ const entry = state.toolParameters.get(id)
45
+ state.prune.tools.set(id, entry?.tokenCount ?? 0)
46
+ }
47
+ }
48
+
49
+ function createToolSignature(tool, params) {
50
+ if (!params) return tool
51
+ const norm = normalizeParams(params)
52
+ const sorted = sortKeys(norm)
53
+ return `${tool}::${JSON.stringify(sorted)}`
54
+ }
55
+
56
+ function normalizeParams(p) {
57
+ if (typeof p !== "object" || p === null) return p
58
+ if (Array.isArray(p)) return p
59
+ const n = {}
60
+ for (const [k, v] of Object.entries(p)) {
61
+ if (v !== undefined && v !== null) n[k] = v
62
+ }
63
+ return n
64
+ }
65
+
66
+ function sortKeys(o) {
67
+ if (typeof o !== "object" || o === null) return o
68
+ if (Array.isArray(o)) return o.map(sortKeys)
69
+ const s = {}
70
+ for (const k of Object.keys(o).sort()) s[k] = sortKeys(o[k])
71
+ return s
72
+ }
@@ -0,0 +1,2 @@
1
+ export { deduplicate } from "./deduplication.mjs"
2
+ export { purgeErrors } from "./purge-errors.mjs"
@@ -0,0 +1,43 @@
1
+ import { isToolNameProtected, getFilePathsFromParameters, isFilePathProtected } from "../protected-patterns.mjs"
2
+ import { getTotalToolTokens } from "../token-utils.mjs"
3
+
4
+ export function purgeErrors(state, config, messages) {
5
+ if (state.manualMode && !config.manualMode?.automaticStrategies) return
6
+
7
+ if (!config.strategies?.purgeErrors?.enabled) return
8
+
9
+ const allIds = state.toolIdList
10
+ if (!allIds?.length) return
11
+
12
+ const unprunedIds = allIds.filter(id => !state.prune.tools.has(id))
13
+ if (!unprunedIds.length) return
14
+
15
+ const protectedTools = config.strategies.purgeErrors.protectedTools || []
16
+ const threshold = Math.max(1, config.strategies.purgeErrors.turns ?? 4)
17
+
18
+ const toPrune = []
19
+ for (const id of unprunedIds) {
20
+ const meta = state.toolParameters.get(id)
21
+ if (!meta) continue
22
+
23
+ if (isToolNameProtected(meta.tool, protectedTools)) continue
24
+
25
+ const fps = getFilePathsFromParameters(meta.tool, meta.parameters)
26
+ if (isFilePathProtected(fps, config.protectedFilePatterns)) continue
27
+
28
+ if (meta.status !== "error") continue
29
+
30
+ const turnAge = state.currentTurn - meta.turn
31
+ if (turnAge >= threshold) {
32
+ toPrune.push(id)
33
+ }
34
+ }
35
+
36
+ if (!toPrune.length) return
37
+
38
+ state.stats.totalPruneTokens += getTotalToolTokens(state, toPrune)
39
+ for (const id of toPrune) {
40
+ const entry = state.toolParameters.get(id)
41
+ state.prune.tools.set(id, entry?.tokenCount ?? 0)
42
+ }
43
+ }
@@ -0,0 +1,26 @@
1
+ import { totalTokens } from "./reaper.mjs"
2
+
3
+ export { totalTokens }
4
+
5
+ export function countTokens(value) {
6
+ if (typeof value === "string") return Math.ceil(value.length / 4)
7
+ if (typeof value === "object" && value !== null) return Math.ceil(JSON.stringify(value).length / 4)
8
+ return 0
9
+ }
10
+
11
+ export function getTotalToolTokens(state, toolIds) {
12
+ let total = 0
13
+ for (const id of toolIds) {
14
+ const entry = state.toolParameters.get(id)
15
+ if (entry?.tokenCount) total += entry.tokenCount
16
+ }
17
+ return total
18
+ }
19
+
20
+ function estimateToolTokenCost(part) {
21
+ if (part.type !== "tool") return 0
22
+ let t = 0
23
+ if (part.state?.input) t += countTokens(part.state.input)
24
+ if (part.state?.output) t += countTokens(part.state.output)
25
+ return t
26
+ }
@@ -3,7 +3,10 @@ import path from "node:path"
3
3
  import os from "node:os"
4
4
 
5
5
  const CONFIG_PATH = path.join(os.homedir(), ".config", "opencode", "opencode.json")
6
- const CACHE_ROOT = path.join(os.homedir(), ".cache", "opencode", "packages")
6
+ const CACHE_ROOTS = [
7
+ path.join(os.homedir(), ".cache", "opencode", "packages"),
8
+ path.join(os.homedir(), ".cache", "opencode", "node_modules"),
9
+ ]
7
10
 
8
11
  function detectInstallMethod() {
9
12
  let raw = {}
@@ -33,18 +36,37 @@ function detectInstallMethod() {
33
36
  return { method: "unknown", entry: null, reason: "openhermes not found in plugin config" }
34
37
  }
35
38
 
36
- function findCacheDirs() {
37
- const results = []
38
- if (!fs.existsSync(CACHE_ROOT)) return results
39
+ function walkCacheDirs(root, results) {
40
+ if (!fs.existsSync(root)) return
41
+ if (path.basename(root).startsWith("openhermes")) {
42
+ results.push({ name: path.basename(root), path: root })
43
+ }
44
+ let entries = []
39
45
  try {
40
- for (const e of fs.readdirSync(CACHE_ROOT)) {
41
- const full = path.join(CACHE_ROOT, e)
42
- if (e.startsWith("openhermes") && fs.statSync(full).isDirectory()) {
43
- results.push({ name: e, path: full })
44
- }
46
+ entries = fs.readdirSync(root, { withFileTypes: true })
47
+ } catch {
48
+ return
49
+ }
50
+
51
+ for (const entry of entries) {
52
+ if (!entry.isDirectory()) continue
53
+ const full = path.join(root, entry.name)
54
+ if (entry.name.startsWith("openhermes")) {
55
+ results.push({ name: entry.name, path: full })
45
56
  }
46
- } catch {}
47
- return results
57
+ walkCacheDirs(full, results)
58
+ }
59
+ }
60
+
61
+ export function findCacheDirs({ cacheRoots = CACHE_ROOTS } = {}) {
62
+ const results = []
63
+ for (const root of cacheRoots) walkCacheDirs(root, results)
64
+ const seen = new Set()
65
+ return results.filter(dir => {
66
+ if (seen.has(dir.path)) return false
67
+ seen.add(dir.path)
68
+ return true
69
+ })
48
70
  }
49
71
 
50
72
  async function handleUpdateMe(ctx, input, output) {
@@ -65,7 +87,8 @@ async function handleUpdateMe(ctx, input, output) {
65
87
  output.parts.length = 0
66
88
  output.parts.push({
67
89
  type: "text",
68
- text: `[Update-Me] No cached version found. Already at the latest (method: ${info.method}). Restart OpenCode if you suspect a stale install.`,
90
+ text: `[Update-Me] No OpenHermes cache found under OpenCode package/node_modules caches.
91
+ Restart OpenCode to redownload from ${info.method === "git" ? "git HEAD" : "npm registry"}.`,
69
92
  })
70
93
  return
71
94
  }
@@ -92,7 +115,7 @@ async function handleUpdateMe(ctx, input, output) {
92
115
  if (failed.length > 0) {
93
116
  msg += `\n⚠ Could not remove (file may be locked):\n`
94
117
  for (const f of failed) msg += ` ✗ ${f.name} — ${f.error}\n`
95
- msg += `Try deleting manually:\n ${CACHE_ROOT}\n`
118
+ msg += `Try deleting manually:\n ${CACHE_ROOTS.join("\n ")}\n`
96
119
  }
97
120
 
98
121
  msg += `\nRestart OpenCode to load the latest OpenHermes from ${info.method === "git" ? "git HEAD" : "npm registry"}.`
package/lib/paths.mjs CHANGED
@@ -16,9 +16,6 @@ function resolveRoot(envVar, fallback) {
16
16
  return fallback
17
17
  }
18
18
 
19
- export function getConfigRoot() {
20
- return null // legacy — no longer used
21
- }
22
19
 
23
20
  export function getDataRoot() {
24
21
  return resolveRoot("OPENCODE_ALLOW_PROJECT_HARNESS", DATA_ROOT)
package/lib/search.mjs ADDED
@@ -0,0 +1,48 @@
1
+ export function scoreRelevance(r, query, project) {
2
+ const q = query.toLowerCase()
3
+ const tokens = q.split(/\s+/).filter(t => t.length > 2)
4
+ let score = 0
5
+
6
+ const primaryFields = [r.summary, r.description, r.mission, r.current_state, r.failure, r.root_cause, r.fix, r.prevention, r.id].filter(Boolean)
7
+ const secondaryFields = [r.command, r.project, r.scope].filter(Boolean)
8
+ const listFields = [...(Array.isArray(r.tags) ? r.tags : []), ...(Array.isArray(r.next_actions) ? r.next_actions : []), ...(Array.isArray(r.refs) ? r.refs : [])].filter(Boolean)
9
+
10
+ for (const f of primaryFields) {
11
+ const str = String(f).toLowerCase()
12
+ let idx = 0; let count = 0
13
+ while ((idx = str.indexOf(q, idx)) !== -1) { count++; idx += q.length }
14
+ score += count * 15
15
+ if (str.startsWith(q)) score += 10
16
+ if (str.includes(q)) score += 4
17
+ for (const token of tokens) {
18
+ if (str.includes(token)) score += 4
19
+ if (str.startsWith(token)) score += 2
20
+ }
21
+ }
22
+
23
+ for (const f of secondaryFields) {
24
+ const str = String(f).toLowerCase()
25
+ if (str.includes(q)) score += 8
26
+ for (const token of tokens) {
27
+ if (str.includes(token)) score += 3
28
+ }
29
+ }
30
+
31
+ for (const f of listFields) {
32
+ const str = String(f).toLowerCase()
33
+ if (str.includes(q)) score += 5
34
+ for (const token of tokens) {
35
+ if (str.includes(token)) score += 2
36
+ }
37
+ }
38
+
39
+ if (r.project && r.project.toLowerCase() === (project || "").toLowerCase()) score += 25
40
+ if (r.project && project && r.project.toLowerCase().includes(project.toLowerCase())) score += 12
41
+
42
+ const age = Date.now() - Date.parse(r.updated_at || r.created_at || 0)
43
+ if (!Number.isNaN(age)) score += Math.max(0, 10 - age / 604800000)
44
+ if (r.status === "active") score += 4
45
+ if (r.status === "closed") score -= 3
46
+
47
+ return score
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openhermes",
3
- "version": "1.13.1",
3
+ "version": "2.5.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",
@@ -20,7 +20,9 @@
20
20
  "task_id": { "type": "string" },
21
21
  "db_refs": { "type": "array", "items": { "type": "string" } },
22
22
  "file_refs": { "type": "array", "items": { "type": "string" } },
23
- "log_refs": { "type": "array", "items": { "type": "string" } }
23
+ "log_refs": { "type": "array", "items": { "type": "string" } },
24
+ "harness_root": { "type": "string" },
25
+ "project_root": { "type": "string" }
24
26
  }
25
27
  },
26
28
  "created_at": { "type": "string", "format": "date-time", "description": "ISO-8601 timestamp" },
@@ -56,6 +58,25 @@
56
58
  "provenance_ok": { "type": "boolean", "description": "All objects have valid provenance" },
57
59
  "duplicates_ok": { "type": "boolean", "description": "No duplicate IDs found" }
58
60
  }
61
+ },
62
+ "description": { "type": "string", "description": "Optional description" },
63
+ "environment_fingerprint": {
64
+ "type": "object",
65
+ "description": "System fingerprint at creation time",
66
+ "properties": {
67
+ "cwd": { "type": "string" },
68
+ "harness_root": { "type": "string" },
69
+ "project_root": { "type": "string" },
70
+ "project": { "type": "string" },
71
+ "session_id": { "type": "string" },
72
+ "os": { "type": "string" },
73
+ "release": { "type": "string" },
74
+ "arch": { "type": "string" },
75
+ "shell": { "type": "string" },
76
+ "provider": { "type": "string" },
77
+ "model": { "type": "string" },
78
+ "sha256": { "type": "string" }
79
+ }
59
80
  }
60
81
  }
61
82
  }
@@ -20,7 +20,9 @@
20
20
  "task_id": { "type": "string" },
21
21
  "db_refs": { "type": "array", "items": { "type": "string" } },
22
22
  "file_refs": { "type": "array", "items": { "type": "string" } },
23
- "log_refs": { "type": "array", "items": { "type": "string" } }
23
+ "log_refs": { "type": "array", "items": { "type": "string" } },
24
+ "harness_root": { "type": "string" },
25
+ "project_root": { "type": "string" }
24
26
  }
25
27
  },
26
28
  "created_at": { "type": "string", "format": "date-time", "description": "ISO-8601 timestamp" },
@@ -37,6 +39,25 @@
37
39
  "priority": { "type": "string", "enum": ["low", "medium", "high", "critical", "P0", "P1", "P2", "P3", "P4"], "description": "Priority level (low/medium/high/critical or P0-P4)" },
38
40
  "trigger": { "type": "string", "enum": ["audit", "mistake", "drift", "user", "manual"], "description": "What triggered creation of this item" },
39
41
  "evidence_refs": { "type": "array", "items": { "type": "string" }, "description": "References to evidence (audit IDs, mistake IDs, file paths)" },
40
- "done_when": { "type": "array", "items": { "type": "string" }, "description": "Acceptance criteria — concrete conditions for closure" }
42
+ "done_when": { "type": "array", "items": { "type": "string" }, "description": "Acceptance criteria — concrete conditions for closure" },
43
+ "description": { "type": "string", "description": "Optional description" },
44
+ "environment_fingerprint": {
45
+ "type": "object",
46
+ "description": "System fingerprint at creation time",
47
+ "properties": {
48
+ "cwd": { "type": "string" },
49
+ "harness_root": { "type": "string" },
50
+ "project_root": { "type": "string" },
51
+ "project": { "type": "string" },
52
+ "session_id": { "type": "string" },
53
+ "os": { "type": "string" },
54
+ "release": { "type": "string" },
55
+ "arch": { "type": "string" },
56
+ "shell": { "type": "string" },
57
+ "provider": { "type": "string" },
58
+ "model": { "type": "string" },
59
+ "sha256": { "type": "string" }
60
+ }
61
+ }
41
62
  }
42
63
  }