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
@@ -2,6 +2,21 @@ const HIGH_RISK = /(npm publish|publish package|deploy|production|git push|force
2
2
  const MEDIUM_RISK = /(migration|schema|dependency upgrade|lockfile|generated code|shared config|auth|permission|security)/i
3
3
  const REVERSIBLE = /(local|test|rename|format|refactor|temporary|fixture|internal|reversible)/i
4
4
 
5
+ function confidenceForDecision(input = {}) {
6
+ if (input.requiresUser === true && (input.destructive === true || input.externalSideEffect === true || input.explicitlyIrreversible === true)) return 0.99
7
+ if (input.reversible === true && input.risk === "low") return 0.96
8
+ if (input.risk === "medium") return 0.82
9
+ if (input.requiresUser === true) return 0.90
10
+ return 0.74
11
+ }
12
+
13
+ function confidenceBand(value) {
14
+ const score = Number(value || 0)
15
+ if (score >= 0.90) return "high"
16
+ if (score >= 0.72) return "medium"
17
+ return "low"
18
+ }
19
+
5
20
  export function classifyDecisionPolicy(text = "", facts = {}) {
6
21
  const value = String(text || "").trim()
7
22
  const explicitlyIrreversible = facts.irreversible === true
@@ -11,10 +26,33 @@ export function classifyDecisionPolicy(text = "", facts = {}) {
11
26
  const reversible = facts.reversible === true || (!destructive && REVERSIBLE.test(value))
12
27
  const risk = destructive || explicitlyIrreversible || externalSideEffect ? "high" : medium ? "medium" : "low"
13
28
  const requiresUser = risk === "high" || facts.productDecision === true
29
+ const autoResolvable = !requiresUser && (reversible || risk === "low")
30
+ const confidence = confidenceForDecision({
31
+ requiresUser,
32
+ destructive,
33
+ externalSideEffect,
34
+ explicitlyIrreversible,
35
+ reversible,
36
+ risk,
37
+ })
14
38
  return {
15
- schemaVersion: 1, risk, reversible, destructive, externalSideEffect, requiresUser,
16
- autoResolvable: !requiresUser && (reversible || risk === "low"),
39
+ schemaVersion: 1,
40
+ risk,
41
+ reversible,
42
+ destructive,
43
+ externalSideEffect,
44
+ requiresUser,
45
+ autoResolvable,
17
46
  reason: requiresUser ? "human-approval-required" : reversible ? "reversible-local-decision" : "bounded-engineering-decision",
47
+ decision: {
48
+ schemaVersion: 1,
49
+ kind: String(facts.decisionKind || "engineering-choice"),
50
+ source: "deterministic",
51
+ value: requiresUser ? "escalate-to-user" : autoResolvable ? "auto-resolve" : "bounded-review",
52
+ confidence,
53
+ confidenceBand: confidenceBand(confidence),
54
+ crossCheckRecommended: confidence < 0.72 || risk === "medium",
55
+ },
18
56
  }
19
57
  }
20
58
 
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto"
2
2
  import { existsSync } from "node:fs"
3
- import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"
3
+ import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises"
4
4
  import path from "node:path"
5
5
 
6
6
  const STORE_VERSION = 1
@@ -12,6 +12,82 @@ const AUTO_GC_EVERY_WRITES = 8
12
12
  const AUTO_GC_LARGE_ITEM_BYTES = 16 * 1024 * 1024
13
13
  const PUT_COUNTS = new Map()
14
14
 
15
+ const STORE_MUTATION_TAILS = new Map()
16
+ const STORE_LOCK_STALE_MS = 30_000
17
+ const STORE_LOCK_WAIT_MS = 40_000
18
+
19
+ function sleep(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms))
21
+ }
22
+
23
+ function storeLockPath(root) {
24
+ return path.join(evidenceStoreRoot(root), ".mutation.lock")
25
+ }
26
+
27
+ async function withCrossProcessStoreLock(root, fn) {
28
+ const lockDir = storeLockPath(root)
29
+ await mkdir(path.dirname(lockDir), { recursive: true })
30
+ const deadline = Date.now() + STORE_LOCK_WAIT_MS
31
+ let delayMs = 8
32
+ while (true) {
33
+ try {
34
+ await mkdir(lockDir)
35
+ break
36
+ } catch (error) {
37
+ if (error?.code !== "EEXIST") throw error
38
+ const info = await stat(lockDir).catch(() => null)
39
+ if (info && Date.now() - info.mtimeMs > STORE_LOCK_STALE_MS) {
40
+ const confirmed = await stat(lockDir).catch(() => null)
41
+ if (
42
+ confirmed &&
43
+ confirmed.ino === info.ino &&
44
+ confirmed.mtimeMs === info.mtimeMs
45
+ ) {
46
+ await rm(lockDir, { recursive: true, force: true }).catch(() => {})
47
+ continue
48
+ }
49
+ }
50
+ if (Date.now() >= deadline) throw new Error("Timed out waiting for evidence-store mutation lock")
51
+ await sleep(delayMs)
52
+ delayMs = Math.min(100, delayMs * 2)
53
+ }
54
+ }
55
+ try {
56
+ return await fn()
57
+ } finally {
58
+ await rm(lockDir, { recursive: true, force: true }).catch(() => {})
59
+ }
60
+ }
61
+
62
+ async function withStoreMutationLock(root, fn) {
63
+ const key = path.resolve(root)
64
+ const previous = STORE_MUTATION_TAILS.get(key) || Promise.resolve()
65
+ let release
66
+ const barrier = new Promise((resolve) => { release = resolve })
67
+ const tail = previous.catch(() => {}).then(() => barrier)
68
+ STORE_MUTATION_TAILS.set(key, tail)
69
+
70
+ await previous.catch(() => {})
71
+ try {
72
+ return await withCrossProcessStoreLock(root, fn)
73
+ } finally {
74
+ release()
75
+ if (STORE_MUTATION_TAILS.get(key) === tail) STORE_MUTATION_TAILS.delete(key)
76
+ }
77
+ }
78
+
79
+ async function atomicWrite(file, value, encoding) {
80
+ await mkdir(path.dirname(file), { recursive: true })
81
+ const temp = file + "." + process.pid + "." + Date.now() + ".tmp"
82
+ await writeFile(temp, value, encoding)
83
+ try {
84
+ await rename(temp, file)
85
+ } catch (error) {
86
+ await rm(temp, { force: true }).catch(() => {})
87
+ throw error
88
+ }
89
+ }
90
+
15
91
  function boundedNumber(value, fallback, min, max) {
16
92
  const parsed = Number(value)
17
93
  if (!Number.isFinite(parsed)) return fallback
@@ -104,42 +180,50 @@ export async function putEvidence(root, value, options = {}) {
104
180
  const encoded = toBytes(value, options)
105
181
  const hash = createHash("sha256").update(encoded.bytes).digest("hex")
106
182
  const target = evidencePaths(root, hash)
107
- await mkdir(target.dir, { recursive: true })
108
183
 
109
- const now = new Date().toISOString()
110
- const existing = existsSync(target.meta)
111
- ? JSON.parse(await readFile(target.meta, "utf8").catch(() => "{}"))
112
- : null
184
+ const mutation = await withStoreMutationLock(root, async () => {
185
+ await mkdir(target.dir, { recursive: true })
186
+ const now = new Date().toISOString()
187
+ const existing = existsSync(target.meta)
188
+ ? JSON.parse(await readFile(target.meta, "utf8").catch(() => "{}"))
189
+ : null
113
190
 
114
- if (!existsSync(target.data)) await writeFile(target.data, encoded.bytes)
191
+ if (!existsSync(target.data)) {
192
+ await atomicWrite(target.data, encoded.bytes)
193
+ }
115
194
 
116
- const preview = encoded.encoding === "utf8"
117
- ? encoded.bytes.toString("utf8", 0, Math.min(encoded.bytes.length, 600))
118
- : null
195
+ const preview = encoded.encoding === "utf8"
196
+ ? encoded.bytes.toString("utf8", 0, Math.min(encoded.bytes.length, 600))
197
+ : null
119
198
 
120
- const metadata = {
121
- schemaVersion: STORE_VERSION,
122
- ref: evidenceReference(hash),
123
- sha256: hash,
124
- bytes: encoded.bytes.length,
125
- encoding: encoded.encoding,
126
- mediaType: encoded.mediaType,
127
- kind: options.kind || existing?.kind || "tool-output",
128
- source: options.source || existing?.source || null,
129
- summary: options.summary || existing?.summary || null,
130
- createdAt: existing?.createdAt || now,
131
- lastSeenAt: now,
132
- preview,
133
- }
134
- await writeFile(target.meta, JSON.stringify(metadata, null, 2) + "\n", "utf8")
199
+ const metadata = {
200
+ schemaVersion: STORE_VERSION,
201
+ ref: evidenceReference(hash),
202
+ sha256: hash,
203
+ bytes: encoded.bytes.length,
204
+ encoding: encoded.encoding,
205
+ mediaType: encoded.mediaType,
206
+ kind: options.kind || existing?.kind || "tool-output",
207
+ source: options.source || existing?.source || null,
208
+ summary: options.summary || existing?.summary || null,
209
+ createdAt: existing?.createdAt || now,
210
+ lastSeenAt: now,
211
+ preview,
212
+ }
213
+ await atomicWrite(target.meta, JSON.stringify(metadata, null, 2) + "\n", "utf8")
135
214
 
136
- const writes = (PUT_COUNTS.get(root) || 0) + 1
137
- PUT_COUNTS.set(root, writes)
138
- if (writes % AUTO_GC_EVERY_WRITES === 0 || encoded.bytes.length >= AUTO_GC_LARGE_ITEM_BYTES) {
215
+ const writes = (PUT_COUNTS.get(root) || 0) + 1
216
+ PUT_COUNTS.set(root, writes)
217
+ return {
218
+ metadata,
219
+ shouldGc: writes % AUTO_GC_EVERY_WRITES === 0 || encoded.bytes.length >= AUTO_GC_LARGE_ITEM_BYTES,
220
+ }
221
+ })
222
+
223
+ if (mutation.shouldGc) {
139
224
  await gcEvidenceStore(root, defaultStoreLimits()).catch(() => null)
140
225
  }
141
-
142
- return metadata
226
+ return mutation.metadata
143
227
  }
144
228
 
145
229
  export async function getEvidence(root, ref, options = {}) {
@@ -275,7 +359,7 @@ export async function evidenceStoreStatus(root = process.cwd()) {
275
359
  }
276
360
  }
277
361
 
278
- export async function gcEvidenceStore(root = process.cwd(), options = {}) {
362
+ async function gcEvidenceStoreUnlocked(root = process.cwd(), options = {}) {
279
363
  root = path.resolve(root)
280
364
  const files = await metadataFiles(root)
281
365
  const defaults = defaultStoreLimits()
@@ -332,6 +416,12 @@ export async function gcEvidenceStore(root = process.cwd(), options = {}) {
332
416
  }
333
417
  }
334
418
 
419
+
420
+ export async function gcEvidenceStore(root = process.cwd(), options = {}) {
421
+ root = path.resolve(root)
422
+ return withStoreMutationLock(root, () => gcEvidenceStoreUnlocked(root, options))
423
+ }
424
+
335
425
  export async function evidenceExists(root, ref) {
336
426
  try {
337
427
  const target = evidencePaths(root, normalizeHash(ref))
@@ -0,0 +1,69 @@
1
+ import { accessSync, constants as fsConstants } from "node:fs"
2
+ import path from "node:path"
3
+
4
+ const DEFAULT_TTL_MS = 60_000
5
+ const commandProbeCache = new Map()
6
+
7
+ function executableFile(target) {
8
+ try {
9
+ accessSync(target, process.platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK)
10
+ return true
11
+ } catch {
12
+ return false
13
+ }
14
+ }
15
+
16
+ function probeKey(name) {
17
+ return [
18
+ name,
19
+ String(process.env.PATH || ""),
20
+ process.platform === "win32" ? String(process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD") : "",
21
+ ].join("\u0000")
22
+ }
23
+
24
+ export function commandExists(command, options = {}) {
25
+ if (!command) return false
26
+ const name = String(command).trim()
27
+ if (!name) return false
28
+
29
+ const ttlMs = Math.max(0, Number(options.ttlMs ?? DEFAULT_TTL_MS))
30
+ const key = probeKey(name)
31
+ const now = Date.now()
32
+ const cached = commandProbeCache.get(key)
33
+ if (cached && now - cached.checkedAt < ttlMs) return cached.available
34
+
35
+ const hasPath = path.isAbsolute(name) || name.includes("/") || name.includes("\\")
36
+ let available = false
37
+ if (hasPath) {
38
+ available = executableFile(path.resolve(name))
39
+ } else {
40
+ const dirs = String(process.env.PATH || "")
41
+ .split(path.delimiter)
42
+ .map((item) => item.trim().replace(/^"|"$/g, ""))
43
+ .filter(Boolean)
44
+ const extension = path.extname(name)
45
+ const suffixes = process.platform === "win32" && !extension
46
+ ? String(process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD")
47
+ .split(";")
48
+ .map((item) => item.trim())
49
+ .filter(Boolean)
50
+ : [""]
51
+
52
+ outer:
53
+ for (const dir of dirs) {
54
+ for (const suffix of suffixes) {
55
+ if (executableFile(path.join(dir, name + suffix))) {
56
+ available = true
57
+ break outer
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ commandProbeCache.set(key, { available, checkedAt: now })
64
+ return available
65
+ }
66
+
67
+ export function clearExecutableProbeCache() {
68
+ commandProbeCache.clear()
69
+ }
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto"
2
2
  import { spawnSync } from "node:child_process"
3
3
  import { lstatSync, readFileSync, readlinkSync } from "node:fs"
4
4
  import path from "node:path"
5
- import { sourceFacingPaths } from "./runtime-artifacts.mjs"
5
+ import { sourceFacingPaths, sourceGitPathspecs } from "./runtime-artifacts.mjs"
6
6
 
7
7
  const PHASE_HEADING = /^\s*(?:=+\s*)?PHASE\s+(\d+)\s*(?:[—–:-]+)\s*(.+?)\s*$/i
8
8
  const LOCAL_ENV_BASENAME = /^\.env(?:\.[^/\\]+)?$/i
@@ -187,7 +187,7 @@ export function captureInheritedDirtyState(root, options = {}) {
187
187
  ) {
188
188
  statusOutput = workspaceState.statusOutput
189
189
  } else {
190
- const probe = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], {
190
+ const probe = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...sourceGitPathspecs()], {
191
191
  cwd: resolved,
192
192
  encoding: "utf8",
193
193
  maxBuffer: 16 * 1024 * 1024,
@@ -3,6 +3,7 @@ import { buildSemanticIndex } from "./semantic-index.mjs"
3
3
 
4
4
  const DEFAULT_L0 = 256
5
5
  const DEFAULT_L1 = 4000
6
+ let HIERARCHY_RUNTIME_CACHE = new WeakMap()
6
7
  const STOP = new Set([
7
8
  "this","that","with","from","into","then","than","when","where","what","your","have","will",
8
9
  "task","code","file","files","fix","update","change","changes","được","các","cho","với","trong","này","một","những","không","theo",
@@ -84,6 +85,20 @@ export async function buildContextHierarchy(root = process.cwd(), options = {})
84
85
  maxFiles: options.maxFiles ?? 6000,
85
86
  maxDepth: options.maxDepth ?? 14,
86
87
  })
88
+ const l0Limit = Math.max(96, Number(options.l0Chars || DEFAULT_L0))
89
+ const l1Limit = Math.max(512, Number(options.l1Chars || DEFAULT_L1))
90
+ const indexObject = built?.index && typeof built.index === "object" ? built.index : null
91
+ const cacheKey = [root, l0Limit, l1Limit].join("\u0000")
92
+ const cachedBucket = indexObject ? HIERARCHY_RUNTIME_CACHE.get(indexObject) : null
93
+ const cached = cachedBucket?.get(cacheKey)
94
+ if (cached) {
95
+ return {
96
+ ...cached,
97
+ stats: built.stats,
98
+ runtimeCacheHit: true,
99
+ }
100
+ }
101
+
87
102
  const nodes = new Map()
88
103
  const children = new Map()
89
104
  const ensure = (dir) => {
@@ -122,19 +137,27 @@ export async function buildContextHierarchy(root = process.cwd(), options = {})
122
137
  .map((node) => buildNodeSummary(node, children, options))
123
138
  .sort((a, b) => a.path.localeCompare(b.path))
124
139
 
125
- return {
140
+ const result = {
126
141
  schemaVersion: 1,
127
142
  kind: "ues-context-hierarchy",
128
143
  root,
129
144
  generatedAt: new Date().toISOString(),
130
145
  levels: {
131
- L0: { maxChars: Math.max(96, Number(options.l0Chars || DEFAULT_L0)), purpose: "routing abstract" },
132
- L1: { maxChars: Math.max(512, Number(options.l1Chars || DEFAULT_L1)), purpose: "scope overview" },
146
+ L0: { maxChars: l0Limit, purpose: "routing abstract" },
147
+ L1: { maxChars: l1Limit, purpose: "scope overview" },
133
148
  L2: { purpose: "source excerpts loaded on demand by the context manifest" },
134
149
  },
135
150
  nodes: summaries,
136
151
  stats: built.stats,
152
+ runtimeCacheHit: false,
137
153
  }
154
+
155
+ if (indexObject) {
156
+ const bucket = cachedBucket || new Map()
157
+ bucket.set(cacheKey, result)
158
+ if (!cachedBucket) HIERARCHY_RUNTIME_CACHE.set(indexObject, bucket)
159
+ }
160
+ return result
138
161
  }
139
162
 
140
163
  function containsFile(scope, file) {
@@ -211,5 +234,10 @@ export async function queryContextHierarchy(root, query, options = {}) {
211
234
  levels: hierarchy.levels,
212
235
  scopes,
213
236
  stats: hierarchy.stats,
237
+ runtimeCacheHit: hierarchy.runtimeCacheHit === true,
214
238
  }
239
+ }
240
+
241
+ export function clearContextHierarchyRuntimeCache() {
242
+ HIERARCHY_RUNTIME_CACHE = new WeakMap()
215
243
  }