opencode-agent-skill 14.4.0 → 15.0.0
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.
- package/README.md +33 -15
- package/docs/PI-COMPAT.md +151 -2
- package/docs/V15-MANAGED-RUNTIME.md +128 -0
- package/global-config/agents/architect.md +63 -1
- package/global-config/agents/executor.md +7 -0
- package/global-config/agents/plan-checker.md +8 -1
- package/global-config/agents/verifier.md +3 -3
- package/global-config/skills/database-engineering/references/workflow.md +15 -0
- package/global-config/skills/engineering-orchestrator/references/long-horizon.md +21 -11
- package/lib/activity-deadline.mjs +70 -0
- package/lib/adaptive-context-budget.mjs +25 -5
- package/lib/affected-tests.mjs +1 -1
- package/lib/capability-fabric.mjs +17 -0
- package/lib/completion-auditor.mjs +25 -5
- package/lib/evidence-store.mjs +92 -8
- package/lib/execution-contract.mjs +579 -0
- package/lib/leaf-runtime-optimizer.mjs +76 -0
- package/lib/pi-child-invocation.mjs +65 -0
- package/lib/pi-rpc-pool.mjs +61 -9
- package/lib/plan-salvage.mjs +124 -0
- package/lib/planning-speed-policy.mjs +131 -0
- package/lib/repo-graph.mjs +1 -1
- package/lib/repo-inspect.mjs +1 -0
- package/lib/runtime-artifacts.mjs +42 -0
- package/lib/runtime-events.mjs +33 -1
- package/lib/safety.mjs +1 -0
- package/lib/semantic-index.mjs +1 -1
- package/lib/service-manager.mjs +526 -0
- package/lib/session-display.mjs +49 -0
- package/lib/task-graph.mjs +87 -0
- package/lib/task-policy.mjs +150 -4
- package/lib/trajectory.mjs +111 -1
- package/lib/turbo-fast-path.mjs +44 -0
- package/lib/workspace-fingerprint.mjs +2 -11
- package/lib/workspace-root.mjs +49 -0
- package/lib/worktree-sandbox.mjs +234 -11
- package/package.json +8 -5
- package/pi/extensions/ues-child-runtime.ts +145 -2
- package/pi/extensions/ues.ts +1760 -160
- package/scripts/check-release-consistency.mjs +25 -2
- package/scripts/check-source-integrity.mjs +471 -4
- package/scripts/eval-pi.mjs +76 -36
- package/scripts/smoke-pi-extension.mjs +1 -1
- package/pi/prompts/ues-run.md +0 -17
package/lib/worktree-sandbox.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync } from "node:fs"
|
|
2
2
|
import { createHash } from "node:crypto"
|
|
3
|
-
import { copyFile, mkdir, readFile, rm, unlink, writeFile } from "node:fs/promises"
|
|
3
|
+
import { copyFile, mkdir, readFile, readdir, rm, unlink, writeFile } from "node:fs/promises"
|
|
4
4
|
import { spawnSync } from "node:child_process"
|
|
5
5
|
import path from "node:path"
|
|
6
|
+
import { isUesRuntimeArtifactPath, sourceFacingPaths, sourceGitPathspecs } from "./runtime-artifacts.mjs"
|
|
6
7
|
|
|
7
8
|
function git(root, args, options = {}) {
|
|
8
9
|
return spawnSync("git", args, {
|
|
@@ -22,6 +23,51 @@ function statusFiles(root) {
|
|
|
22
23
|
.map((line) => line.slice(3).trim())
|
|
23
24
|
.map((value) => value.includes(" -> ") ? value.split(" -> ").at(-1) : value)
|
|
24
25
|
.map((value) => value.replaceAll("\\", "/"))
|
|
26
|
+
.filter((value) => !isUesRuntimeArtifactPath(value))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sourceIntentBatches(values, options = {}) {
|
|
30
|
+
const maxItems = Math.max(1, Math.min(256, Number(options.maxItems || 64)))
|
|
31
|
+
const maxChars = Math.max(1024, Math.min(24_000, Number(options.maxChars || 12_000)))
|
|
32
|
+
const batches = []
|
|
33
|
+
let batch = []
|
|
34
|
+
let chars = 0
|
|
35
|
+
|
|
36
|
+
for (const value of sourceFacingPaths(values)) {
|
|
37
|
+
const cost = value.length + 3
|
|
38
|
+
if (batch.length && (batch.length >= maxItems || chars + cost > maxChars)) {
|
|
39
|
+
batches.push(batch)
|
|
40
|
+
batch = []
|
|
41
|
+
chars = 0
|
|
42
|
+
}
|
|
43
|
+
batch.push(value)
|
|
44
|
+
chars += cost
|
|
45
|
+
}
|
|
46
|
+
if (batch.length) batches.push(batch)
|
|
47
|
+
return batches
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function intentToAddSourceFiles(root) {
|
|
51
|
+
const untracked = git(root, [
|
|
52
|
+
"ls-files",
|
|
53
|
+
"--others",
|
|
54
|
+
"--exclude-standard",
|
|
55
|
+
"-z",
|
|
56
|
+
"--",
|
|
57
|
+
...sourceGitPathspecs(),
|
|
58
|
+
])
|
|
59
|
+
if (untracked.status !== 0) {
|
|
60
|
+
throw new Error((untracked.stderr || untracked.stdout || "git ls-files for intent-to-add failed").trim())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const candidates = sourceFacingPaths(untracked.stdout.split("\0").filter(Boolean))
|
|
64
|
+
for (const batch of sourceIntentBatches(candidates)) {
|
|
65
|
+
const addIntent = git(root, ["add", "-N", "--", ...batch])
|
|
66
|
+
if (addIntent.status !== 0) {
|
|
67
|
+
throw new Error((addIntent.stderr || addIntent.stdout || "git add -N failed").trim())
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return candidates
|
|
25
71
|
}
|
|
26
72
|
|
|
27
73
|
function safeName(value) {
|
|
@@ -40,13 +86,19 @@ async function readSandboxMetadata(dir) {
|
|
|
40
86
|
}
|
|
41
87
|
}
|
|
42
88
|
|
|
89
|
+
export async function taskSandboxOwnerRoot(dir) {
|
|
90
|
+
const metadata = await readSandboxMetadata(path.resolve(dir)).catch(() => null)
|
|
91
|
+
const root = metadata?.root ? path.resolve(String(metadata.root)) : null
|
|
92
|
+
return root || null
|
|
93
|
+
}
|
|
94
|
+
|
|
43
95
|
async function writeSandboxMetadata(dir, value) {
|
|
44
96
|
await writeFile(metadataFile(dir), JSON.stringify(value, null, 2) + "\n", "utf8")
|
|
45
97
|
}
|
|
46
98
|
|
|
47
99
|
async function inheritDirtySnapshot(root, dir) {
|
|
48
100
|
const inheritedUntrackedHashes = {}
|
|
49
|
-
const diff = git(root, ["diff", "--binary", "--no-ext-diff", "HEAD", "--"])
|
|
101
|
+
const diff = git(root, ["diff", "--binary", "--no-ext-diff", "HEAD", "--", ...sourceGitPathspecs()])
|
|
50
102
|
if (diff.status !== 0) throw new Error((diff.stderr || diff.stdout || "git diff HEAD failed").trim())
|
|
51
103
|
if (diff.stdout) {
|
|
52
104
|
const applied = git(dir, ["apply", "--whitespace=nowarn", "-"], { input: diff.stdout })
|
|
@@ -56,10 +108,12 @@ async function inheritDirtySnapshot(root, dir) {
|
|
|
56
108
|
const untracked = git(root, ["ls-files", "--others", "--exclude-standard", "-z"])
|
|
57
109
|
if (untracked.status !== 0) throw new Error((untracked.stderr || untracked.stdout || "git ls-files failed").trim())
|
|
58
110
|
for (const relative of untracked.stdout.split("\0").filter(Boolean)) {
|
|
111
|
+
const normalizedRelative = relative.replaceAll("\\", "/")
|
|
112
|
+
if (isUesRuntimeArtifactPath(normalizedRelative)) continue
|
|
59
113
|
const source = path.join(root, relative)
|
|
60
114
|
const target = path.join(dir, relative)
|
|
61
115
|
const bytes = await readFile(source)
|
|
62
|
-
inheritedUntrackedHashes[
|
|
116
|
+
inheritedUntrackedHashes[normalizedRelative] = createHash("sha256").update(bytes).digest("hex")
|
|
63
117
|
await mkdir(path.dirname(target), { recursive: true })
|
|
64
118
|
await copyFile(source, target)
|
|
65
119
|
}
|
|
@@ -87,9 +141,7 @@ async function inheritDirtySnapshot(root, dir) {
|
|
|
87
141
|
|
|
88
142
|
export async function createTaskSandbox(root, slug, taskID, options = {}) {
|
|
89
143
|
root = path.resolve(root)
|
|
90
|
-
const
|
|
91
|
-
if (status.status !== 0) throw new Error((status.stderr || status.stdout || "git status failed").trim())
|
|
92
|
-
const rootDirty = Boolean(status.stdout.trim())
|
|
144
|
+
const rootDirty = statusFiles(root).length > 0
|
|
93
145
|
if (rootDirty && options.allowDirtyRoot !== true && options.inheritDirtyRoot !== true) {
|
|
94
146
|
throw new Error("sandbox creation requires a clean root working tree so the isolated executor cannot miss existing uncommitted changes")
|
|
95
147
|
}
|
|
@@ -129,6 +181,7 @@ export async function createTaskSandbox(root, slug, taskID, options = {}) {
|
|
|
129
181
|
inheritedDirtyRoot,
|
|
130
182
|
inheritedUntrackedHashes,
|
|
131
183
|
createdAt: new Date().toISOString(),
|
|
184
|
+
ownerPid: process.pid,
|
|
132
185
|
}
|
|
133
186
|
await writeSandboxMetadata(dir, metadata)
|
|
134
187
|
return metadata
|
|
@@ -155,11 +208,12 @@ export async function integrateTaskSandbox(root, dir, options = {}) {
|
|
|
155
208
|
baseSha = base.stdout.trim()
|
|
156
209
|
}
|
|
157
210
|
|
|
158
|
-
// Intent-to-add
|
|
159
|
-
|
|
160
|
-
|
|
211
|
+
// Intent-to-add only the non-ignored untracked source files. A root-wide
|
|
212
|
+
// `git add -N -- .` fails when an ignored UES runtime directory such as
|
|
213
|
+
// .ues-cache exists, even when an exclude pathspec is also present.
|
|
214
|
+
intentToAddSourceFiles(dir)
|
|
161
215
|
|
|
162
|
-
const changedRun = git(dir, ["diff", "--name-only", baseSha, "--"])
|
|
216
|
+
const changedRun = git(dir, ["diff", "--name-only", baseSha, "--", ...sourceGitPathspecs()])
|
|
163
217
|
if (changedRun.status !== 0) throw new Error((changedRun.stderr || changedRun.stdout || "git diff --name-only failed").trim())
|
|
164
218
|
const changed = changedRun.stdout.split(/\r?\n/).filter(Boolean).map((value) => value.replaceAll("\\", "/"))
|
|
165
219
|
if (changed.length === 0) {
|
|
@@ -195,7 +249,7 @@ export async function integrateTaskSandbox(root, dir, options = {}) {
|
|
|
195
249
|
throw new Error("sandbox integration conflicts with existing root changes: " + unsafeOverlap.join(", "))
|
|
196
250
|
}
|
|
197
251
|
|
|
198
|
-
const patch = git(dir, ["diff", "--binary", "--no-ext-diff", baseSha, "--"])
|
|
252
|
+
const patch = git(dir, ["diff", "--binary", "--no-ext-diff", baseSha, "--", ...sourceGitPathspecs()])
|
|
199
253
|
if (patch.status !== 0) throw new Error((patch.stderr || patch.stdout || "git diff failed").trim())
|
|
200
254
|
const applied = git(root, ["apply", "--whitespace=nowarn", "-"], { input: patch.stdout })
|
|
201
255
|
if (applied.status !== 0) {
|
|
@@ -206,6 +260,175 @@ export async function integrateTaskSandbox(root, dir, options = {}) {
|
|
|
206
260
|
return { integrated: true, changed, base: baseSha, empty: false }
|
|
207
261
|
}
|
|
208
262
|
|
|
263
|
+
function processAlive(pid) {
|
|
264
|
+
const value = Number(pid)
|
|
265
|
+
if (!Number.isInteger(value) || value <= 0) return false
|
|
266
|
+
try {
|
|
267
|
+
process.kill(value, 0)
|
|
268
|
+
return true
|
|
269
|
+
} catch {
|
|
270
|
+
return false
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function pruneOrphanTaskSandboxes(root, options = {}) {
|
|
275
|
+
root = path.resolve(root)
|
|
276
|
+
const now = Date.now()
|
|
277
|
+
const minAgeMs = Math.max(60_000, Number(options.minAgeMs || 5 * 60_000))
|
|
278
|
+
const legacyMinAgeMs = Math.max(minAgeMs, Number(options.legacyMinAgeMs || 30 * 60_000))
|
|
279
|
+
const ownedMinAgeMs = Math.max(0, Number(options.ownedMinAgeMs ?? 60_000))
|
|
280
|
+
const reclaimOwnerPid = Number(options.reclaimOwnerPid || 0)
|
|
281
|
+
const protectedDirs = new Set(
|
|
282
|
+
(options.protectedDirs || []).map((value) => path.resolve(String(value))),
|
|
283
|
+
)
|
|
284
|
+
const expectedBase = path.resolve(
|
|
285
|
+
options.baseDir || path.join(path.dirname(root), "." + path.basename(root) + ".ues-sandboxes"),
|
|
286
|
+
)
|
|
287
|
+
const removed = []
|
|
288
|
+
const skipped = []
|
|
289
|
+
const sidecarsRemoved = []
|
|
290
|
+
|
|
291
|
+
for (const item of listTaskSandboxes(root)) {
|
|
292
|
+
const branch = String(item.branch || "").replace(/^refs\/heads\//, "")
|
|
293
|
+
const dir = path.resolve(String(item.path || ""))
|
|
294
|
+
if (!branch.startsWith("ues/")) continue
|
|
295
|
+
if (path.dirname(dir) !== expectedBase) continue
|
|
296
|
+
if (protectedDirs.has(dir)) {
|
|
297
|
+
skipped.push({ dir, reason: "protected-active" })
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const metadata = await readSandboxMetadata(dir)
|
|
302
|
+
if (!metadata || path.resolve(String(metadata.root || "")) !== root) {
|
|
303
|
+
skipped.push({ dir, reason: "metadata-missing-or-root-mismatch" })
|
|
304
|
+
continue
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const createdAt = Date.parse(String(metadata.createdAt || ""))
|
|
308
|
+
const ageMs = Number.isFinite(createdAt) ? Math.max(0, now - createdAt) : Infinity
|
|
309
|
+
const ownerPid = Number(metadata.ownerPid || 0)
|
|
310
|
+
const reclaimOwned = reclaimOwnerPid > 0 && ownerPid === reclaimOwnerPid
|
|
311
|
+
|
|
312
|
+
if (ownerPid > 0 && processAlive(ownerPid) && !reclaimOwned) {
|
|
313
|
+
skipped.push({ dir, reason: "owner-alive", ownerPid })
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const requiredAge = reclaimOwned
|
|
318
|
+
? ownedMinAgeMs
|
|
319
|
+
: ownerPid > 0
|
|
320
|
+
? minAgeMs
|
|
321
|
+
: legacyMinAgeMs
|
|
322
|
+
if (ageMs < requiredAge) {
|
|
323
|
+
skipped.push({ dir, reason: "too-recent", ageMs, ownerPid: ownerPid || null })
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const result = await removeTaskSandbox(root, dir, {
|
|
329
|
+
force: true,
|
|
330
|
+
deleteBranch: true,
|
|
331
|
+
branch,
|
|
332
|
+
})
|
|
333
|
+
removed.push({ ...result, ageMs, ownerPid: ownerPid || null, reclaimOwned })
|
|
334
|
+
} catch (error) {
|
|
335
|
+
skipped.push({
|
|
336
|
+
dir,
|
|
337
|
+
reason: "remove-failed",
|
|
338
|
+
error: error instanceof Error ? error.message : String(error),
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Clean detached physical sandbox directories left after Git forgot the
|
|
344
|
+
// worktree registration (for example after a crash/manual prune). Only remove
|
|
345
|
+
// directories with UES metadata bound to this exact root repository.
|
|
346
|
+
try {
|
|
347
|
+
const registered = new Set(
|
|
348
|
+
listTaskSandboxes(root).map((item) => path.resolve(String(item.path || ""))),
|
|
349
|
+
)
|
|
350
|
+
const entries = await readdir(expectedBase, { withFileTypes: true })
|
|
351
|
+
for (const entry of entries) {
|
|
352
|
+
if (!entry.isDirectory()) continue
|
|
353
|
+
const dir = path.resolve(expectedBase, entry.name)
|
|
354
|
+
if (registered.has(dir) || protectedDirs.has(dir)) continue
|
|
355
|
+
|
|
356
|
+
const metadata = await readSandboxMetadata(dir)
|
|
357
|
+
if (!metadata || path.resolve(String(metadata.root || "")) !== root) continue
|
|
358
|
+
|
|
359
|
+
const createdAt = Date.parse(String(metadata.createdAt || ""))
|
|
360
|
+
const ageMs = Number.isFinite(createdAt) ? Math.max(0, now - createdAt) : Infinity
|
|
361
|
+
const ownerPid = Number(metadata.ownerPid || 0)
|
|
362
|
+
const reclaimOwned = reclaimOwnerPid > 0 && ownerPid === reclaimOwnerPid
|
|
363
|
+
|
|
364
|
+
if (ownerPid > 0 && processAlive(ownerPid) && !reclaimOwned) {
|
|
365
|
+
skipped.push({ dir, reason: "detached-owner-alive", ownerPid })
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const requiredAge = reclaimOwned
|
|
370
|
+
? ownedMinAgeMs
|
|
371
|
+
: ownerPid > 0
|
|
372
|
+
? minAgeMs
|
|
373
|
+
: legacyMinAgeMs
|
|
374
|
+
if (ageMs < requiredAge) {
|
|
375
|
+
skipped.push({ dir, reason: "detached-too-recent", ageMs, ownerPid: ownerPid || null })
|
|
376
|
+
continue
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
await rm(dir, { recursive: true, force: true })
|
|
381
|
+
await unlink(metadataFile(dir)).catch(() => {})
|
|
382
|
+
const branch = String(metadata.branch || "")
|
|
383
|
+
if (branch.startsWith("ues/")) git(root, ["branch", "-D", branch])
|
|
384
|
+
removed.push({
|
|
385
|
+
removed: !existsSync(dir),
|
|
386
|
+
dir,
|
|
387
|
+
branch: branch || null,
|
|
388
|
+
ageMs,
|
|
389
|
+
ownerPid: ownerPid || null,
|
|
390
|
+
reclaimOwned,
|
|
391
|
+
detached: true,
|
|
392
|
+
})
|
|
393
|
+
} catch (error) {
|
|
394
|
+
skipped.push({
|
|
395
|
+
dir,
|
|
396
|
+
reason: "detached-remove-failed",
|
|
397
|
+
error: error instanceof Error ? error.message : String(error),
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch {}
|
|
402
|
+
|
|
403
|
+
// Clean metadata sidecars left after a crash or manual worktree removal.
|
|
404
|
+
try {
|
|
405
|
+
const entries = await readdir(expectedBase, { withFileTypes: true })
|
|
406
|
+
for (const entry of entries) {
|
|
407
|
+
if (!entry.isFile() || !entry.name.endsWith(".ues-meta.json")) continue
|
|
408
|
+
const sidecar = path.join(expectedBase, entry.name)
|
|
409
|
+
const dir = sidecar.slice(0, -".ues-meta.json".length)
|
|
410
|
+
if (existsSync(dir)) continue
|
|
411
|
+
try {
|
|
412
|
+
const metadata = JSON.parse(await readFile(sidecar, "utf8"))
|
|
413
|
+
if (path.resolve(String(metadata.root || "")) !== root) continue
|
|
414
|
+
const createdAt = Date.parse(String(metadata.createdAt || ""))
|
|
415
|
+
const ageMs = Number.isFinite(createdAt) ? Math.max(0, now - createdAt) : Infinity
|
|
416
|
+
if (ageMs < minAgeMs) continue
|
|
417
|
+
await unlink(sidecar)
|
|
418
|
+
sidecarsRemoved.push(sidecar)
|
|
419
|
+
} catch {}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const remaining = await readdir(expectedBase)
|
|
423
|
+
if (remaining.length === 0) {
|
|
424
|
+
await rm(expectedBase, { recursive: false, force: true })
|
|
425
|
+
}
|
|
426
|
+
} catch {}
|
|
427
|
+
|
|
428
|
+
git(root, ["worktree", "prune"])
|
|
429
|
+
return { removed, skipped, sidecarsRemoved, baseRemoved: !existsSync(expectedBase) }
|
|
430
|
+
}
|
|
431
|
+
|
|
209
432
|
export async function removeTaskSandbox(root, dir, options = {}) {
|
|
210
433
|
root = path.resolve(root)
|
|
211
434
|
dir = path.resolve(dir)
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-agent-skill",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Pi
|
|
3
|
+
"version": "15.0.0",
|
|
4
|
+
"description": "Pi-native UES coding-agent runtime with adaptive stability, Turbo Fast Path, durable verification, managed services, and weak-model orchestration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"
|
|
8
|
-
"
|
|
7
|
+
"ocskill": "bin/ocskill.mjs",
|
|
8
|
+
"ues": "bin/ocskill.mjs"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"bin/ocskill.mjs",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"docs/V14-CONTEXT-MEMORY-FABRIC.md",
|
|
22
22
|
"docs/V14.1-QUALITY-PERFORMANCE-FABRIC.md",
|
|
23
23
|
"docs/V14.2-TURBO-WEAK-MODEL-RUNTIME.md",
|
|
24
|
+
"docs/V15-MANAGED-RUNTIME.md",
|
|
24
25
|
"docs/PI-COMPAT.md",
|
|
25
26
|
"docs/OPENCODE-COMPAT.md",
|
|
26
27
|
"README.md",
|
|
@@ -40,7 +41,9 @@
|
|
|
40
41
|
"smoke:packed": "node scripts/smoke-packed-install.mjs",
|
|
41
42
|
"eval:v14": "node --test test/capability-fabric-v14.test.mjs test/hierarchical-context-v14.test.mjs test/memory-engine-v14.test.mjs test/context-engine-v14.test.mjs test/task-engine-v14-context.test.mjs test/cli-v14.test.mjs test/v14-contract.test.mjs test/performance-fabric-v14.test.mjs test/browser-mcp-routing-v14.test.mjs test/process-hang-detector-v14.test.mjs test/v14.2-runtime.test.mjs test/benchmark-confidence.test.mjs test/v14.3-intelligence.test.mjs",
|
|
42
43
|
"integrity": "node scripts/check-source-integrity.mjs",
|
|
43
|
-
"runtime:exports": "node scripts/check-runtime-exports.mjs"
|
|
44
|
+
"runtime:exports": "node scripts/check-runtime-exports.mjs",
|
|
45
|
+
"eval:v15": "node --test test/v15-runtime.test.mjs test/trajectory.test.mjs test/evidence-store-v11.test.mjs test/runtime-events.test.mjs test/v14.3-intelligence.test.mjs test/v14.2-runtime.test.mjs test/pi-package.test.mjs test/execution-contract.test.mjs",
|
|
46
|
+
"release:verify": "npm run ci && npm run eval:v15"
|
|
44
47
|
},
|
|
45
48
|
"keywords": [
|
|
46
49
|
"pi-package",
|
|
@@ -6,6 +6,7 @@ import { getEvidenceSelected } from "../../lib/evidence-store.mjs";
|
|
|
6
6
|
import { recordVerification } from "../../lib/verification-broker.mjs";
|
|
7
7
|
import { runtimeWorkspaceFingerprint } from "../../lib/workspace-fingerprint.mjs";
|
|
8
8
|
import { destructiveShellRisk } from "../../lib/safety.mjs";
|
|
9
|
+
import { crossToolTempPathRisk, isLocalEnvPath, localEnvWriteRisk } from "../../lib/execution-contract.mjs";
|
|
9
10
|
import {
|
|
10
11
|
canonicalVerificationCommand,
|
|
11
12
|
looksLikeVerificationCommand,
|
|
@@ -19,6 +20,16 @@ import {
|
|
|
19
20
|
} from "../../lib/code-intelligence/index.mjs";
|
|
20
21
|
import { ingestDocument } from "../../lib/document-ingestion.mjs";
|
|
21
22
|
import { compactContext, expandContext, searchContext } from "../../lib/reversible-context.mjs";
|
|
23
|
+
import {
|
|
24
|
+
looksLikeLongRunningServiceCommand,
|
|
25
|
+
restartService,
|
|
26
|
+
serviceLogs,
|
|
27
|
+
serviceStatus,
|
|
28
|
+
startService,
|
|
29
|
+
stopAllServices,
|
|
30
|
+
stopService,
|
|
31
|
+
waitForService,
|
|
32
|
+
} from "../../lib/service-manager.mjs";
|
|
22
33
|
|
|
23
34
|
const toolExecutionState = new Map<string, {
|
|
24
35
|
startedAt: number;
|
|
@@ -81,15 +92,67 @@ async function capturedText(event: any, fallback: string) {
|
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
export default function (pi: ExtensionAPI) {
|
|
95
|
+
// Internal-only extension: the package host may discover this file alongside
|
|
96
|
+
// ues.ts, but only specialist child Pi processes should register its tools.
|
|
97
|
+
// Parent launch paths set UES_CHILD_PROCESS=1 for both CLI and RPC workers.
|
|
98
|
+
if (process.env.UES_CHILD_PROCESS !== "1") return;
|
|
99
|
+
|
|
84
100
|
const clearExecutionState = () => toolExecutionState.clear();
|
|
85
101
|
pi.on("session_start", clearExecutionState);
|
|
86
|
-
pi.on("session_shutdown",
|
|
102
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
103
|
+
clearExecutionState();
|
|
104
|
+
await stopAllServices(ctx.cwd).catch(() => []);
|
|
105
|
+
});
|
|
87
106
|
|
|
88
107
|
pi.on("tool_call", async (event, ctx) => {
|
|
89
108
|
const toolName = String(event.toolName || "");
|
|
109
|
+
const input: any = event.input || {};
|
|
110
|
+
const localEnvAllowed = String(process.env.UES_CHILD_ALLOW_LOCAL_ENV_WRITE || "") === "1";
|
|
111
|
+
const writeTool = ["edit", "write", "write_file", "apply_patch", "ues_code_edit"].includes(toolName);
|
|
112
|
+
const fileTool = ["read", "edit", "write", "write_file", "apply_patch", "ues_code", "ues_code_edit"].includes(toolName);
|
|
113
|
+
const fileCandidate = String(input.file || input.path || input.filePath || input.target || "");
|
|
114
|
+
const tempPathRisk = crossToolTempPathRisk(fileCandidate);
|
|
115
|
+
if (fileTool && tempPathRisk.risky) {
|
|
116
|
+
toolExecutionState.delete(String(event.toolCallId || ""));
|
|
117
|
+
return {
|
|
118
|
+
block: true,
|
|
119
|
+
reason:
|
|
120
|
+
"UES portable temp-path guard blocked " + fileCandidate +
|
|
121
|
+
". On Windows, /tmp and /var/tmp may resolve differently between Pi file tools and bash/MSYS. " +
|
|
122
|
+
"For transient transforms, keep creation/read in one shell pipeline; for cross-tool scratch use a repository-local ignored UES path such as .ues-cache/tmp after creating it.",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (writeTool && isLocalEnvPath(fileCandidate) && !localEnvAllowed) {
|
|
126
|
+
toolExecutionState.delete(String(event.toolCallId || ""));
|
|
127
|
+
return {
|
|
128
|
+
block: true,
|
|
129
|
+
reason:
|
|
130
|
+
"UES local-env guard blocked a write to " + fileCandidate +
|
|
131
|
+
". .env/.env.* are local runtime inputs; update an example/template or report NEEDS_USER_ENV unless the user explicitly authorized this local env mutation.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
90
135
|
if (!["bash", "powershell"].includes(toolName)) return undefined;
|
|
91
136
|
|
|
92
|
-
const command = String(
|
|
137
|
+
const command = String(input.command || "");
|
|
138
|
+
const envRisk = localEnvWriteRisk(command);
|
|
139
|
+
if (envRisk.risky && !localEnvAllowed) {
|
|
140
|
+
toolExecutionState.delete(String(event.toolCallId || ""));
|
|
141
|
+
return {
|
|
142
|
+
block: true,
|
|
143
|
+
reason:
|
|
144
|
+
"UES local-env guard blocked a shell write to .env/.env.*. Use .env.example/sample/template or report NEEDS_USER_ENV unless explicitly authorized.",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (looksLikeLongRunningServiceCommand(command)) {
|
|
148
|
+
toolExecutionState.delete(String(event.toolCallId || ""));
|
|
149
|
+
return {
|
|
150
|
+
block: true,
|
|
151
|
+
reason:
|
|
152
|
+
"UES detected a likely long-running foreground service command. " +
|
|
153
|
+
"Use the ues_service tool (start -> wait-ready/logs -> stop) instead of bash/powershell so the agent does not stall.",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
93
156
|
const risk = destructiveShellRisk(command);
|
|
94
157
|
if (risk.risky) {
|
|
95
158
|
toolExecutionState.delete(String(event.toolCallId || ""));
|
|
@@ -345,6 +408,86 @@ export default function (pi: ExtensionAPI) {
|
|
|
345
408
|
},
|
|
346
409
|
});
|
|
347
410
|
|
|
411
|
+
pi.registerTool({
|
|
412
|
+
name: "ues_service",
|
|
413
|
+
label: "UES Managed Service",
|
|
414
|
+
description:
|
|
415
|
+
"Manage long-running development servers/watchers without blocking the agent. Use start, wait-ready, status, logs, stop, or restart. For start/restart, command is the executable only (for example node or npm); put every argument in args. Services are bounded to the current workspace/runtime and are cleaned up on session shutdown.",
|
|
416
|
+
parameters: Type.Object({
|
|
417
|
+
action: Type.Union([
|
|
418
|
+
Type.Literal("start"),
|
|
419
|
+
Type.Literal("wait-ready"),
|
|
420
|
+
Type.Literal("status"),
|
|
421
|
+
Type.Literal("logs"),
|
|
422
|
+
Type.Literal("stop"),
|
|
423
|
+
Type.Literal("restart"),
|
|
424
|
+
]),
|
|
425
|
+
name: Type.String({ minLength: 1, maxLength: 64 }),
|
|
426
|
+
command: Type.Optional(Type.String({ minLength: 1 })),
|
|
427
|
+
args: Type.Optional(Type.Array(Type.String(), { maxItems: 128 })),
|
|
428
|
+
cwd: Type.Optional(Type.String()),
|
|
429
|
+
readyPort: Type.Optional(Type.Number({ minimum: 1, maximum: 65535 })),
|
|
430
|
+
readyHost: Type.Optional(Type.String({ maxLength: 255 })),
|
|
431
|
+
readyLog: Type.Optional(Type.String({ maxLength: 512 })),
|
|
432
|
+
timeoutMs: Type.Optional(Type.Number({ minimum: 100, maximum: 600000 })),
|
|
433
|
+
lifetimeMs: Type.Optional(Type.Number({ minimum: 10000, maximum: 7200000 })),
|
|
434
|
+
idleTimeoutMs: Type.Optional(Type.Number({ minimum: 5000, maximum: 3600000 })),
|
|
435
|
+
maxChars: Type.Optional(Type.Number({ minimum: 256, maximum: 128000 })),
|
|
436
|
+
}),
|
|
437
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
438
|
+
try {
|
|
439
|
+
let result: any;
|
|
440
|
+
if (params.action === "start") {
|
|
441
|
+
if (!params.command) throw new Error("ues_service start requires command");
|
|
442
|
+
const riskText = [params.command, ...(params.args || [])].join(" ");
|
|
443
|
+
const risk = destructiveShellRisk(riskText);
|
|
444
|
+
if (risk.risky) throw new Error(`UES service safety blocked ${risk.id || "destructive"} command`);
|
|
445
|
+
result = await startService(ctx.cwd, {
|
|
446
|
+
name: params.name,
|
|
447
|
+
command: params.command,
|
|
448
|
+
args: params.args || [],
|
|
449
|
+
cwd: params.cwd,
|
|
450
|
+
readyPort: params.readyPort,
|
|
451
|
+
readyHost: params.readyHost,
|
|
452
|
+
readyLog: params.readyLog,
|
|
453
|
+
timeoutMs: params.timeoutMs,
|
|
454
|
+
lifetimeMs: params.lifetimeMs,
|
|
455
|
+
idleTimeoutMs: params.idleTimeoutMs,
|
|
456
|
+
});
|
|
457
|
+
} else if (params.action === "wait-ready") {
|
|
458
|
+
result = await waitForService(ctx.cwd, params.name, { timeoutMs: params.timeoutMs });
|
|
459
|
+
} else if (params.action === "status") {
|
|
460
|
+
result = await serviceStatus(ctx.cwd, params.name);
|
|
461
|
+
} else if (params.action === "logs") {
|
|
462
|
+
result = await serviceLogs(ctx.cwd, params.name, { maxChars: params.maxChars, evidence: true });
|
|
463
|
+
} else if (params.action === "stop") {
|
|
464
|
+
result = await stopService(ctx.cwd, params.name, { timeoutMs: params.timeoutMs });
|
|
465
|
+
} else if (params.action === "restart") {
|
|
466
|
+
result = await restartService(ctx.cwd, params.name, {
|
|
467
|
+
timeoutMs: params.timeoutMs,
|
|
468
|
+
lifetimeMs: params.lifetimeMs,
|
|
469
|
+
idleTimeoutMs: params.idleTimeoutMs,
|
|
470
|
+
});
|
|
471
|
+
} else {
|
|
472
|
+
throw new Error("Unknown ues_service action");
|
|
473
|
+
}
|
|
474
|
+
const failed =
|
|
475
|
+
(params.action === "start" || params.action === "wait-ready") && result?.ready !== true;
|
|
476
|
+
return {
|
|
477
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
478
|
+
details: result,
|
|
479
|
+
isError: failed,
|
|
480
|
+
};
|
|
481
|
+
} catch (error) {
|
|
482
|
+
return {
|
|
483
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
484
|
+
details: { action: params.action, name: params.name },
|
|
485
|
+
isError: true,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
|
|
348
491
|
pi.registerTool({
|
|
349
492
|
name: "ues_evidence_get",
|
|
350
493
|
label: "UES Evidence Get",
|