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,5 +1,5 @@
1
1
  import { existsSync } from "node:fs"
2
- import { mkdir, readFile, writeFile } from "node:fs/promises"
2
+ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"
3
3
  import path from "node:path"
4
4
  import { defaultModelPolicy, resolveModel } from "./model-policy.mjs"
5
5
  import { normalizeCapabilityProfile } from "./capability-registry.mjs"
@@ -7,6 +7,70 @@ import { normalizePerformanceHistory, recordPerformanceOutcome } from "./model-p
7
7
 
8
8
  const TIERS = new Set(["light", "standard", "heavy"])
9
9
 
10
+ const MODEL_POLICY_WRITE_TAILS = new Map()
11
+ const MODEL_POLICY_LOCK_STALE_MS = 15_000
12
+ const MODEL_POLICY_LOCK_WAIT_MS = 20_000
13
+
14
+ function sleep(ms) {
15
+ return new Promise((resolve) => setTimeout(resolve, ms))
16
+ }
17
+
18
+ function modelPolicyLockDir(configDir) {
19
+ return modelPolicyFile(configDir) + ".lock"
20
+ }
21
+
22
+ async function withCrossProcessModelPolicyLock(configDir, fn) {
23
+ const lockDir = modelPolicyLockDir(configDir)
24
+ await mkdir(path.dirname(lockDir), { recursive: true })
25
+ const deadline = Date.now() + MODEL_POLICY_LOCK_WAIT_MS
26
+ let delayMs = 8
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 > MODEL_POLICY_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 model-policy write lock")
46
+ await sleep(delayMs)
47
+ delayMs = Math.min(100, delayMs * 2)
48
+ }
49
+ }
50
+ try {
51
+ return await fn()
52
+ } finally {
53
+ await rm(lockDir, { recursive: true, force: true }).catch(() => {})
54
+ }
55
+ }
56
+
57
+ async function withModelPolicyWriteLock(configDir, fn) {
58
+ const key = path.resolve(configDir)
59
+ const previous = MODEL_POLICY_WRITE_TAILS.get(key) || Promise.resolve()
60
+ let release
61
+ const barrier = new Promise((resolve) => { release = resolve })
62
+ const tail = previous.catch(() => {}).then(() => barrier)
63
+ MODEL_POLICY_WRITE_TAILS.set(key, tail)
64
+
65
+ await previous.catch(() => {})
66
+ try {
67
+ return await withCrossProcessModelPolicyLock(configDir, fn)
68
+ } finally {
69
+ release()
70
+ if (MODEL_POLICY_WRITE_TAILS.get(key) === tail) MODEL_POLICY_WRITE_TAILS.delete(key)
71
+ }
72
+ }
73
+
10
74
  function validModelID(value) {
11
75
  return typeof value === "string" && /^[^\s/]+\/[^\s#]+(?:#[^\s]+)?$/.test(value)
12
76
  }
@@ -61,8 +125,7 @@ export async function readModelPolicy(configDir) {
61
125
  }
62
126
  }
63
127
 
64
- export async function writeModelPolicy(configDir, patch = {}) {
65
- const current = await readModelPolicy(configDir)
128
+ async function persistModelPolicy(configDir, current, patch = {}) {
66
129
  const merged = normalize({
67
130
  ...current,
68
131
  ...patch,
@@ -73,10 +136,24 @@ export async function writeModelPolicy(configDir, patch = {}) {
73
136
  })
74
137
  const file = modelPolicyFile(configDir)
75
138
  await mkdir(path.dirname(file), { recursive: true })
76
- await writeFile(file, JSON.stringify(merged, null, 2) + "\n", "utf8")
139
+ const temp = file + "." + process.pid + "." + Date.now() + ".tmp"
140
+ await writeFile(temp, JSON.stringify(merged, null, 2) + "\n", "utf8")
141
+ try {
142
+ await rename(temp, file)
143
+ } catch (error) {
144
+ await rm(temp, { force: true }).catch(() => {})
145
+ throw error
146
+ }
77
147
  return { ...merged, file }
78
148
  }
79
149
 
150
+ export async function writeModelPolicy(configDir, patch = {}) {
151
+ return withModelPolicyWriteLock(configDir, async () => {
152
+ const current = await readModelPolicy(configDir)
153
+ return persistModelPolicy(configDir, current, patch)
154
+ })
155
+ }
156
+
80
157
  export function validateModelID(value) {
81
158
  return validModelID(value)
82
159
  }
@@ -102,7 +179,9 @@ export function applyConfiguredModel(source, role, policy) {
102
179
  }
103
180
 
104
181
  export async function recordModelPerformance(configDir, outcome = {}) {
105
- const current = await readModelPolicy(configDir)
106
- const performance = recordPerformanceOutcome(current.performance || {}, outcome)
107
- return writeModelPolicy(configDir, { performance })
182
+ return withModelPolicyWriteLock(configDir, async () => {
183
+ const current = await readModelPolicy(configDir)
184
+ const performance = recordPerformanceOutcome(current.performance || {}, outcome)
185
+ return persistModelPolicy(configDir, current, { performance })
186
+ })
108
187
  }
@@ -91,6 +91,73 @@ function gitDiffRows(source, limit = 140) {
91
91
  ], limit)
92
92
  }
93
93
 
94
+ function gitStatusRows(source, limit = 120) {
95
+ return matchingLines(source, [
96
+ /^##\s+/,
97
+ /^On branch\s+/i,
98
+ /^Your branch\s+/i,
99
+ /^Changes to be committed:/i,
100
+ /^Changes not staged for commit:/i,
101
+ /^Untracked files:/i,
102
+ /^Unmerged paths:/i,
103
+ /^\s*(?:nothing to commit|working tree clean)/i,
104
+ /^[ MARCUD?!]{1,2}\s+\S+/,
105
+ ], limit)
106
+ }
107
+
108
+ function gitLogRows(source, limit = 100) {
109
+ return matchingLines(source, [
110
+ /^commit\s+[0-9a-f]{7,40}\b/i,
111
+ /^Author:\s+/i,
112
+ /^Date:\s+/i,
113
+ /^Merge:\s+/i,
114
+ /^\s{4}\S+/,
115
+ /^fatal:/i,
116
+ /^error:/i,
117
+ ], limit)
118
+ }
119
+
120
+ function verificationTestRows(source, limit = 120) {
121
+ return matchingLines(source, [
122
+ /^(?:ok|FAIL)\s+\S+/,
123
+ /^---\s+(?:PASS|FAIL):\s+/,
124
+ /^test result:\s+(?:ok|FAILED)\b/i,
125
+ /^failures?:/i,
126
+ /^running\s+\d+\s+tests?/i,
127
+ /^Finished\s+test\b/i,
128
+ /^Total tests:\s*\d+/i,
129
+ /^\s*(?:Passed|Failed|Skipped):\s*\d+/i,
130
+ /\b(?:panic|assertion failed|FAILED|FAILURE|ERROR)\b/i,
131
+ /\berror\[E\d+\]/,
132
+ /\bBUILD\s+(?:SUCCESS|FAILURE)\b/i,
133
+ /\bTests run:\s*\d+/i,
134
+ ], limit)
135
+ }
136
+
137
+ function pythonLintRows(source, limit = 120) {
138
+ return matchingLines(source, [
139
+ /^.+:\d+:\d+:\s+[A-Z]+\d+\s+/,
140
+ /^.+:\d+:\d+:\s+(?:error|warning)\b/i,
141
+ /^Found\s+\d+\s+errors?/i,
142
+ /^All checks passed!?$/i,
143
+ /^\d+\s+files?\s+(?:reformatted|left unchanged)/i,
144
+ /\b(?:error|warning|failed)\b/i,
145
+ ], limit)
146
+ }
147
+
148
+ function packageInstallRows(source, limit = 120) {
149
+ return matchingLines(source, [
150
+ /\b(?:added|removed|changed)\s+\d+\s+packages?\b/i,
151
+ /\baudited\s+\d+\s+packages?\b/i,
152
+ /\b\d+\s+packages?\s+(?:are\s+)?looking for funding\b/i,
153
+ /\b\d+\s+vulnerabilit(?:y|ies)\b/i,
154
+ /\b(?:deprecated|warning|warn|error|ERR!|ELIFECYCLE|ERR_)\b/i,
155
+ /^Packages:\s+[+-]?\d+/i,
156
+ /^Progress:\s+/i,
157
+ /^Done in\s+/i,
158
+ ], limit)
159
+ }
160
+
94
161
  function rgRows(source, limit = 100) {
95
162
  const lines = String(source || "").split(/\r?\n/).filter(Boolean)
96
163
  const matched = []
@@ -213,6 +280,16 @@ const REDUCERS = Object.freeze([
213
280
  match: (hint) => /\b(?:pytest|py\.test)\b/.test(hint),
214
281
  rows: pytestRows,
215
282
  },
283
+ {
284
+ family: "git-status",
285
+ match: (hint) => /\bgit\s+status\b/.test(hint),
286
+ rows: gitStatusRows,
287
+ },
288
+ {
289
+ family: "git-log",
290
+ match: (hint) => /\bgit\s+log\b/.test(hint),
291
+ rows: gitLogRows,
292
+ },
216
293
  {
217
294
  family: "git-diff",
218
295
  match: (hint) => /\bgit\s+(?:diff|show)\b/.test(hint),
@@ -248,6 +325,21 @@ const REDUCERS = Object.freeze([
248
325
  match: (hint) => /\bprisma\b|schema\.prisma/.test(hint),
249
326
  rows: prismaRows,
250
327
  },
328
+ {
329
+ family: "package-install",
330
+ match: (hint) => /(?:^|\s)(?:npm\s+(?:i|install|ci)|pnpm\s+(?:i|install|add)|yarn\s+(?:install|add))(?:\s|$)/.test(hint),
331
+ rows: packageInstallRows,
332
+ },
333
+ {
334
+ family: "verification-test",
335
+ match: (hint) => /\b(?:go\s+test|cargo\s+test|dotnet\s+test|mvn\s+test|gradle\s+test)\b/.test(hint),
336
+ rows: verificationTestRows,
337
+ },
338
+ {
339
+ family: "python-lint",
340
+ match: (hint) => /\b(?:ruff|flake8|pylint)\b/.test(hint),
341
+ rows: pythonLintRows,
342
+ },
251
343
  {
252
344
  family: "build",
253
345
  match: (hint) => /\b(?:compile|build|gradle|maven|mvn|cargo\s+check|dotnet\s+build)\b/.test(hint),
@@ -0,0 +1,232 @@
1
+ import { readFile, stat } from "node:fs/promises"
2
+
3
+ const EFFECTS = new Set(["allow", "deny", "ask"])
4
+
5
+ function normalizeSlashes(value) {
6
+ return String(value || "").replaceAll("\\", "/")
7
+ }
8
+
9
+ function escapeRegex(value) {
10
+ return value.replace(/[|\\{}()[\]^$+.\-]/g, "\\$&")
11
+ }
12
+
13
+ function wildcardRegex(pattern, { caseInsensitive = false, shell = false } = {}) {
14
+ let value = normalizeSlashes(pattern)
15
+ const shellBare = shell && value.endsWith(" *")
16
+ ? value.slice(0, -2)
17
+ : null
18
+ const compile = (input) => {
19
+ let source = ""
20
+ for (const char of input) {
21
+ if (char === "*") source += ".*"
22
+ else if (char === "?") source += "."
23
+ else source += escapeRegex(char)
24
+ }
25
+ return source
26
+ }
27
+ const alternatives = shellBare === null
28
+ ? [compile(value)]
29
+ : [compile(value), compile(shellBare)]
30
+ return new RegExp(`^(?:${alternatives.join("|")})$`, caseInsensitive ? "i" : "")
31
+ }
32
+
33
+ export function permissionPatternMatches(pattern, value, options = {}) {
34
+ return wildcardRegex(String(pattern || ""), options).test(normalizeSlashes(value))
35
+ }
36
+
37
+ export function normalizePermissionRules(value) {
38
+ const rules = Array.isArray(value) ? value : []
39
+ return rules.map((rule, index) => {
40
+ if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
41
+ throw new Error(`permission rule ${index} must be an object`)
42
+ }
43
+ const action = String(rule.action || "").trim()
44
+ const resource = String(rule.resource || "").trim()
45
+ const effect = String(rule.effect || "").trim().toLowerCase()
46
+ if (!action || !resource || !EFFECTS.has(effect)) {
47
+ throw new Error(`permission rule ${index} requires action, resource and effect=allow|deny|ask`)
48
+ }
49
+ return { action, resource, effect }
50
+ })
51
+ }
52
+
53
+ function resourceValue(action, value) {
54
+ if (["read", "edit", "external_directory"].includes(String(action || ""))) {
55
+ return normalizeSlashes(value)
56
+ }
57
+ return String(value || "")
58
+ }
59
+
60
+ function matchRule(rule, request, platform) {
61
+ const caseInsensitive = platform === "win32"
62
+ const actionMatches = permissionPatternMatches(rule.action, request.action, { caseInsensitive })
63
+ if (!actionMatches) return false
64
+ return permissionPatternMatches(
65
+ rule.resource,
66
+ resourceValue(request.action, request.resource),
67
+ { caseInsensitive, shell: request.action === "shell" },
68
+ )
69
+ }
70
+
71
+ export function evaluatePermissionRules(rules, request = {}, options = {}) {
72
+ const normalized = normalizePermissionRules(rules)
73
+ const action = String(request.action || "").trim()
74
+ const resources = (Array.isArray(request.resources) ? request.resources : [request.resource])
75
+ .filter((value) => value !== undefined && value !== null)
76
+ .map((value) => String(value))
77
+ const platform = String(options.platform || process.platform)
78
+ const defaultEffect = EFFECTS.has(options.defaultEffect) ? options.defaultEffect : "allow"
79
+ if (!action) throw new Error("permission request requires action")
80
+ if (!resources.length) resources.push("*")
81
+
82
+ const decisions = resources.map((resource) => {
83
+ let matchedRule = null
84
+ for (let index = 0; index < normalized.length; index += 1) {
85
+ const rule = normalized[index]
86
+ if (matchRule(rule, { action, resource }, platform)) {
87
+ matchedRule = { ...rule, index }
88
+ }
89
+ }
90
+ return {
91
+ resource,
92
+ effect: matchedRule?.effect || defaultEffect,
93
+ matchedRule,
94
+ }
95
+ })
96
+
97
+ const effect = decisions.some((item) => item.effect === "deny")
98
+ ? "deny"
99
+ : decisions.some((item) => item.effect === "ask")
100
+ ? "ask"
101
+ : "allow"
102
+
103
+ return {
104
+ schemaVersion: 1,
105
+ action,
106
+ effect,
107
+ defaultEffect,
108
+ decisions,
109
+ }
110
+ }
111
+
112
+ export function toolPermissionRequest(toolName, input = {}) {
113
+ const name = String(toolName || "").trim()
114
+ const lower = name.toLowerCase()
115
+ if (lower === "bash" || lower === "powershell") {
116
+ return { action: "shell", resources: [String(input.command || "")] }
117
+ }
118
+ if (lower === "read") {
119
+ return { action: "read", resources: [String(input.path || input.file || input.filePath || "*")] }
120
+ }
121
+ if (["edit", "write", "write_file", "apply_patch", "ues_code_edit"].includes(lower)) {
122
+ return { action: "edit", resources: [String(input.path || input.file || input.filePath || input.target || "*")] }
123
+ }
124
+ if (lower === "grep") {
125
+ return { action: "grep", resources: [String(input.pattern || input.query || "*")] }
126
+ }
127
+ if (lower === "find" || lower === "glob") {
128
+ return { action: "glob", resources: [String(input.pattern || input.glob || input.query || "*")] }
129
+ }
130
+ if (lower === "ues_dispatch") {
131
+ return { action: "subagent", resources: [String(input.agent || input.target || "*")] }
132
+ }
133
+ if (lower === "ues_session") {
134
+ return { action: "session", resources: [String(input.action || "*")] }
135
+ }
136
+ const action = lower.replace(/[^a-z0-9_]+/g, "_") || "tool"
137
+ return { action, resources: ["*"] }
138
+ }
139
+
140
+ export function permissionRecoveryHint(request = {}, decision = null, options = {}) {
141
+ const action = String(request.action || decision?.action || "tool").trim() || "tool"
142
+ const resources = (
143
+ Array.isArray(request.resources)
144
+ ? request.resources
145
+ : request.resource !== undefined
146
+ ? [request.resource]
147
+ : []
148
+ ).map((value) => String(value)).filter(Boolean)
149
+ const denied = Array.isArray(decision?.decisions)
150
+ ? decision.decisions.filter((item) => item?.effect === "deny" || item?.effect === "ask")
151
+ : []
152
+ const resource = String(denied[0]?.resource || resources[0] || "*")
153
+ const effect = String(options.effect || decision?.effect || "deny")
154
+ const prefix = effect === "ask"
155
+ ? "UES approval was not granted for this action."
156
+ : "UES policy denied this action."
157
+
158
+ return [
159
+ prefix,
160
+ `Do not repeat the same blocked ${action} request for ${resource}.`,
161
+ "Continue by choosing the narrowest allowed, reversible alternative that still advances the task.",
162
+ "Prefer read-only inspection or a less-privileged resource when possible.",
163
+ "If no allowed path can satisfy the task, report the blocker with evidence instead of bypassing the guard.",
164
+ ].join(" ")
165
+ }
166
+
167
+ function normalizedConfig(parsed) {
168
+ if (Array.isArray(parsed)) {
169
+ return { permissions: normalizePermissionRules(parsed), agents: {}, defaultEffect: "allow" }
170
+ }
171
+ if (!parsed || typeof parsed !== "object") {
172
+ throw new Error("permission config must be an array or object")
173
+ }
174
+ const permissions = normalizePermissionRules(parsed.permissions || [])
175
+ const agents = {}
176
+ for (const [agent, value] of Object.entries(parsed.agents || {})) {
177
+ const rules = Array.isArray(value) ? value : value?.permissions
178
+ agents[String(agent)] = normalizePermissionRules(rules || [])
179
+ }
180
+ const defaultEffect = EFFECTS.has(parsed.defaultEffect) ? parsed.defaultEffect : "allow"
181
+ return { permissions, agents, defaultEffect }
182
+ }
183
+
184
+ export class PermissionPolicyStore {
185
+ constructor(file) {
186
+ this.file = String(file || "")
187
+ this.cacheKey = null
188
+ this.cached = null
189
+ }
190
+
191
+ async load() {
192
+ if (!this.file) return { configured: false, config: null }
193
+ const info = await stat(this.file).catch((error) => {
194
+ if (error?.code === "ENOENT") return null
195
+ throw error
196
+ })
197
+ if (!info?.isFile()) {
198
+ this.cacheKey = null
199
+ this.cached = null
200
+ return { configured: false, config: null }
201
+ }
202
+
203
+ const cacheKey = `${info.mtimeMs}:${info.size}`
204
+ if (this.cached && this.cacheKey === cacheKey) {
205
+ return { configured: true, config: this.cached, cached: true }
206
+ }
207
+
208
+ const parsed = JSON.parse(await readFile(this.file, "utf8"))
209
+ const config = normalizedConfig(parsed)
210
+ this.cacheKey = cacheKey
211
+ this.cached = config
212
+ return { configured: true, config, cached: false }
213
+ }
214
+
215
+ async evaluate(request, options = {}) {
216
+ const loaded = await this.load()
217
+ if (!loaded.configured) return { configured: false, decision: null }
218
+ const agent = String(options.agent || "")
219
+ const agentRules = agent ? (loaded.config.agents[agent] || []) : []
220
+ const rules = [...loaded.config.permissions, ...agentRules]
221
+ const decision = evaluatePermissionRules(rules, request, {
222
+ platform: options.platform,
223
+ defaultEffect: loaded.config.defaultEffect,
224
+ })
225
+ return {
226
+ configured: true,
227
+ decision,
228
+ agent: agent || null,
229
+ cached: loaded.cached === true,
230
+ }
231
+ }
232
+ }