opencode-agent-skill 15.1.0 → 15.1.2

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 (43) hide show
  1. package/README.md +5 -5
  2. package/docs/V15-MANAGED-RUNTIME.md +18 -0
  3. package/global-config/agents/verifier.md +6 -2
  4. package/lib/affected-tests.mjs +28 -7
  5. package/lib/capability-fabric.mjs +107 -48
  6. package/lib/code-intelligence/index.mjs +50 -24
  7. package/lib/code-intelligence/lsp-pool.mjs +831 -0
  8. package/lib/code-intelligence/lsp-provider.mjs +197 -20
  9. package/lib/compaction-resume-guard.mjs +76 -4
  10. package/lib/completion-auditor.mjs +20 -0
  11. package/lib/context-manifest.mjs +88 -36
  12. package/lib/decision-policy.mjs +40 -2
  13. package/lib/evidence-store.mjs +121 -31
  14. package/lib/executable-probe.mjs +69 -0
  15. package/lib/execution-contract.mjs +2 -2
  16. package/lib/hierarchical-context.mjs +31 -3
  17. package/lib/learning-engine.mjs +164 -65
  18. package/lib/memory-engine.mjs +177 -95
  19. package/lib/model-config.mjs +86 -7
  20. package/lib/performance-fabric.mjs +92 -0
  21. package/lib/permission-policy.mjs +232 -0
  22. package/lib/pi-rpc-pool.mjs +220 -24
  23. package/lib/provider-recovery.mjs +51 -0
  24. package/lib/repo-graph.mjs +40 -7
  25. package/lib/repo-inspect.mjs +3 -2
  26. package/lib/runtime-events.mjs +31 -10
  27. package/lib/semantic-index.mjs +69 -21
  28. package/lib/skill-compiler.mjs +84 -18
  29. package/lib/task-engine.mjs +28 -9
  30. package/lib/task-policy.mjs +80 -7
  31. package/lib/trajectory.mjs +25 -1
  32. package/lib/untrusted-output.mjs +106 -0
  33. package/lib/workspace-fingerprint.mjs +17 -9
  34. package/lib/workspace-hygiene.mjs +2 -2
  35. package/lib/worktree-sandbox.mjs +14 -8
  36. package/package.json +8 -6
  37. package/pi/extensions/ues-child-runtime.ts +116 -5
  38. package/pi/extensions/ues.ts +780 -81
  39. package/scripts/benchmark-lsp-pool.mjs +128 -0
  40. package/scripts/check-release-consistency.mjs +16 -4
  41. package/scripts/check-source-integrity.mjs +118 -6
  42. package/scripts/run-test-suite.mjs +143 -0
  43. package/scripts/smoke-package-closure.mjs +4 -0
@@ -1,11 +1,88 @@
1
1
  import { existsSync } from "node:fs"
2
- import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"
2
+ import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises"
3
3
  import { createHash } from "node:crypto"
4
4
  import path from "node:path"
5
5
  import { pairedBenchmarkConfidence } from "./benchmark-confidence.mjs"
6
6
 
7
7
  const LEARNING_DIR = ".ues-learning"
8
8
  const STATE_FILE = "LEARNINGS.json"
9
+ const LEARNING_WRITE_TAILS = new Map()
10
+ const LEARNING_LOCK_STALE_MS = 15_000
11
+ const LEARNING_LOCK_WAIT_MS = 20_000
12
+
13
+ function sleep(ms) {
14
+ return new Promise((resolve) => setTimeout(resolve, ms))
15
+ }
16
+
17
+ function learningLockPath(root) {
18
+ return path.join(path.resolve(root), LEARNING_DIR, ".learning-write.lock")
19
+ }
20
+
21
+ async function withCrossProcessLearningLock(root, fn) {
22
+ const lockDir = learningLockPath(root)
23
+ await mkdir(path.dirname(lockDir), { recursive: true })
24
+ const deadline = Date.now() + LEARNING_LOCK_WAIT_MS
25
+ let delayMs = 8
26
+
27
+ while (true) {
28
+ try {
29
+ await mkdir(lockDir)
30
+ break
31
+ } catch (error) {
32
+ if (error?.code !== "EEXIST") throw error
33
+ const info = await stat(lockDir).catch(() => null)
34
+ if (info && Date.now() - info.mtimeMs > LEARNING_LOCK_STALE_MS) {
35
+ const confirmed = await stat(lockDir).catch(() => null)
36
+ if (
37
+ confirmed &&
38
+ confirmed.ino === info.ino &&
39
+ confirmed.mtimeMs === info.mtimeMs
40
+ ) {
41
+ await rm(lockDir, { recursive: true, force: true }).catch(() => {})
42
+ continue
43
+ }
44
+ }
45
+ if (Date.now() >= deadline) throw new Error("Timed out waiting for learning write lock")
46
+ await sleep(delayMs)
47
+ delayMs = Math.min(100, delayMs * 2)
48
+ }
49
+ }
50
+
51
+ try {
52
+ return await fn()
53
+ } finally {
54
+ await rm(lockDir, { recursive: true, force: true }).catch(() => {})
55
+ }
56
+ }
57
+
58
+ async function withLearningWriteLock(root, fn) {
59
+ const key = path.resolve(root)
60
+ const previous = LEARNING_WRITE_TAILS.get(key) || Promise.resolve()
61
+ let release
62
+ const barrier = new Promise((resolve) => { release = resolve })
63
+ const tail = previous.catch(() => {}).then(() => barrier)
64
+ LEARNING_WRITE_TAILS.set(key, tail)
65
+
66
+ await previous.catch(() => {})
67
+ try {
68
+ return await withCrossProcessLearningLock(root, fn)
69
+ } finally {
70
+ release()
71
+ if (LEARNING_WRITE_TAILS.get(key) === tail) LEARNING_WRITE_TAILS.delete(key)
72
+ }
73
+ }
74
+
75
+ async function atomicJson(file, value) {
76
+ await mkdir(path.dirname(file), { recursive: true })
77
+ const temp = file + "." + process.pid + "." + Date.now() + ".tmp"
78
+ await writeFile(temp, JSON.stringify(value, null, 2) + "\n", "utf8")
79
+ try {
80
+ await rename(temp, file)
81
+ } catch (error) {
82
+ await rm(temp, { force: true }).catch(() => {})
83
+ throw error
84
+ }
85
+ }
9
86
 
10
87
  function idFor(type, key) {
11
88
  return createHash("sha256").update(type + ":" + key).digest("hex").slice(0, 16)
@@ -27,6 +104,22 @@ async function walkJson(dir, limit = 500) {
27
104
  return files
28
105
  }
29
106
 
107
+ async function mapLimit(items, limit, worker) {
108
+ if (!items.length) return []
109
+ const output = new Array(items.length)
110
+ let cursor = 0
111
+ const width = Math.min(items.length, Math.max(1, Number(limit || 1)))
112
+ const runners = Array.from({ length: width }, async () => {
113
+ while (true) {
114
+ const index = cursor++
115
+ if (index >= items.length) return
116
+ output[index] = await worker(items[index], index)
117
+ }
118
+ })
119
+ await Promise.all(runners)
120
+ return output
121
+ }
122
+
30
123
  function failureReasons(item) {
31
124
  const reasons = []
32
125
  if (item.timedOut) reasons.push("hard-timeout")
@@ -71,15 +164,15 @@ function clampRate(value) {
71
164
 
72
165
  export async function analyzeEvalTraces(evalDir) {
73
166
  const files = await walkJson(path.resolve(evalDir))
74
- const results = []
75
- for (const file of files) {
167
+ const batches = await mapLimit(files, 12, async (file) => {
76
168
  try {
77
169
  const payload = JSON.parse(await readFile(file, "utf8"))
78
- for (const item of payload.results || []) {
79
- results.push({ ...item, source: file })
80
- }
81
- } catch {}
82
- }
170
+ return (payload.results || []).map((item) => ({ ...item, source: file }))
171
+ } catch {
172
+ return []
173
+ }
174
+ })
175
+ const results = batches.flat()
83
176
 
84
177
  const reasonCounters = new Map()
85
178
  const clusters = new Map()
@@ -186,48 +279,49 @@ export async function readLearningState(root) {
186
279
  }
187
280
 
188
281
  export async function saveLearningAnalysis(root, analysis) {
189
- const dir = path.dirname(learningFile(root))
190
- await mkdir(dir, { recursive: true })
191
- const current = await readLearningState(root)
192
- const acceptedIDs = new Set(current.accepted.map((item) => item.id))
193
- const existing = new Map(current.proposals.map((item) => [item.id, item]))
194
- const proposals = analysis.proposals
195
- .filter((item) => !acceptedIDs.has(item.id))
196
- .map((item) => ({
197
- ...item,
198
- firstProposedAt: existing.get(item.id)?.firstProposedAt || new Date().toISOString(),
199
- lastAnalyzedAt: analysis.analyzedAt,
200
- }))
201
- const next = {
202
- schemaVersion: 2,
203
- updatedAt: new Date().toISOString(),
204
- proposals,
205
- accepted: current.accepted,
206
- }
207
- await writeFile(learningFile(root), JSON.stringify(next, null, 2) + "\n", "utf8")
208
- return next
282
+ return withLearningWriteLock(root, async () => {
283
+ const current = await readLearningState(root)
284
+ const acceptedIDs = new Set(current.accepted.map((item) => item.id))
285
+ const existing = new Map(current.proposals.map((item) => [item.id, item]))
286
+ const proposals = analysis.proposals
287
+ .filter((item) => !acceptedIDs.has(item.id))
288
+ .map((item) => ({
289
+ ...item,
290
+ firstProposedAt: existing.get(item.id)?.firstProposedAt || new Date().toISOString(),
291
+ lastAnalyzedAt: analysis.analyzedAt,
292
+ }))
293
+ const next = {
294
+ schemaVersion: 2,
295
+ updatedAt: new Date().toISOString(),
296
+ proposals,
297
+ accepted: current.accepted,
298
+ }
299
+ await atomicJson(learningFile(root), next)
300
+ return next
301
+ })
209
302
  }
210
303
 
211
304
  // Backward-compatible manual acceptance. Shadow-required proposals remain excluded
212
305
  // from retrieval until they are promoted with measured benchmark improvement.
213
306
  export async function acceptLearning(root, id) {
214
- const current = await readLearningState(root)
215
- const item = current.proposals.find((entry) => entry.id === id)
216
- if (!item) throw new Error("unknown learning proposal: " + id)
217
- const accepted = {
218
- ...item,
219
- status: item.shadowRequired ? "accepted-awaiting-shadow" : "accepted",
220
- acceptedAt: new Date().toISOString(),
221
- }
222
- const next = {
223
- schemaVersion: 2,
224
- updatedAt: accepted.acceptedAt,
225
- proposals: current.proposals.filter((entry) => entry.id !== id),
226
- accepted: [...current.accepted.filter((entry) => entry.id !== id), accepted],
227
- }
228
- await mkdir(path.dirname(learningFile(root)), { recursive: true })
229
- await writeFile(learningFile(root), JSON.stringify(next, null, 2) + "\n", "utf8")
230
- return accepted
307
+ return withLearningWriteLock(root, async () => {
308
+ const current = await readLearningState(root)
309
+ const item = current.proposals.find((entry) => entry.id === id)
310
+ if (!item) throw new Error("unknown learning proposal: " + id)
311
+ const accepted = {
312
+ ...item,
313
+ status: item.shadowRequired ? "accepted-awaiting-shadow" : "accepted",
314
+ acceptedAt: new Date().toISOString(),
315
+ }
316
+ const next = {
317
+ schemaVersion: 2,
318
+ updatedAt: accepted.acceptedAt,
319
+ proposals: current.proposals.filter((entry) => entry.id !== id),
320
+ accepted: [...current.accepted.filter((entry) => entry.id !== id), accepted],
321
+ }
322
+ await atomicJson(learningFile(root), next)
323
+ return accepted
324
+ })
231
325
  }
232
326
 
233
327
  async function benchmarkValidationFromArtifact(root, reportPath) {
@@ -295,32 +389,37 @@ async function benchmarkValidationFromArtifact(root, reportPath) {
295
389
  }
296
390
 
297
391
  export async function promoteLearning(root, id, validation = {}) {
298
- const current = await readLearningState(root)
299
- const item = current.accepted.find((entry) => entry.id === id)
300
- if (!item) {
301
- if (current.proposals.some((entry) => entry.id === id)) {
392
+ const initial = await readLearningState(root)
393
+ const initialItem = initial.accepted.find((entry) => entry.id === id)
394
+ if (!initialItem) {
395
+ if (initial.proposals.some((entry) => entry.id === id)) {
302
396
  throw new Error("learning proposal must be explicitly accepted before shadow promotion: " + id)
303
397
  }
304
398
  throw new Error("unknown learning proposal: " + id)
305
399
  }
306
400
 
307
401
  const shadowValidation = await benchmarkValidationFromArtifact(root, validation.report)
308
- const promotedAt = new Date().toISOString()
309
- const promoted = {
310
- ...item,
311
- status: "promoted",
312
- promotedAt,
313
- shadowValidation,
314
- }
315
- const next = {
316
- schemaVersion: 2,
317
- updatedAt: promotedAt,
318
- proposals: current.proposals.filter((entry) => entry.id !== id),
319
- accepted: [...current.accepted.filter((entry) => entry.id !== id), promoted],
320
- }
321
- await mkdir(path.dirname(learningFile(root)), { recursive: true })
322
- await writeFile(learningFile(root), JSON.stringify(next, null, 2) + "\n", "utf8")
323
- return promoted
402
+ return withLearningWriteLock(root, async () => {
403
+ const current = await readLearningState(root)
404
+ const item = current.accepted.find((entry) => entry.id === id)
405
+ if (!item) throw new Error("learning proposal changed during promotion: " + id)
406
+
407
+ const promotedAt = new Date().toISOString()
408
+ const promoted = {
409
+ ...item,
410
+ status: "promoted",
411
+ promotedAt,
412
+ shadowValidation,
413
+ }
414
+ const next = {
415
+ schemaVersion: 2,
416
+ updatedAt: promotedAt,
417
+ proposals: current.proposals.filter((entry) => entry.id !== id),
418
+ accepted: [...current.accepted.filter((entry) => entry.id !== id), promoted],
419
+ }
420
+ await atomicJson(learningFile(root), next)
421
+ return promoted
422
+ })
324
423
  }
325
424
 
326
425
  function retrievalTerms(text) {
@@ -1,10 +1,12 @@
1
1
  import { createHash } from "node:crypto"
2
2
  import { existsSync } from "node:fs"
3
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
3
+ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"
4
4
  import { spawnSync } from "node:child_process"
5
5
  import path from "node:path"
6
6
  import { evidenceExists, putEvidence } from "./evidence-store.mjs"
7
7
  import { buildRepoGraphCached } from "./repo-graph.mjs"
8
+ import { sourceFacingPaths, sourceGitPathspecs } from "./runtime-artifacts.mjs"
9
+ import { workspaceStatusEntries } from "./workspace-fingerprint.mjs"
8
10
 
9
11
  const MEMORY_DIR = ".ues-memory"
10
12
  const MEMORY_FILE = "MEMORY.json"
@@ -16,6 +18,72 @@ const STOP = new Set([
16
18
  "file","files","change","changes","được","các","cho","với","trong","này","một","những","không","theo",
17
19
  ])
18
20
 
21
+ const MEMORY_WRITE_TAILS = new Map()
22
+ const MEMORY_LOCK_STALE_MS = 15_000
23
+ const MEMORY_LOCK_WAIT_MS = 20_000
24
+
25
+ function sleep(ms) {
26
+ return new Promise((resolve) => setTimeout(resolve, ms))
27
+ }
28
+
29
+ function memoryLockPath(root) {
30
+ return path.join(path.resolve(root), MEMORY_DIR, ".memory-write.lock")
31
+ }
32
+
33
+ async function withCrossProcessMemoryLock(root, fn) {
34
+ const lockDir = memoryLockPath(root)
35
+ await mkdir(path.dirname(lockDir), { recursive: true })
36
+ const deadline = Date.now() + MEMORY_LOCK_WAIT_MS
37
+ let delayMs = 8
38
+
39
+ while (true) {
40
+ try {
41
+ await mkdir(lockDir)
42
+ break
43
+ } catch (error) {
44
+ if (error?.code !== "EEXIST") throw error
45
+ const info = await stat(lockDir).catch(() => null)
46
+ if (info && Date.now() - info.mtimeMs > MEMORY_LOCK_STALE_MS) {
47
+ const confirmed = await stat(lockDir).catch(() => null)
48
+ if (
49
+ confirmed &&
50
+ confirmed.ino === info.ino &&
51
+ confirmed.mtimeMs === info.mtimeMs
52
+ ) {
53
+ await rm(lockDir, { recursive: true, force: true }).catch(() => {})
54
+ continue
55
+ }
56
+ }
57
+ if (Date.now() >= deadline) throw new Error("Timed out waiting for memory write lock")
58
+ await sleep(delayMs)
59
+ delayMs = Math.min(100, delayMs * 2)
60
+ }
61
+ }
62
+
63
+ try {
64
+ return await fn()
65
+ } finally {
66
+ await rm(lockDir, { recursive: true, force: true }).catch(() => {})
67
+ }
68
+ }
69
+
70
+ async function withMemoryWriteLock(root, fn) {
71
+ const key = path.resolve(root)
72
+ const previous = MEMORY_WRITE_TAILS.get(key) || Promise.resolve()
73
+ let release
74
+ const barrier = new Promise((resolve) => { release = resolve })
75
+ const tail = previous.catch(() => {}).then(() => barrier)
76
+ MEMORY_WRITE_TAILS.set(key, tail)
77
+
78
+ await previous.catch(() => {})
79
+ try {
80
+ return await withCrossProcessMemoryLock(root, fn)
81
+ } finally {
82
+ release()
83
+ if (MEMORY_WRITE_TAILS.get(key) === tail) MEMORY_WRITE_TAILS.delete(key)
84
+ }
85
+ }
86
+
19
87
  function now() {
20
88
  return new Date().toISOString()
21
89
  }
@@ -118,55 +186,58 @@ export async function proposeMemory(root, input = {}) {
118
186
  if (!content) throw new Error("memory content is required")
119
187
  const type = VALID_TYPES.has(input.type) ? input.type : "episodic"
120
188
  const scope = VALID_SCOPES.has(input.scope) ? input.scope : "project"
121
- const state = await readMemoryState(root)
122
189
  const id = idFor({ ...input, type, scope, content })
123
- const timestamp = now()
124
- const existing = state.memories.find((item) => item.id === id) ||
125
- state.memories.find((item) => sameMemoryIdentity(item, { ...input, type, scope, content }))
126
- if (existing) {
127
- const reinforced = {
128
- ...existing,
129
- lastSeenAt: timestamp,
130
- reinforcementCount: Number(existing.reinforcementCount || 1) + 1,
131
- evidenceRefs: uniq([...(existing.evidenceRefs || []), ...(input.evidenceRefs || [])]),
132
- files: uniq([...(existing.files || []), ...(input.files || []).map(normalizeFile)]),
133
- dependencies: uniq([...(existing.dependencies || []), ...(input.dependencies || []).map(normalizeFile)]),
134
- tags: uniq([...(existing.tags || []), ...(input.tags || [])]),
135
- confidence: Math.max(clamp(existing.confidence), clamp(input.confidence, existing.confidence ?? 0.7)),
136
- taskClass: input.taskClass || existing.taskClass || null,
137
- expiresAt: input.expiresAt || existing.expiresAt || null,
190
+
191
+ return withMemoryWriteLock(root, async () => {
192
+ const state = await readMemoryState(root)
193
+ const timestamp = now()
194
+ const existing = state.memories.find((item) => item.id === id) ||
195
+ state.memories.find((item) => sameMemoryIdentity(item, { ...input, type, scope, content }))
196
+ if (existing) {
197
+ const reinforced = {
198
+ ...existing,
199
+ lastSeenAt: timestamp,
200
+ reinforcementCount: Number(existing.reinforcementCount || 1) + 1,
201
+ evidenceRefs: uniq([...(existing.evidenceRefs || []), ...(input.evidenceRefs || [])]),
202
+ files: uniq([...(existing.files || []), ...(input.files || []).map(normalizeFile)]),
203
+ dependencies: uniq([...(existing.dependencies || []), ...(input.dependencies || []).map(normalizeFile)]),
204
+ tags: uniq([...(existing.tags || []), ...(input.tags || [])]),
205
+ confidence: Math.max(clamp(existing.confidence), clamp(input.confidence, existing.confidence ?? 0.7)),
206
+ taskClass: input.taskClass || existing.taskClass || null,
207
+ expiresAt: input.expiresAt || existing.expiresAt || null,
208
+ }
209
+ await writeMemoryState(root, state.memories.map((item) => item.id === existing.id ? reinforced : item))
210
+ return reinforced
138
211
  }
139
- await writeMemoryState(root, state.memories.map((item) => item.id === id ? reinforced : item))
140
- return reinforced
141
- }
142
212
 
143
- const memory = {
144
- id,
145
- key: input.key || null,
146
- type,
147
- scope,
148
- content,
149
- status: "candidate",
150
- confidence: clamp(input.confidence),
151
- evidenceRefs: uniq(input.evidenceRefs || []),
152
- files: uniq((input.files || []).map(normalizeFile)),
153
- dependencies: uniq((input.dependencies || []).map(normalizeFile)),
154
- tags: uniq(input.tags || []),
155
- sourceTask: input.sourceTask || null,
156
- sourceCommit: input.sourceCommit || null,
157
- taskClass: input.taskClass || null,
158
- expiresAt: input.expiresAt || null,
159
- createdAt: timestamp,
160
- lastSeenAt: timestamp,
161
- lastUsedAt: null,
162
- useCount: 0,
163
- reinforcementCount: 1,
164
- verifiedAt: null,
165
- verifier: null,
166
- supersededBy: null,
167
- }
168
- await writeMemoryState(root, [...state.memories, memory])
169
- return memory
213
+ const memory = {
214
+ id,
215
+ key: input.key || null,
216
+ type,
217
+ scope,
218
+ content,
219
+ status: "candidate",
220
+ confidence: clamp(input.confidence),
221
+ evidenceRefs: uniq(input.evidenceRefs || []),
222
+ files: uniq((input.files || []).map(normalizeFile)),
223
+ dependencies: uniq((input.dependencies || []).map(normalizeFile)),
224
+ tags: uniq(input.tags || []),
225
+ sourceTask: input.sourceTask || null,
226
+ sourceCommit: input.sourceCommit || null,
227
+ taskClass: input.taskClass || null,
228
+ expiresAt: input.expiresAt || null,
229
+ createdAt: timestamp,
230
+ lastSeenAt: timestamp,
231
+ lastUsedAt: null,
232
+ useCount: 0,
233
+ reinforcementCount: 1,
234
+ verifiedAt: null,
235
+ verifier: null,
236
+ supersededBy: null,
237
+ }
238
+ await writeMemoryState(root, [...state.memories, memory])
239
+ return memory
240
+ })
170
241
  }
171
242
 
172
243
  export async function verifyMemory(root, id, verification = {}) {
@@ -174,47 +245,52 @@ export async function verifyMemory(root, id, verification = {}) {
174
245
  throw new Error("memory verification requires verdict PASS")
175
246
  }
176
247
  if (!verification.verifier) throw new Error("memory verification requires an independent verifier identity")
177
- const state = await readMemoryState(root)
178
- const current = state.memories.find((item) => item.id === id)
179
- if (!current) throw new Error("unknown memory: " + id)
180
- if (current.status === "superseded") throw new Error("cannot verify a superseded memory")
181
-
182
- const refs = uniq([...(current.evidenceRefs || []), ...(verification.evidenceRefs || [])])
183
- if (!refs.length) throw new Error("memory verification requires at least one durable evidence reference")
184
- for (const ref of refs) {
185
- if (!(await evidenceExists(root, ref))) throw new Error("memory evidence reference is missing: " + ref)
186
- }
187
248
 
188
- const verified = {
189
- ...current,
190
- status: "verified",
191
- evidenceRefs: refs,
192
- verifier: String(verification.verifier),
193
- verifiedAt: now(),
194
- lastSeenAt: now(),
195
- confidence: Math.max(clamp(current.confidence), clamp(verification.confidence, current.confidence ?? 0.7)),
196
- }
197
- await writeMemoryState(root, state.memories.map((item) => item.id === id ? verified : item))
198
- return verified
249
+ return withMemoryWriteLock(root, async () => {
250
+ const state = await readMemoryState(root)
251
+ const current = state.memories.find((item) => item.id === id)
252
+ if (!current) throw new Error("unknown memory: " + id)
253
+ if (current.status === "superseded") throw new Error("cannot verify a superseded memory")
254
+
255
+ const refs = uniq([...(current.evidenceRefs || []), ...(verification.evidenceRefs || [])])
256
+ if (!refs.length) throw new Error("memory verification requires at least one durable evidence reference")
257
+ for (const ref of refs) {
258
+ if (!(await evidenceExists(root, ref))) throw new Error("memory evidence reference is missing: " + ref)
259
+ }
260
+
261
+ const verified = {
262
+ ...current,
263
+ status: "verified",
264
+ evidenceRefs: refs,
265
+ verifier: String(verification.verifier),
266
+ verifiedAt: now(),
267
+ lastSeenAt: now(),
268
+ confidence: Math.max(clamp(current.confidence), clamp(verification.confidence, current.confidence ?? 0.7)),
269
+ }
270
+ await writeMemoryState(root, state.memories.map((item) => item.id === id ? verified : item))
271
+ return verified
272
+ })
199
273
  }
200
274
 
201
275
  export async function supersedeMemory(root, id, replacementId) {
202
276
  if (!replacementId || replacementId === id) throw new Error("replacement memory id must be different")
203
- const state = await readMemoryState(root)
204
- const current = state.memories.find((item) => item.id === id)
205
- const replacement = state.memories.find((item) => item.id === replacementId)
206
- if (!current) throw new Error("unknown memory: " + id)
207
- if (!replacement) throw new Error("unknown replacement memory: " + replacementId)
208
- if (replacement.status !== "verified") throw new Error("replacement memory must be verified before supersession")
209
- const timestamp = now()
210
- const superseded = {
211
- ...current,
212
- status: "superseded",
213
- supersededBy: replacementId,
214
- lastSeenAt: timestamp,
215
- }
216
- await writeMemoryState(root, state.memories.map((item) => item.id === id ? superseded : item))
217
- return superseded
277
+ return withMemoryWriteLock(root, async () => {
278
+ const state = await readMemoryState(root)
279
+ const current = state.memories.find((item) => item.id === id)
280
+ const replacement = state.memories.find((item) => item.id === replacementId)
281
+ if (!current) throw new Error("unknown memory: " + id)
282
+ if (!replacement) throw new Error("unknown replacement memory: " + replacementId)
283
+ if (replacement.status !== "verified") throw new Error("replacement memory must be verified before supersession")
284
+ const timestamp = now()
285
+ const superseded = {
286
+ ...current,
287
+ status: "superseded",
288
+ supersededBy: replacementId,
289
+ lastSeenAt: timestamp,
290
+ }
291
+ await writeMemoryState(root, state.memories.map((item) => item.id === id ? superseded : item))
292
+ return superseded
293
+ })
218
294
  }
219
295
 
220
296
  function tokenVector(value, dims = 96) {
@@ -550,9 +626,12 @@ export async function retrieveMemories(root, query, options = {}) {
550
626
  if (options.touch === true && results.length) {
551
627
  const used = new Set(results.map((item) => item.id))
552
628
  const timestamp = now()
553
- await writeMemoryState(root, state.memories.map((item) => used.has(item.id)
554
- ? { ...item, lastUsedAt: timestamp, useCount: Number(item.useCount || 0) + 1 }
555
- : item))
629
+ await withMemoryWriteLock(root, async () => {
630
+ const latest = await readMemoryState(root)
631
+ await writeMemoryState(root, latest.memories.map((item) => used.has(item.id)
632
+ ? { ...item, lastUsedAt: timestamp, useCount: Number(item.useCount || 0) + 1 }
633
+ : item))
634
+ })
556
635
  }
557
636
  return {
558
637
  schemaVersion: SCHEMA_VERSION,
@@ -594,21 +673,24 @@ export async function memoryStatus(root = process.cwd()) {
594
673
  }
595
674
 
596
675
  function changedFiles(root) {
597
- const result = spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], {
676
+ const result = spawnSync("git", [
677
+ "status",
678
+ "--porcelain=v1",
679
+ "-z",
680
+ "--untracked-files=all",
681
+ "--",
682
+ ...sourceGitPathspecs(),
683
+ ], {
598
684
  cwd: path.resolve(root),
599
685
  encoding: "utf8",
600
686
  windowsHide: true,
601
687
  maxBuffer: 4 * 1024 * 1024,
602
688
  })
603
689
  if (result.status !== 0) return []
604
- return uniq(
605
- String(result.stdout || "")
606
- .split(/\r?\n/)
607
- .filter(Boolean)
608
- .map((line) => line.slice(3).trim())
609
- .map((value) => value.includes(" -> ") ? value.split(" -> ").at(-1) : value)
610
- .map(normalizeFile),
611
- ).slice(0, 80)
690
+ const changed = workspaceStatusEntries(String(result.stdout || ""))
691
+ .filter((entry) => entry.renameSource !== true)
692
+ .map((entry) => normalizeFile(entry.file))
693
+ return uniq(sourceFacingPaths(changed)).slice(0, 80)
612
694
  }
613
695
 
614
696
  function compactVerification(value, limit = 1200) {