opencode-agent-skill 15.1.0 → 15.1.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 (37) hide show
  1. package/README.md +3 -3
  2. package/docs/V15-MANAGED-RUNTIME.md +18 -0
  3. package/global-config/agents/verifier.md +6 -2
  4. package/lib/affected-tests.mjs +21 -4
  5. package/lib/capability-fabric.mjs +107 -48
  6. package/lib/code-intelligence/index.mjs +3 -15
  7. package/lib/code-intelligence/lsp-pool.mjs +694 -0
  8. package/lib/code-intelligence/lsp-provider.mjs +160 -20
  9. package/lib/compaction-resume-guard.mjs +76 -4
  10. package/lib/completion-auditor.mjs +20 -0
  11. package/lib/context-manifest.mjs +38 -21
  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/hierarchical-context.mjs +31 -3
  16. package/lib/learning-engine.mjs +164 -65
  17. package/lib/memory-engine.mjs +163 -86
  18. package/lib/model-config.mjs +86 -7
  19. package/lib/performance-fabric.mjs +92 -0
  20. package/lib/permission-policy.mjs +232 -0
  21. package/lib/pi-rpc-pool.mjs +170 -5
  22. package/lib/provider-recovery.mjs +51 -0
  23. package/lib/repo-graph.mjs +38 -5
  24. package/lib/runtime-events.mjs +31 -10
  25. package/lib/semantic-index.mjs +67 -20
  26. package/lib/skill-compiler.mjs +84 -18
  27. package/lib/task-policy.mjs +80 -7
  28. package/lib/trajectory.mjs +25 -1
  29. package/lib/untrusted-output.mjs +106 -0
  30. package/package.json +8 -6
  31. package/pi/extensions/ues-child-runtime.ts +78 -3
  32. package/pi/extensions/ues.ts +695 -81
  33. package/scripts/benchmark-lsp-pool.mjs +128 -0
  34. package/scripts/check-release-consistency.mjs +16 -4
  35. package/scripts/check-source-integrity.mjs +60 -1
  36. package/scripts/run-test-suite.mjs +143 -0
  37. package/scripts/smoke-package-closure.mjs +4 -0
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Package:** <code>opencode-agent-skill</code>
6
6
  **Host chính:** Pi Agent
7
- **Phiên bản package hiện tại:** <code>15.1.0</code>
7
+ **Phiên bản package hiện tại:** <code>15.1.1</code>
8
8
  **Nhánh phát triển hiện tại:** V15.19 Finalization Hardening
9
9
  **Stable npm hiện tại:** <code>15.1.0</code>
10
10
  **Runtime:** Node.js 22.19+
@@ -1431,7 +1431,7 @@ npm pack
1431
1431
  Package version hiện tại trên nhánh <code>main</code> là:
1432
1432
 
1433
1433
  ~~~text
1434
- 15.1.0
1434
+ 15.1.1
1435
1435
  ~~~
1436
1436
 
1437
1437
  Stable npm public hiện vẫn là <code>15.1.0</code>. V15.3 DEEP Speed giảm exploration trùng lặp ở long-horizon bằng diagnosis deduplication và bounded specialist exploration, nhưng giữ nguyên plan/verifier/integration gates. V15.4 ACP-safe child runtime ngăn UES spawn lại ACP/Zed entrypoint; child specialist luôn được chạy bằng Pi CLI thật. V15.5 Per-Leaf Turbo cho phép leaf task nhỏ trong DEEP plan tự xuống FAST, giữ failure delta task-local, và tái sử dụng context theo root namespace + workspace fingerprint để giảm retry/rebuild latency. V15.6 Fast Planning đặt soft-steer/hard-idle budget riêng cho architect/plan-checker, recovery từ warm context thay vì scan lại, và lease/cleanup sandbox orphan an toàn sau crash/Stop. V15.7 Lightweight Sandbox Cleanup thu hồi worktree theo trace ngay khi /ues-run kết thúc, dọn metadata mồ côi, giảm legacy grace xuống 30 phút và cung cấp /ues-clean để dọn stale artifacts an toàn mà không đụng sandbox đang active. V15.8 Plan Gate Recovery bổ sung soft-steer + bounded recovery cho plan-checker và quét cả detached physical sandbox folders mà Git worktree registry đã quên. V15.9 Runtime Artifact Isolation loại .ues-traces/.ues-cache/.ues-services/.ues-work và runtime state khác khỏi task delta, write-scope conflict và sandbox integration, nhưng vẫn giữ safety gate cho source/config thật như apps/mobile/package.json. V15.10 Adaptive Stability Runtime thay hard-timeout tuyệt đối bằng activity-aware bounded deadlines, giữ absolute cap chống treo, salvage plan JSON đã validate từ partial output, phát graph trước prose, và role-bound context cho read-only planner trong task high-risk mà không giảm evidence budget của executor/verifier. V15.1 bắt đầu bằng deterministic <code>/ues-run</code> admission và Managed Background Services để model yếu không thể bỏ qua controller và không bị treo bởi dev server foreground.
@@ -1439,7 +1439,7 @@ Stable npm public hiện vẫn là <code>15.1.0</code>. V15.3 DEEP Speed giảm
1439
1439
  Test file packed với Pi:
1440
1440
 
1441
1441
  ~~~cmd
1442
- pi install .\opencode-agent-skill-15.1.0.tgz
1442
+ pi install .\opencode-agent-skill-15.1.1.tgz
1443
1443
  pi list
1444
1444
  ~~~
1445
1445
 
@@ -126,3 +126,21 @@ The planning watchdog is progress-aware rather than a fixed wall-clock kill swit
126
126
  Architect structured planning is JSON-first. A complete graph emitted before a late transport timeout can be recovered only through deterministic normalization/validation and must still pass the independent plan-checker.
127
127
 
128
128
  High-risk execution and verification retain the original evidence ceiling. Only read-only planning context is role-bounded to reduce first-token latency and duplicate repository ingestion.
129
+
130
+
131
+ ## Managed LSP V2
132
+
133
+ Code Intelligence now treats language servers as a bounded accelerator rather than a mandatory dependency.
134
+
135
+ - Persistent subprocesses are lazy-started and isolated by workspace, provider, and project-configuration fingerprint.
136
+ - Warm requests reuse the initialized server; document content is hash-tracked and synchronized with full-text `didChange` versions after edits.
137
+ - Diagnostics are fenced by document version so a delayed notification cannot silently prove an older source revision.
138
+ - The pool is bounded by global/per-workspace limits, idle TTL, and LRU eviction. Transient transport/startup failures receive at most the configured bounded restart budget.
139
+ - Non-transient request rejection does not destroy an otherwise healthy warm session.
140
+ - Windows npm shims are resolved through the existing safe Windows command resolver instead of attempting to execute `.cmd` wrappers directly.
141
+ - Pi child shutdown stops pooled language servers alongside managed services.
142
+ - If the persistent path is disabled, saturated, or unhealthy, Code Intelligence falls back to the existing ephemeral LSP path; semantic index/search remains independent of LSP availability.
143
+
144
+ Runtime tuning is optional through `UES_LSP_PERSISTENT`, `UES_LSP_IDLE_TTL_MS`, `UES_LSP_MAX_SERVERS`, `UES_LSP_MAX_PER_WORKSPACE`, `UES_LSP_REQUEST_TIMEOUT_MS`, `UES_LSP_STARTUP_TIMEOUT_MS`, and `UES_LSP_MAX_RESTARTS`.
145
+
146
+ Use `npm run bench:lsp` for a deterministic cold/warm protocol benchmark. The benchmark uses a mock language server so it measures pool overhead and reuse, not the indexing cost of a particular external language server.
@@ -16,7 +16,11 @@ Return exactly these sections:
16
16
  Command/check, exit/result, and what claim it proves.
17
17
 
18
18
  ## Acceptance criteria proven
19
- Criterion-by-criterion evidence.
19
+ Criterion-by-criterion evidence. Prefix each criterion with exactly one evidence status:
20
+ - `VERIFIED:` only when fresh direct evidence proves it.
21
+ - `INFERRED:` when it is only supported by reasoning or indirect evidence.
22
+ - `UNKNOWN:` when it was not checked or evidence is insufficient.
23
+ Any requested criterion marked `INFERRED:` or `UNKNOWN:` must also appear under **Unresolved gaps** and cannot support PASS.
20
24
 
21
25
  ## Failures
22
26
  Actual failed checks or unmet criteria. If none, write exactly `None`.
@@ -30,4 +34,4 @@ What was skipped and why. Put optional or out-of-scope checks here rather than t
30
34
  ## Completion evidence
31
35
  A concise statement limited to what the fresh evidence supports.
32
36
 
33
- Do not infer success from another agent's report or from compilation alone.
37
+ Do not infer success from another agent's report or from compilation alone. A semantic claim about code, behavior, or an interface must be backed by an inspected path/symbol or fresh executable evidence before it can be marked VERIFIED.
@@ -11,6 +11,22 @@ const AFFECTED_TEST_CACHE = new Map()
11
11
  const AFFECTED_TEST_INFLIGHT = new Map()
12
12
  const TEST_CONTENT_CACHE = new Map()
13
13
 
14
+ async function mapLimit(items, limit, worker) {
15
+ if (!items.length) return []
16
+ const output = new Array(items.length)
17
+ let cursor = 0
18
+ const width = Math.min(items.length, Math.max(1, Number(limit || 1)))
19
+ const runners = Array.from({ length: width }, async () => {
20
+ while (true) {
21
+ const index = cursor++
22
+ if (index >= items.length) return
23
+ output[index] = await worker(items[index], index)
24
+ }
25
+ })
26
+ await Promise.all(runners)
27
+ return output
28
+ }
29
+
14
30
  function git(root, args) {
15
31
  return spawnSync("git", args, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 })
16
32
  }
@@ -228,8 +244,8 @@ export async function resolveAffectedTests(root = process.cwd(), options = {}) {
228
244
  .map((file) => normalize(path.relative(root, file)))
229
245
  .filter((file) => TEST_RE.test(file))
230
246
 
231
- const ranked = []
232
- for (const testFile of tests) {
247
+ const ioConcurrency = Math.max(1, Math.min(32, Number(options.ioConcurrency ?? 12)))
248
+ const scoredTests = await mapLimit(tests, ioConcurrency, async (testFile) => {
233
249
  const info = await stat(path.join(root, testFile)).catch(() => null)
234
250
  const content = await cachedTestContent(path.join(root, testFile), info)
235
251
  let best = { score: 0, reasons: [], changedFile: null }
@@ -237,8 +253,9 @@ export async function resolveAffectedTests(root = process.cwd(), options = {}) {
237
253
  const current = scoreTest(testFile, changedFile, content)
238
254
  if (current.score > best.score) best = { ...current, changedFile }
239
255
  }
240
- if (best.score > 0) ranked.push({ path: testFile, ...best })
241
- }
256
+ return best.score > 0 ? { path: testFile, ...best } : null
257
+ })
258
+ const ranked = scoredTests.filter(Boolean)
242
259
 
243
260
  ranked.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
244
261
  const selected = ranked.slice(0, limit)
@@ -1,12 +1,77 @@
1
1
  import { existsSync } from "node:fs"
2
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
3
- import { spawnSync } from "node:child_process"
2
+ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"
4
3
  import path from "node:path"
4
+ import { commandExists } from "./executable-probe.mjs"
5
5
 
6
6
  const HEALTH_WEIGHT = { healthy: 40, degraded: 15, unknown: 0, unavailable: -1000 }
7
7
  const COST_PENALTY = { low: 0, medium: 5, high: 12 }
8
8
  const LATENCY_PENALTY = { fast: 0, medium: 3, slow: 8 }
9
9
  const OBSERVATION_FILE = "CAPABILITY-OBSERVATIONS.json"
10
+ const OBSERVATION_WRITE_TAILS = new Map()
11
+ const OBSERVATION_LOCK_STALE_MS = 15_000
12
+ const OBSERVATION_LOCK_WAIT_MS = 20_000
13
+
14
+ function sleep(ms) {
15
+ return new Promise((resolve) => setTimeout(resolve, ms))
16
+ }
17
+
18
+ function observationLockPath(root) {
19
+ return path.join(path.resolve(root), ".ues-learning", ".capability-observations.lock")
20
+ }
21
+
22
+ async function withCrossProcessObservationLock(root, fn) {
23
+ const lockDir = observationLockPath(root)
24
+ await mkdir(path.dirname(lockDir), { recursive: true })
25
+ const deadline = Date.now() + OBSERVATION_LOCK_WAIT_MS
26
+ let delayMs = 8
27
+
28
+ while (true) {
29
+ try {
30
+ await mkdir(lockDir)
31
+ break
32
+ } catch (error) {
33
+ if (error?.code !== "EEXIST") throw error
34
+ const info = await stat(lockDir).catch(() => null)
35
+ if (info && Date.now() - info.mtimeMs > OBSERVATION_LOCK_STALE_MS) {
36
+ const confirmed = await stat(lockDir).catch(() => null)
37
+ if (
38
+ confirmed &&
39
+ confirmed.ino === info.ino &&
40
+ confirmed.mtimeMs === info.mtimeMs
41
+ ) {
42
+ await rm(lockDir, { recursive: true, force: true }).catch(() => {})
43
+ continue
44
+ }
45
+ }
46
+ if (Date.now() >= deadline) throw new Error("Timed out waiting for capability observation lock")
47
+ await sleep(delayMs)
48
+ delayMs = Math.min(100, delayMs * 2)
49
+ }
50
+ }
51
+
52
+ try {
53
+ return await fn()
54
+ } finally {
55
+ await rm(lockDir, { recursive: true, force: true }).catch(() => {})
56
+ }
57
+ }
58
+
59
+ async function withObservationWriteLock(root, fn) {
60
+ const key = path.resolve(root)
61
+ const previous = OBSERVATION_WRITE_TAILS.get(key) || Promise.resolve()
62
+ let release
63
+ const barrier = new Promise((resolve) => { release = resolve })
64
+ const tail = previous.catch(() => {}).then(() => barrier)
65
+ OBSERVATION_WRITE_TAILS.set(key, tail)
66
+
67
+ await previous.catch(() => {})
68
+ try {
69
+ return await withCrossProcessObservationLock(root, fn)
70
+ } finally {
71
+ release()
72
+ if (OBSERVATION_WRITE_TAILS.get(key) === tail) OBSERVATION_WRITE_TAILS.delete(key)
73
+ }
74
+ }
10
75
 
11
76
  function observationPath(root) {
12
77
  return path.join(path.resolve(root), ".ues-learning", OBSERVATION_FILE)
@@ -25,27 +90,29 @@ export async function readCapabilityObservations(root = process.cwd()) {
25
90
  }
26
91
  export async function recordCapabilityObservation(root, capability, providerId, input = {}) {
27
92
  if (!capability || !providerId) throw new Error("capability observation requires capability and provider id")
28
- const state = await readCapabilityObservations(root)
29
- const previous = state.capabilities?.[capability]?.[providerId] || {}
30
- const success = input.success === true, failure = input.success === false
31
- const samples = Number(previous.samples || 0) + (success || failure ? 1 : 0)
32
- const successes = Number(previous.successes || 0) + (success ? 1 : 0)
33
- const failures = Number(previous.failures || 0) + (failure ? 1 : 0)
34
- const latencyMs = Number.isFinite(Number(input.latencyMs)) ? Math.max(0, Number(input.latencyMs)) : null
35
- const previousLatencySamples = Number(previous.latencySamples || 0)
36
- const latencySamples = previousLatencySamples + (latencyMs == null ? 0 : 1)
37
- const avgLatencyMs = latencyMs == null ? (previous.avgLatencyMs ?? null) : ((Number(previous.avgLatencyMs || 0) * previousLatencySamples) + latencyMs) / Math.max(1, latencySamples)
38
- const next = {
39
- samples, successes, failures,
40
- successRate: samples ? successes / samples : 0,
41
- failureRate: samples ? failures / samples : 0,
42
- latencySamples, avgLatencyMs: avgLatencyMs == null ? null : Number(avgLatencyMs.toFixed(2)),
43
- lastLatencyMs: latencyMs, lastObservedAt: new Date().toISOString(),
44
- lastError: input.error ? String(input.error).slice(0, 500) : null,
45
- }
46
- const capabilities = { ...state.capabilities, [capability]: { ...(state.capabilities?.[capability] || {}), [providerId]: next } }
47
- await atomicJson(observationPath(root), { schemaVersion: 1, updatedAt: next.lastObservedAt, capabilities })
48
- return next
93
+ return withObservationWriteLock(root, async () => {
94
+ const state = await readCapabilityObservations(root)
95
+ const previous = state.capabilities?.[capability]?.[providerId] || {}
96
+ const success = input.success === true, failure = input.success === false
97
+ const samples = Number(previous.samples || 0) + (success || failure ? 1 : 0)
98
+ const successes = Number(previous.successes || 0) + (success ? 1 : 0)
99
+ const failures = Number(previous.failures || 0) + (failure ? 1 : 0)
100
+ const latencyMs = Number.isFinite(Number(input.latencyMs)) ? Math.max(0, Number(input.latencyMs)) : null
101
+ const previousLatencySamples = Number(previous.latencySamples || 0)
102
+ const latencySamples = previousLatencySamples + (latencyMs == null ? 0 : 1)
103
+ const avgLatencyMs = latencyMs == null ? (previous.avgLatencyMs ?? null) : ((Number(previous.avgLatencyMs || 0) * previousLatencySamples) + latencyMs) / Math.max(1, latencySamples)
104
+ const next = {
105
+ samples, successes, failures,
106
+ successRate: samples ? successes / samples : 0,
107
+ failureRate: samples ? failures / samples : 0,
108
+ latencySamples, avgLatencyMs: avgLatencyMs == null ? null : Number(avgLatencyMs.toFixed(2)),
109
+ lastLatencyMs: latencyMs, lastObservedAt: new Date().toISOString(),
110
+ lastError: input.error ? String(input.error).slice(0, 500) : null,
111
+ }
112
+ const capabilities = { ...state.capabilities, [capability]: { ...(state.capabilities?.[capability] || {}), [providerId]: next } }
113
+ await atomicJson(observationPath(root), { schemaVersion: 1, updatedAt: next.lastObservedAt, capabilities })
114
+ return next
115
+ })
49
116
  }
50
117
 
51
118
  function clamp(value, min = 0, max = 1, fallback = 0) {
@@ -53,21 +120,6 @@ function clamp(value, min = 0, max = 1, fallback = 0) {
53
120
  return Number.isFinite(number) ? Math.max(min, Math.min(max, number)) : fallback
54
121
  }
55
122
 
56
- const COMMAND_PROBE_TTL_MS = 60_000
57
- const commandProbeCache = new Map()
58
-
59
- function commandExists(command) {
60
- if (!command) return false
61
- const now = Date.now()
62
- const cached = commandProbeCache.get(command)
63
- if (cached && now - cached.checkedAt < COMMAND_PROBE_TTL_MS) return cached.available
64
- const finder = process.platform === "win32" ? "where" : "which"
65
- const result = spawnSync(finder, [command], { stdio: "ignore", windowsHide: true })
66
- const available = result.status === 0
67
- commandProbeCache.set(command, { available, checkedAt: now })
68
- return available
69
- }
70
-
71
123
  async function packageDeclared(root, name) {
72
124
  try {
73
125
  const pkg = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"))
@@ -348,20 +400,27 @@ export async function capabilityFabricStatus(root = process.cwd(), options = {})
348
400
  const persistedObservations = options.observations ? null : await readCapabilityObservations(root)
349
401
  const capabilities = {}
350
402
  const rows = []
351
- for (const [name, providers] of Object.entries(registry.capabilities || {})) {
352
- const probed = []
353
- for (const provider of providers || []) probed.push(await probeCapabilityProvider(provider, root))
403
+ const entries = Object.entries(registry.capabilities || {})
404
+ const evaluated = await Promise.all(entries.map(async ([name, providers]) => {
405
+ const probed = await Promise.all((providers || []).map((provider) => probeCapabilityProvider(provider, root)))
354
406
  const selected = selectCapabilityProvider(name, probed, {
355
407
  observations: options.observations?.[name] || persistedObservations?.capabilities?.[name] || {},
356
408
  allowUnknown: options.allowUnknown,
357
409
  })
358
- capabilities[name] = selected
359
- rows.push({
360
- capability: name,
361
- selected: selected.selected?.id || null,
362
- status: selected.selected?.status || "unavailable",
363
- fallbacks: selected.fallbacks.map((item) => item.id),
364
- })
410
+ return {
411
+ name,
412
+ selected,
413
+ row: {
414
+ capability: name,
415
+ selected: selected.selected?.id || null,
416
+ status: selected.selected?.status || "unavailable",
417
+ fallbacks: selected.fallbacks.map((item) => item.id),
418
+ },
419
+ }
420
+ }))
421
+ for (const item of evaluated) {
422
+ capabilities[item.name] = item.selected
423
+ rows.push(item.row)
365
424
  }
366
425
  return {
367
426
  schemaVersion: 1,
@@ -1,24 +1,12 @@
1
1
  import { lstat, readFile, realpath, rename, rm, writeFile } from "node:fs/promises"
2
2
  import { spawnSync } from "node:child_process"
3
3
  import path from "node:path"
4
+ import { commandExists } from "../executable-probe.mjs"
4
5
  import { querySemanticIndex } from "../semantic-index.mjs"
5
6
  import { anchoredLines, applyAnchoredEdits } from "./edit-anchor.mjs"
6
- import { diagnoseCode, lspOperation, lspProviderStatus, LSP_OPERATIONS } from "./lsp-provider.mjs"
7
+ import { diagnoseCode, lspOperation, lspPoolStatus, lspProviderStatus, shutdownLspPool, LSP_OPERATIONS } from "./lsp-provider.mjs"
7
8
 
8
9
  const AST_COMMANDS = ["ast-grep", "sg"]
9
- const commandCache = new Map()
10
- const COMMAND_TTL_MS = 60_000
11
-
12
- function commandExists(command) {
13
- const now = Date.now()
14
- const cached = commandCache.get(command)
15
- if (cached && now - cached.at < COMMAND_TTL_MS) return cached.available
16
- const finder = process.platform === "win32" ? "where" : "which"
17
- const available = spawnSync(finder, [command], { stdio: "ignore", windowsHide: true }).status === 0
18
- commandCache.set(command, { available, at: now })
19
- return available
20
- }
21
-
22
10
  function inside(root, target) { return target === root || target.startsWith(root + path.sep) }
23
11
 
24
12
  async function safeFile(root, relative) {
@@ -93,4 +81,4 @@ export async function searchCodeIntelligence(root, query, options = {}) {
93
81
  }
94
82
  }
95
83
 
96
- export { diagnoseCode, lspOperation, lspProviderStatus, LSP_OPERATIONS } from "./lsp-provider.mjs"
84
+ export { diagnoseCode, lspOperation, lspPoolStatus, lspProviderStatus, shutdownLspPool, LSP_OPERATIONS } from "./lsp-provider.mjs"