opencode-agent-skill 7.7.0 → 10.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/CHANGELOG.md +112 -3
- package/README.md +396 -281
- package/bin/ocskill.mjs +382 -156
- package/docs/DETERMINISTIC-TOOLS.md +25 -8
- package/docs/ENGINEERING-DESIGN.md +31 -13
- package/docs/EVALS.md +34 -12
- package/docs/NPM-PUBLISH.md +6 -6
- package/docs/OPENCODE-COMPAT.md +11 -8
- package/docs/TRACE-SCHEMA.md +15 -2
- package/docs/V8-INTELLIGENCE-RELIABILITY.md +206 -0
- package/docs/V9-SPEED-INTELLIGENCE.md +102 -0
- package/evals/live/tasks.json +6 -6
- package/evals/polyglot/fixtures/polyglot-bench/api/generated/client.ts +2 -0
- package/evals/polyglot/fixtures/polyglot-bench/api/openapi.json +25 -0
- package/evals/polyglot/fixtures/polyglot-bench/db/migrations/20260920_add_order_key.sql +1 -0
- package/evals/polyglot/fixtures/polyglot-bench/dotnet/OrderService.cs +8 -0
- package/evals/polyglot/fixtures/polyglot-bench/java/PriceService.java +5 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/package.json +6 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/api/package.json +4 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/web/package.json +7 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/pnpm-lock.yaml +5 -0
- package/evals/polyglot/fixtures/polyglot-bench/next/app/api/products/route.ts +7 -0
- package/evals/polyglot/fixtures/polyglot-bench/python/tenant_auth.py +4 -0
- package/evals/polyglot/fixtures/polyglot-bench/react-native/keyboard.ts +3 -0
- package/evals/polyglot/graders/polyglot-bench.mjs +101 -0
- package/evals/polyglot/tasks.json +54 -0
- package/global-config/AGENTS.md +78 -160
- package/global-config/agents/integration-verifier.md +1 -1
- package/global-config/agents/plan-checker.md +1 -1
- package/global-config/commands/run.md +9 -5
- package/global-config/plugins/ues-router/capabilities.js +4 -0
- package/global-config/plugins/ues-router/index.js +784 -37
- package/global-config/plugins/ues-router/router.js +175 -23
- package/global-config/plugins/ues-router/runtime-guard.js +265 -0
- package/global-config/skills/engineering-orchestrator/references/long-horizon.md +6 -4
- package/lib/aci.mjs +128 -0
- package/lib/benchmark-confidence.mjs +173 -0
- package/lib/cli-utils.mjs +41 -0
- package/lib/container-sandbox.mjs +102 -0
- package/lib/context-manifest.mjs +300 -22
- package/lib/control-center.mjs +36 -3
- package/lib/eval-ablation.mjs +104 -0
- package/lib/eval-order.mjs +9 -0
- package/lib/eval-report.mjs +11 -0
- package/lib/eval-telemetry.mjs +8 -2
- package/lib/gate-receipt.mjs +52 -0
- package/lib/installer.mjs +39 -25
- package/lib/learning-engine.mjs +236 -38
- package/lib/model-policy.mjs +6 -0
- package/lib/opencode-compat.mjs +25 -10
- package/lib/orchestrator-policy.mjs +195 -21
- package/lib/process-runner.mjs +30 -9
- package/lib/runtime-events.mjs +31 -0
- package/lib/semantic-index.mjs +318 -0
- package/lib/task-engine.mjs +557 -28
- package/lib/trajectory.mjs +89 -0
- package/lib/windows-shim.mjs +227 -0
- package/lib/worktree-sandbox.mjs +85 -3
- package/package.json +11 -4
- package/scripts/check-release-tag.mjs +22 -0
- package/scripts/control-center.mjs +25 -0
- package/scripts/eval-ablation.mjs +44 -0
- package/scripts/eval-live.mjs +39 -58
- package/scripts/eval-matrix.mjs +166 -0
- package/scripts/smoke-packed-install.mjs +138 -4
- package/scripts/smoke-plain-install.mjs +91 -0
- package/scripts/validate-live-suite.mjs +3 -3
- package/scripts/validate.mjs +27 -5
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { Plugin } from "@opencode/plugin"
|
|
2
|
-
import { readFileSync } from "node:fs"
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs"
|
|
3
3
|
import { fileURLToPath } from "node:url"
|
|
4
4
|
import path from "node:path"
|
|
5
5
|
import { spawnSync } from "node:child_process"
|
|
6
|
-
import {
|
|
6
|
+
import { classifyIntent, routeSkillsForPolicy } from "./router.js"
|
|
7
7
|
import { destructiveShellRisk } from "./safety.js"
|
|
8
8
|
import { runtimeCapabilities } from "./capabilities.js"
|
|
9
|
+
import {
|
|
10
|
+
budgetToolResult,
|
|
11
|
+
classifyProviderFailure,
|
|
12
|
+
createRuntimeGuard,
|
|
13
|
+
providerRecoveryPlan,
|
|
14
|
+
progressWatchdogDecision,
|
|
15
|
+
stableRuntimeHash,
|
|
16
|
+
} from "./runtime-guard.js"
|
|
9
17
|
|
|
10
18
|
const CONFIG_FILE = path.resolve(
|
|
11
19
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -28,13 +36,43 @@ function routerConfig() {
|
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
|
|
39
|
+
function findWindowsCommand(name) {
|
|
40
|
+
const result = spawnSync("where", [name], { encoding: "utf8" })
|
|
41
|
+
if (result.status !== 0 || !result.stdout) return null
|
|
42
|
+
const matches = result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
43
|
+
return matches.find((item) => /\.(cmd|bat)$/i.test(item)) || matches[0] || null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function findNodeShimEntry(cmdPath) {
|
|
47
|
+
const dir = path.dirname(cmdPath)
|
|
48
|
+
let shim = ""
|
|
49
|
+
try { shim = readFileSync(cmdPath, "utf8") } catch {}
|
|
50
|
+
const match = shim.match(/node_modules[\\/][^\s"]+?\.(?:js|mjs)/gi)?.at(-1)
|
|
51
|
+
if (!match) return null
|
|
52
|
+
const entry = path.resolve(dir, match)
|
|
53
|
+
return existsSync(entry) ? entry : null
|
|
54
|
+
}
|
|
55
|
+
|
|
31
56
|
function runOcskill(args, cwd) {
|
|
32
|
-
const
|
|
57
|
+
const common = {
|
|
33
58
|
cwd,
|
|
34
59
|
encoding: "utf8",
|
|
35
|
-
shell: process.platform === "win32",
|
|
36
60
|
maxBuffer: 4 * 1024 * 1024,
|
|
37
|
-
}
|
|
61
|
+
}
|
|
62
|
+
let result
|
|
63
|
+
if (process.platform !== "win32") {
|
|
64
|
+
result = spawnSync("ocskill", args, common)
|
|
65
|
+
} else {
|
|
66
|
+
const resolved = findWindowsCommand("ocskill")
|
|
67
|
+
if (!resolved) throw new Error("ocskill command was not found on PATH")
|
|
68
|
+
if (/\.(cmd|bat)$/i.test(resolved)) {
|
|
69
|
+
const entry = findNodeShimEntry(resolved)
|
|
70
|
+
if (!entry) throw new Error("refusing to execute an unrecognized ocskill batch shim through cmd.exe")
|
|
71
|
+
result = spawnSync(process.execPath, [entry, ...args], common)
|
|
72
|
+
} else {
|
|
73
|
+
result = spawnSync(resolved, args, common)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
38
76
|
if (result.status !== 0) {
|
|
39
77
|
throw new Error((result.stderr || result.stdout || "ocskill command failed").trim())
|
|
40
78
|
}
|
|
@@ -50,6 +88,13 @@ function runOcskillJSON(args, cwd) {
|
|
|
50
88
|
}
|
|
51
89
|
}
|
|
52
90
|
|
|
91
|
+
function appendTrace(traceID, type, payload, cwd) {
|
|
92
|
+
try {
|
|
93
|
+
const encoded = Buffer.from(JSON.stringify(payload || {}), "utf8").toString("base64")
|
|
94
|
+
runOcskill(["trace", "append", traceID, cwd, "--type", type, "--payload-b64", encoded], cwd)
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
|
|
53
98
|
function modelRef(value) {
|
|
54
99
|
if (!value || typeof value !== "string") return null
|
|
55
100
|
const [base, variant] = value.split("#", 2)
|
|
@@ -68,11 +113,313 @@ function messageExcerpt(messages) {
|
|
|
68
113
|
return text.length <= 24000 ? text : text.slice(-24000)
|
|
69
114
|
}
|
|
70
115
|
|
|
116
|
+
function taskHasWrites(task) {
|
|
117
|
+
const files = task?.files
|
|
118
|
+
if (Array.isArray(files)) return files.length > 0
|
|
119
|
+
if (!files || typeof files !== "object") return false
|
|
120
|
+
return ["create", "modify", "test", "delete"].some(
|
|
121
|
+
(key) => Array.isArray(files[key]) && files[key].length > 0,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function workspaceSignal(root) {
|
|
126
|
+
const scope = [
|
|
127
|
+
"--",
|
|
128
|
+
".",
|
|
129
|
+
":(exclude).ues-work",
|
|
130
|
+
":(exclude).ues-learning",
|
|
131
|
+
":(exclude).ues-dashboard",
|
|
132
|
+
":(exclude).ues-sandboxes",
|
|
133
|
+
":(exclude).ues-cache",
|
|
134
|
+
":(exclude).ues-traces",
|
|
135
|
+
]
|
|
136
|
+
const commands = [
|
|
137
|
+
["status", "--porcelain=v1", "--untracked-files=all", ...scope],
|
|
138
|
+
["diff", "--binary", "--no-ext-diff", ...scope],
|
|
139
|
+
["diff", "--cached", "--binary", "--no-ext-diff", ...scope],
|
|
140
|
+
]
|
|
141
|
+
const parts = []
|
|
142
|
+
for (const args of commands) {
|
|
143
|
+
const result = spawnSync("git", args, {
|
|
144
|
+
cwd: root,
|
|
145
|
+
encoding: "utf8",
|
|
146
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
147
|
+
})
|
|
148
|
+
if (result.status !== 0) return stableRuntimeHash("non-git:" + root)
|
|
149
|
+
parts.push(result.stdout || "")
|
|
150
|
+
}
|
|
151
|
+
return stableRuntimeHash(parts.join("\n---UES-WORKSPACE-SIGNAL---\n"))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sessionContextDigest(messages) {
|
|
155
|
+
const recent = Array.isArray(messages) ? messages.slice(-12) : messages
|
|
156
|
+
return stableRuntimeHash(recent || [])
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function policySkills(policy) {
|
|
160
|
+
const selected = []
|
|
161
|
+
const add = (id) => { if (id && !selected.includes(id)) selected.push(id) }
|
|
162
|
+
if (policy?.mode === "long-horizon") {
|
|
163
|
+
add("ues-engineering-orchestrator")
|
|
164
|
+
add("ues-long-task-state")
|
|
165
|
+
add("ues-task-planner")
|
|
166
|
+
} else if (policy?.mode === "standard" || policy?.risk === "high") {
|
|
167
|
+
add("ues-engineering-orchestrator")
|
|
168
|
+
}
|
|
169
|
+
const map = {
|
|
170
|
+
"auth-security": "ues-auth-security",
|
|
171
|
+
payment: "ues-payment-engineering",
|
|
172
|
+
database: "ues-database-engineering",
|
|
173
|
+
"api-contract": "ues-api-contract",
|
|
174
|
+
"react-native": "ues-react-native-engineering",
|
|
175
|
+
nextjs: "ues-nextjs-engineering",
|
|
176
|
+
react: "ues-react-engineering",
|
|
177
|
+
devops: "ues-devops-engineering",
|
|
178
|
+
}
|
|
179
|
+
for (const domain of policy?.domains || []) add(map[domain])
|
|
180
|
+
if (policy?.risk === "high") add("ues-change-impact-analysis")
|
|
181
|
+
return selected
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function projectRoutingFacts(projectRoot) {
|
|
185
|
+
let inspected = null
|
|
186
|
+
let learning = null
|
|
187
|
+
try { inspected = runOcskillJSON(["inspect", projectRoot], projectRoot) } catch {}
|
|
188
|
+
try { learning = runOcskillJSON(["learn", "status", projectRoot], projectRoot) } catch {}
|
|
189
|
+
|
|
190
|
+
const feedbackDomains = []
|
|
191
|
+
for (const item of learning?.accepted || []) {
|
|
192
|
+
if (item.status !== "promoted") continue
|
|
193
|
+
const learned = classifyIntent(item.candidateRule || item.recommendation || item.title || "")
|
|
194
|
+
for (const domain of learned.domains || []) {
|
|
195
|
+
if (!feedbackDomains.includes(domain)) feedbackDomains.push(domain)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
repoStacks: inspected?.stack?.stacks || [],
|
|
201
|
+
feedbackDomains,
|
|
202
|
+
acceptedLearningCount: (learning?.accepted || []).filter((item) => item.status === "promoted").length,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
71
206
|
export default Plugin.define({
|
|
72
207
|
id: "ues-router",
|
|
73
208
|
async setup(ctx) {
|
|
74
209
|
const projectRoot = ctx.location.project?.canonical || ctx.location.directory
|
|
75
210
|
const capabilities = runtimeCapabilities(ctx)
|
|
211
|
+
const runtimeGuard = createRuntimeGuard({ duplicateLimit: 3, loopLimit: 6 })
|
|
212
|
+
const sessionAssignments = new Map()
|
|
213
|
+
const eventController = new AbortController()
|
|
214
|
+
|
|
215
|
+
if (typeof ctx.event?.subscribe === "function") {
|
|
216
|
+
void (async () => {
|
|
217
|
+
try {
|
|
218
|
+
for await (const event of ctx.event.subscribe({ signal: eventController.signal })) {
|
|
219
|
+
if (!["message.part.updated", "message.part.delta", "message.updated"].includes(event?.type)) continue
|
|
220
|
+
const properties = event?.properties || {}
|
|
221
|
+
const sessionID =
|
|
222
|
+
properties.sessionID ||
|
|
223
|
+
properties.part?.sessionID ||
|
|
224
|
+
properties.info?.sessionID ||
|
|
225
|
+
null
|
|
226
|
+
if (sessionID && sessionAssignments.has(sessionID)) {
|
|
227
|
+
runtimeGuard.touch(sessionID)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch {}
|
|
231
|
+
})()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const leaseSupervisor = setInterval(() => {
|
|
235
|
+
const workRoot = path.join(projectRoot, ".ues-work")
|
|
236
|
+
if (!existsSync(workRoot)) return
|
|
237
|
+
let entries = []
|
|
238
|
+
try {
|
|
239
|
+
entries = readdirSync(workRoot, { withFileTypes: true })
|
|
240
|
+
} catch {
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
if (!entry.isDirectory() || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.name)) continue
|
|
245
|
+
try {
|
|
246
|
+
runOcskill(["work", "recover", entry.name, projectRoot], projectRoot)
|
|
247
|
+
} catch {}
|
|
248
|
+
}
|
|
249
|
+
}, 60_000)
|
|
250
|
+
leaseSupervisor.unref?.()
|
|
251
|
+
|
|
252
|
+
async function waitForExecutorProgress(sessionID, options = {}) {
|
|
253
|
+
const timeoutMs = Math.max(30_000, Number(options.timeoutMs || 10 * 60_000))
|
|
254
|
+
const stallMs = Math.max(30_000, Math.min(Number(options.stallMs || 60_000), 5 * 60_000))
|
|
255
|
+
const startedAt = Date.now()
|
|
256
|
+
let lastContext = null
|
|
257
|
+
let polling = false
|
|
258
|
+
let rejectWatchdog = null
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const initial = await ctx.session.context({ sessionID })
|
|
262
|
+
lastContext = sessionContextDigest(initial)
|
|
263
|
+
} catch {}
|
|
264
|
+
runtimeGuard.touch(sessionID)
|
|
265
|
+
|
|
266
|
+
const watchdog = new Promise((_, reject) => {
|
|
267
|
+
rejectWatchdog = reject
|
|
268
|
+
})
|
|
269
|
+
const timer = setInterval(async () => {
|
|
270
|
+
if (polling) return
|
|
271
|
+
polling = true
|
|
272
|
+
try {
|
|
273
|
+
const now = Date.now()
|
|
274
|
+
if (now - startedAt >= timeoutMs) {
|
|
275
|
+
const error = new Error("UES executor timed out after " + timeoutMs + "ms")
|
|
276
|
+
error.code = "UES_TIMEOUT"
|
|
277
|
+
rejectWatchdog?.(error)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const messages = await ctx.session.context({ sessionID })
|
|
283
|
+
const digest = sessionContextDigest(messages)
|
|
284
|
+
if (lastContext === null || digest !== lastContext) {
|
|
285
|
+
lastContext = digest
|
|
286
|
+
runtimeGuard.touch(sessionID, now)
|
|
287
|
+
}
|
|
288
|
+
} catch {}
|
|
289
|
+
|
|
290
|
+
const snapshot = runtimeGuard.snapshot(sessionID)
|
|
291
|
+
const watchdogState = progressWatchdogDecision(snapshot, now, stallMs)
|
|
292
|
+
if (watchdogState.stalled) {
|
|
293
|
+
const error = new Error("UES no-progress watchdog: executor stalled for " + watchdogState.idleMs + "ms")
|
|
294
|
+
error.code = "UES_STALLED"
|
|
295
|
+
rejectWatchdog?.(error)
|
|
296
|
+
}
|
|
297
|
+
} finally {
|
|
298
|
+
polling = false
|
|
299
|
+
}
|
|
300
|
+
}, 5_000)
|
|
301
|
+
timer.unref?.()
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
await Promise.race([
|
|
305
|
+
ctx.session.wait({ sessionID }),
|
|
306
|
+
watchdog,
|
|
307
|
+
])
|
|
308
|
+
} finally {
|
|
309
|
+
clearInterval(timer)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const assignment = sessionAssignments.get(sessionID)
|
|
313
|
+
if (assignment?.resumeRequired) {
|
|
314
|
+
const error = new Error("UES no-progress post-compaction resume: checkpoint.nextAction was not executed before session completion")
|
|
315
|
+
error.code = "UES_RESUME_STALLED"
|
|
316
|
+
throw error
|
|
317
|
+
}
|
|
318
|
+
return ctx.session.context({ sessionID })
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (typeof ctx.tool?.hook === "function") {
|
|
322
|
+
await ctx.tool.hook("execute.before", (event) => {
|
|
323
|
+
const assignment = sessionAssignments.get(event.sessionID)
|
|
324
|
+
if (!assignment) return
|
|
325
|
+
const signal = workspaceSignal(assignment.executionDir || projectRoot)
|
|
326
|
+
const decision = runtimeGuard.before({
|
|
327
|
+
sessionID: event.sessionID,
|
|
328
|
+
tool: event.tool,
|
|
329
|
+
input: event.input,
|
|
330
|
+
callID: event.callID,
|
|
331
|
+
cwd: assignment.executionDir || projectRoot,
|
|
332
|
+
workspaceSignal: signal,
|
|
333
|
+
})
|
|
334
|
+
if (decision.blocked) throw new Error(decision.message)
|
|
335
|
+
|
|
336
|
+
if (assignment.resumeRequired) {
|
|
337
|
+
try {
|
|
338
|
+
const args = [
|
|
339
|
+
"work", "checkpoint-resumed", assignment.slug, assignment.task, projectRoot,
|
|
340
|
+
"--run-id", assignment.runId,
|
|
341
|
+
"--reason", "tool-action-observed-after-compaction",
|
|
342
|
+
]
|
|
343
|
+
runOcskill(args, projectRoot)
|
|
344
|
+
} catch {}
|
|
345
|
+
assignment.resumeRequired = false
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
await ctx.tool.hook("execute.after", (event) => {
|
|
350
|
+
const assignment = sessionAssignments.get(event.sessionID)
|
|
351
|
+
if (!assignment) return
|
|
352
|
+
const signal = workspaceSignal(assignment.executionDir || projectRoot)
|
|
353
|
+
if (event.status === "completed") {
|
|
354
|
+
const shellInput = JSON.stringify(event.input || {})
|
|
355
|
+
const budgetTool =
|
|
356
|
+
event.tool === "bash" && /(?:ocskill\s+repo-graph|\brg\b|\bgrep\b|\bglob\b)/i.test(shellInput)
|
|
357
|
+
? "repo-graph"
|
|
358
|
+
: event.tool
|
|
359
|
+
event.result = budgetToolResult(budgetTool, event.result)
|
|
360
|
+
runtimeGuard.after({
|
|
361
|
+
sessionID: event.sessionID,
|
|
362
|
+
tool: event.tool,
|
|
363
|
+
input: event.input,
|
|
364
|
+
callID: event.callID,
|
|
365
|
+
cwd: assignment.executionDir || projectRoot,
|
|
366
|
+
workspaceSignal: signal,
|
|
367
|
+
status: "completed",
|
|
368
|
+
result: event.result,
|
|
369
|
+
})
|
|
370
|
+
} else {
|
|
371
|
+
runtimeGuard.after({
|
|
372
|
+
sessionID: event.sessionID,
|
|
373
|
+
tool: event.tool,
|
|
374
|
+
input: event.input,
|
|
375
|
+
callID: event.callID,
|
|
376
|
+
cwd: assignment.executionDir || projectRoot,
|
|
377
|
+
workspaceSignal: signal,
|
|
378
|
+
status: "error",
|
|
379
|
+
error: event.error,
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (capabilities.sessionHook) {
|
|
386
|
+
await ctx.session.hook("retry", (event) => {
|
|
387
|
+
const kind = classifyProviderFailure(event.error || {})
|
|
388
|
+
if (kind === "AUTH" || kind === "CONTEXT_TOO_LARGE") {
|
|
389
|
+
event.decision = { retry: false }
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
if (["RATE_LIMIT", "PROVIDER_5XX", "TIMEOUT", "NO_TOKEN"].includes(kind) && event.attempt < 3) {
|
|
393
|
+
event.decision = {
|
|
394
|
+
retry: true,
|
|
395
|
+
delay: kind === "RATE_LIMIT" ? Math.min(10_000, 1_000 * event.attempt) : 0,
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
await ctx.session.hook("compaction", (event) => {
|
|
401
|
+
const assignment = sessionAssignments.get(event.sessionID)
|
|
402
|
+
if (!assignment) return
|
|
403
|
+
try {
|
|
404
|
+
const checkpoint = runOcskillJSON([
|
|
405
|
+
"work", "checkpoint", assignment.slug, assignment.task, projectRoot,
|
|
406
|
+
"--run-id", assignment.runId,
|
|
407
|
+
"--reason", "pre-compaction",
|
|
408
|
+
], projectRoot)
|
|
409
|
+
assignment.checkpoint = checkpoint
|
|
410
|
+
assignment.resumeRequired = true
|
|
411
|
+
assignment.compactionAt = Date.now()
|
|
412
|
+
runtimeGuard.compacted(event.sessionID, assignment.compactionAt)
|
|
413
|
+
event.system.push({
|
|
414
|
+
type: "text",
|
|
415
|
+
text:
|
|
416
|
+
"UES durable checkpoint persisted before compaction. Preserve currentTaskId/runId/planHash/evidence pointers. " +
|
|
417
|
+
"After compaction, execute checkpoint.nextAction before explanatory prose. Checkpoint: " +
|
|
418
|
+
JSON.stringify(checkpoint),
|
|
419
|
+
})
|
|
420
|
+
} catch {}
|
|
421
|
+
})
|
|
422
|
+
}
|
|
76
423
|
|
|
77
424
|
await ctx.tool.transform((editor) => {
|
|
78
425
|
editor.namespace({
|
|
@@ -104,6 +451,75 @@ export default Plugin.define({
|
|
|
104
451
|
content: runOcskill(["task-policy", input.text], projectRoot),
|
|
105
452
|
}),
|
|
106
453
|
})
|
|
454
|
+
editor.add({
|
|
455
|
+
name: "semantic_search",
|
|
456
|
+
description: "Search the persistent incremental semantic index. Returns bounded path/symbol/reference evidence; never treats lexical evidence as semantic proof.",
|
|
457
|
+
input: {
|
|
458
|
+
type: "object",
|
|
459
|
+
properties: {
|
|
460
|
+
query: { type: "string" },
|
|
461
|
+
limit: { type: "integer", minimum: 1, maximum: 50 },
|
|
462
|
+
},
|
|
463
|
+
required: ["query"],
|
|
464
|
+
additionalProperties: false,
|
|
465
|
+
},
|
|
466
|
+
options: { namespace: "ues", codemode: true },
|
|
467
|
+
execute: async (input) => ({
|
|
468
|
+
content: runOcskill(["aci", "search", input.query, projectRoot, "--limit", String(input.limit || 20)], projectRoot),
|
|
469
|
+
}),
|
|
470
|
+
})
|
|
471
|
+
editor.add({
|
|
472
|
+
name: "references",
|
|
473
|
+
description: "Find bounded syntax-aware lexical references and concrete definition lines for one identifier.",
|
|
474
|
+
input: {
|
|
475
|
+
type: "object",
|
|
476
|
+
properties: {
|
|
477
|
+
symbol: { type: "string" },
|
|
478
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
479
|
+
},
|
|
480
|
+
required: ["symbol"],
|
|
481
|
+
additionalProperties: false,
|
|
482
|
+
},
|
|
483
|
+
options: { namespace: "ues", codemode: true },
|
|
484
|
+
execute: async (input) => ({
|
|
485
|
+
content: runOcskill(["aci", "refs", input.symbol, projectRoot, "--limit", String(input.limit || 40)], projectRoot),
|
|
486
|
+
}),
|
|
487
|
+
})
|
|
488
|
+
editor.add({
|
|
489
|
+
name: "view_file",
|
|
490
|
+
description: "Read a bounded line-numbered window from a repository file; refuses root escapes and oversized/binary files.",
|
|
491
|
+
input: {
|
|
492
|
+
type: "object",
|
|
493
|
+
properties: {
|
|
494
|
+
file: { type: "string" },
|
|
495
|
+
line: { type: "integer", minimum: 1 },
|
|
496
|
+
lines: { type: "integer", minimum: 1, maximum: 240 },
|
|
497
|
+
},
|
|
498
|
+
required: ["file"],
|
|
499
|
+
additionalProperties: false,
|
|
500
|
+
},
|
|
501
|
+
options: { namespace: "ues", codemode: true },
|
|
502
|
+
execute: async (input) => ({
|
|
503
|
+
content: runOcskill([
|
|
504
|
+
"aci", "view", input.file, projectRoot,
|
|
505
|
+
"--line", String(input.line || 1),
|
|
506
|
+
"--lines", String(input.lines || 120),
|
|
507
|
+
], projectRoot),
|
|
508
|
+
}),
|
|
509
|
+
})
|
|
510
|
+
editor.add({
|
|
511
|
+
name: "sandbox_capability",
|
|
512
|
+
description: "Report whether fail-closed Docker/Podman verification isolation is actually available. Never assumes a container runtime exists.",
|
|
513
|
+
input: {
|
|
514
|
+
type: "object",
|
|
515
|
+
properties: {},
|
|
516
|
+
additionalProperties: false,
|
|
517
|
+
},
|
|
518
|
+
options: { namespace: "ues", codemode: true },
|
|
519
|
+
execute: async () => ({
|
|
520
|
+
content: runOcskill(["sandbox", "capability"], projectRoot),
|
|
521
|
+
}),
|
|
522
|
+
})
|
|
107
523
|
editor.add({
|
|
108
524
|
name: "work_status",
|
|
109
525
|
description: "Read durable status for a UES .ues-work item without modifying it.",
|
|
@@ -149,14 +565,98 @@ export default Plugin.define({
|
|
|
149
565
|
content: runOcskill(["context-pack", input.slug, input.task, projectRoot], projectRoot),
|
|
150
566
|
}),
|
|
151
567
|
})
|
|
568
|
+
editor.add({
|
|
569
|
+
name: "recover_task",
|
|
570
|
+
description: "Safely recover one stale UES task attempt. If an attached executor session still exists, interrupt it before releasing the durable lease.",
|
|
571
|
+
input: {
|
|
572
|
+
type: "object",
|
|
573
|
+
properties: {
|
|
574
|
+
slug: { type: "string" },
|
|
575
|
+
task: { type: "string" },
|
|
576
|
+
force: { type: "boolean" },
|
|
577
|
+
reason: { type: "string" },
|
|
578
|
+
},
|
|
579
|
+
required: ["slug", "task"],
|
|
580
|
+
additionalProperties: false,
|
|
581
|
+
},
|
|
582
|
+
options: { namespace: "ues", codemode: true },
|
|
583
|
+
execute: async (input) => {
|
|
584
|
+
const status = runOcskillJSON(["work", "status", input.slug, projectRoot], projectRoot)
|
|
585
|
+
const running = (status.running || []).find((item) => item.taskID === input.task)
|
|
586
|
+
if (!running) throw new Error("task is not currently running: " + input.task)
|
|
587
|
+
|
|
588
|
+
let interrupted = false
|
|
589
|
+
if (running.sessionID && capabilities.sessionInterrupt) {
|
|
590
|
+
try {
|
|
591
|
+
await ctx.session.interrupt({ sessionID: running.sessionID, continue: false })
|
|
592
|
+
interrupted = true
|
|
593
|
+
} catch {}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const recoverArgs = ["work", "recover-task", input.slug, input.task, projectRoot]
|
|
597
|
+
if (input.force) recoverArgs.push("--force")
|
|
598
|
+
if (input.reason) recoverArgs.push("--reason", input.reason)
|
|
599
|
+
const recovered = runOcskillJSON(recoverArgs, projectRoot)
|
|
600
|
+
return {
|
|
601
|
+
content: JSON.stringify({
|
|
602
|
+
interrupted,
|
|
603
|
+
sessionID: running.sessionID || null,
|
|
604
|
+
recovered,
|
|
605
|
+
}, null, 2),
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
})
|
|
609
|
+
editor.add({
|
|
610
|
+
name: "cancel_task",
|
|
611
|
+
description: "Interrupt a running UES executor session and mark its durable task attempt failed.",
|
|
612
|
+
input: {
|
|
613
|
+
type: "object",
|
|
614
|
+
properties: {
|
|
615
|
+
slug: { type: "string" },
|
|
616
|
+
task: { type: "string" },
|
|
617
|
+
reason: { type: "string" },
|
|
618
|
+
},
|
|
619
|
+
required: ["slug", "task"],
|
|
620
|
+
additionalProperties: false,
|
|
621
|
+
},
|
|
622
|
+
options: { namespace: "ues", codemode: true },
|
|
623
|
+
execute: async (input) => {
|
|
624
|
+
if (!capabilities.sessionInterrupt) {
|
|
625
|
+
throw new Error("OpenCode runtime does not expose session.interrupt")
|
|
626
|
+
}
|
|
627
|
+
const status = runOcskillJSON(["work", "status", input.slug, projectRoot], projectRoot)
|
|
628
|
+
const running = (status.running || []).find((item) => item.taskID === input.task)
|
|
629
|
+
if (!running) throw new Error("task is not currently running: " + input.task)
|
|
630
|
+
if (!running.sessionID) throw new Error("running task has no attached executor session")
|
|
631
|
+
await ctx.session.interrupt({ sessionID: running.sessionID, continue: false })
|
|
632
|
+
const failArgs = [
|
|
633
|
+
"work", "fail", input.slug, input.task, projectRoot,
|
|
634
|
+
"--reason", input.reason || "cancelled by user/runtime",
|
|
635
|
+
]
|
|
636
|
+
if (running.runId) failArgs.push("--run-id", running.runId)
|
|
637
|
+
const failed = runOcskillJSON(failArgs, projectRoot)
|
|
638
|
+
return {
|
|
639
|
+
content: JSON.stringify({
|
|
640
|
+
interrupted: true,
|
|
641
|
+
sessionID: running.sessionID,
|
|
642
|
+
task: input.task,
|
|
643
|
+
state: failed,
|
|
644
|
+
}, null, 2),
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
})
|
|
152
648
|
editor.add({
|
|
153
649
|
name: "dispatch_task",
|
|
154
|
-
description: "Start one approved UES task and execute it in a fresh ues-executor session
|
|
650
|
+
description: "Start one approved UES task and execute it in a fresh ues-executor session with bounded runtime and interrupt-on-timeout. The parent must inspect the diff and record completion evidence separately.",
|
|
155
651
|
input: {
|
|
156
652
|
type: "object",
|
|
157
653
|
properties: {
|
|
158
654
|
slug: { type: "string" },
|
|
159
655
|
task: { type: "string" },
|
|
656
|
+
timeoutMs: { type: "integer", minimum: 30000, maximum: 3600000 },
|
|
657
|
+
stallMs: { type: "integer", minimum: 30000, maximum: 300000 },
|
|
658
|
+
isolate: { type: "boolean" },
|
|
659
|
+
integrate: { type: "boolean" },
|
|
160
660
|
},
|
|
161
661
|
required: ["slug", "task"],
|
|
162
662
|
additionalProperties: false,
|
|
@@ -170,15 +670,61 @@ export default Plugin.define({
|
|
|
170
670
|
const started = runOcskillJSON(["work", "start", input.slug, input.task, projectRoot], projectRoot)
|
|
171
671
|
const attempt = started?.record?.attempts || 1
|
|
172
672
|
const runId = started?.record?.runId || null
|
|
673
|
+
const traceID = "dispatch-" + input.slug + "-" + input.task + "-" + String(runId || Date.now()).replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
173
674
|
const taskText = [
|
|
174
675
|
started?.contextPack?.task?.title,
|
|
175
676
|
started?.contextPack?.task?.summary,
|
|
176
677
|
...(started?.contextPack?.task?.acceptance || []),
|
|
678
|
+
started?.contextPack?.task?.risk ? "risk: " + started.contextPack.task.risk : null,
|
|
177
679
|
].filter(Boolean).join(" ")
|
|
178
680
|
const taskPolicy = runOcskillJSON(["task-policy", taskText], projectRoot)
|
|
681
|
+
appendTrace(traceID, "dispatch.started", {
|
|
682
|
+
slug: input.slug,
|
|
683
|
+
task: input.task,
|
|
684
|
+
runId,
|
|
685
|
+
attempt,
|
|
686
|
+
taskText,
|
|
687
|
+
taskPolicy,
|
|
688
|
+
context: {
|
|
689
|
+
files: started?.contextPack?.contextManifest?.files || [],
|
|
690
|
+
strategy: started?.contextPack?.contextManifest?.strategy || null,
|
|
691
|
+
used: started?.contextPack?.contextManifest?.used || 0,
|
|
692
|
+
budget: started?.contextPack?.contextManifest?.budget || 0,
|
|
693
|
+
},
|
|
694
|
+
}, projectRoot)
|
|
695
|
+
const timeoutMs = Math.max(
|
|
696
|
+
30_000,
|
|
697
|
+
Math.min(Number(input.timeoutMs || (taskPolicy.mode === "long-horizon" ? 20 * 60_000 : 10 * 60_000)), 60 * 60_000),
|
|
698
|
+
)
|
|
699
|
+
const stallMs = Math.max(
|
|
700
|
+
30_000,
|
|
701
|
+
Math.min(Number(input.stallMs || 60_000), 5 * 60_000),
|
|
702
|
+
)
|
|
179
703
|
const policyArgs = ["model-policy", "executor", "--attempt", String(attempt)]
|
|
180
704
|
if (taskText) policyArgs.push("--text", taskText)
|
|
181
705
|
const policy = runOcskillJSON(policyArgs, projectRoot)
|
|
706
|
+
const workStatus = runOcskillJSON(["work", "status", input.slug, projectRoot], projectRoot)
|
|
707
|
+
const workingTree = runOcskillJSON(["working-tree", projectRoot], projectRoot)
|
|
708
|
+
const rootClean = workingTree?.git === true && workingTree?.clean === true
|
|
709
|
+
const writerTask = taskHasWrites(started?.contextPack?.task)
|
|
710
|
+
if (input.isolate === true && !rootClean) {
|
|
711
|
+
throw new Error("explicit sandbox isolation requires a clean root working tree; commit/stash or integrate existing changes first")
|
|
712
|
+
}
|
|
713
|
+
if (input.isolate !== false && writerTask && !rootClean) {
|
|
714
|
+
throw new Error("writer dispatch requires a clean root for automatic worktree isolation; clean the root or explicitly pass isolate:false to accept shared-root writes")
|
|
715
|
+
}
|
|
716
|
+
const autoIsolate =
|
|
717
|
+
input.isolate === true ||
|
|
718
|
+
(input.isolate !== false && rootClean && writerTask)
|
|
719
|
+
let sandbox = null
|
|
720
|
+
let executionDir = projectRoot
|
|
721
|
+
if (autoIsolate) {
|
|
722
|
+
sandbox = runOcskillJSON(
|
|
723
|
+
["sandbox", "create", input.slug, input.task, projectRoot],
|
|
724
|
+
projectRoot,
|
|
725
|
+
)
|
|
726
|
+
executionDir = sandbox.dir
|
|
727
|
+
}
|
|
182
728
|
const heartbeatStarted = Date.now()
|
|
183
729
|
const heartbeat = setInterval(() => {
|
|
184
730
|
try {
|
|
@@ -190,35 +736,192 @@ export default Plugin.define({
|
|
|
190
736
|
}, 30_000)
|
|
191
737
|
|
|
192
738
|
try {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
739
|
+
const escalationArgs = ["model-policy", "executor", "--attempt", String(attempt + 1)]
|
|
740
|
+
if (taskText) escalationArgs.push("--text", taskText)
|
|
741
|
+
const escalatedPolicy = runOcskillJSON(escalationArgs, projectRoot)
|
|
742
|
+
let selectedPolicy = policy
|
|
743
|
+
let created = null
|
|
744
|
+
let messages = null
|
|
745
|
+
const providerRecovery = []
|
|
746
|
+
let physicalAttempt = 0
|
|
747
|
+
|
|
748
|
+
while (physicalAttempt < 3) {
|
|
749
|
+
physicalAttempt += 1
|
|
750
|
+
created = await ctx.session.create({
|
|
751
|
+
title: "UES " + input.slug + " " + input.task + " p" + physicalAttempt,
|
|
752
|
+
location: { directory: executionDir },
|
|
753
|
+
})
|
|
754
|
+
sessionAssignments.set(created.id, {
|
|
755
|
+
slug: input.slug,
|
|
756
|
+
task: input.task,
|
|
757
|
+
runId,
|
|
758
|
+
executionDir,
|
|
759
|
+
sandboxDir: sandbox?.dir || null,
|
|
760
|
+
resumeRequired: false,
|
|
761
|
+
checkpoint: null,
|
|
762
|
+
compactionAt: null,
|
|
763
|
+
})
|
|
764
|
+
runtimeGuard.touch(created.id)
|
|
765
|
+
|
|
766
|
+
appendTrace(traceID, "dispatch.session-created", {
|
|
767
|
+
sessionID: created.id,
|
|
768
|
+
executionDir,
|
|
769
|
+
isolated: executionDir !== projectRoot,
|
|
770
|
+
physicalAttempt,
|
|
771
|
+
model: selectedPolicy?.model || null,
|
|
772
|
+
}, projectRoot)
|
|
773
|
+
|
|
774
|
+
{
|
|
775
|
+
const attachArgs = [
|
|
776
|
+
"work", "attach-session", input.slug, input.task, projectRoot,
|
|
777
|
+
"--session-id", created.id,
|
|
778
|
+
]
|
|
779
|
+
if (runId) attachArgs.push("--run-id", runId)
|
|
780
|
+
attachArgs.push("--execution-dir", executionDir)
|
|
781
|
+
if (sandbox?.dir) attachArgs.push("--sandbox-dir", sandbox.dir)
|
|
782
|
+
runOcskill(attachArgs, projectRoot)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
await ctx.session.switchAgent({ sessionID: created.id, agent: "ues-executor" })
|
|
786
|
+
const selectedModel = modelRef(selectedPolicy?.model)
|
|
787
|
+
if (selectedModel) {
|
|
788
|
+
await ctx.session.switchModel({ sessionID: created.id, model: selectedModel })
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const recovery = started?.contextPack?.contextPolicy?.recovery
|
|
792
|
+
const recoveryText = recovery?.requireDiagnosis
|
|
793
|
+
? " This is recovery attempt " + attempt + ". Diagnose the previous failure from fresh evidence before editing. " +
|
|
794
|
+
(recovery.directives || []).join("; ") + "."
|
|
795
|
+
: ""
|
|
796
|
+
const providerRecoveryText = providerRecovery.length
|
|
797
|
+
? " Provider/session recovery is active. Continue from durable .ues-work state and current workspace evidence; do not repeat already-proven exploration."
|
|
798
|
+
: ""
|
|
799
|
+
const dispatchContext = physicalAttempt === 1
|
|
800
|
+
? started.contextPack
|
|
801
|
+
: runOcskillJSON(["context-pack", input.slug, input.task, projectRoot], projectRoot)
|
|
802
|
+
|
|
803
|
+
await ctx.session.prompt({
|
|
804
|
+
sessionID: created.id,
|
|
805
|
+
text:
|
|
806
|
+
"Implement exactly this approved UES task in the current repository. " +
|
|
807
|
+
"Do not broaden scope or launch child agents. Run the declared verification and return the executor report." +
|
|
808
|
+
recoveryText + providerRecoveryText + "\n\n" +
|
|
809
|
+
JSON.stringify(dispatchContext, null, 2),
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
try {
|
|
813
|
+
messages = await waitForExecutorProgress(created.id, { timeoutMs, stallMs })
|
|
814
|
+
sessionAssignments.delete(created.id)
|
|
815
|
+
runtimeGuard.clear(created.id)
|
|
816
|
+
break
|
|
817
|
+
} catch (error) {
|
|
818
|
+
try {
|
|
819
|
+
await ctx.session.interrupt({ sessionID: created.id, continue: false })
|
|
820
|
+
} catch {}
|
|
821
|
+
|
|
822
|
+
const failureKind = classifyProviderFailure(error)
|
|
823
|
+
const escalationModel = modelRef(escalatedPolicy?.model)
|
|
824
|
+
const hasEscalationModel = Boolean(
|
|
825
|
+
escalationModel &&
|
|
826
|
+
escalatedPolicy?.model &&
|
|
827
|
+
escalatedPolicy.model !== selectedPolicy?.model,
|
|
828
|
+
)
|
|
829
|
+
const decision = providerRecoveryPlan(failureKind, physicalAttempt, {
|
|
830
|
+
hasEscalationModel,
|
|
831
|
+
})
|
|
832
|
+
providerRecovery.push({
|
|
833
|
+
physicalAttempt,
|
|
834
|
+
sessionID: created.id,
|
|
835
|
+
failureKind,
|
|
836
|
+
action: decision.action,
|
|
837
|
+
model: selectedPolicy?.model || null,
|
|
838
|
+
})
|
|
839
|
+
appendTrace(traceID, "dispatch.provider-recovery", {
|
|
840
|
+
task: input.task,
|
|
841
|
+
runId,
|
|
842
|
+
physicalAttempt,
|
|
843
|
+
sessionID: created.id,
|
|
844
|
+
failureKind,
|
|
845
|
+
decision,
|
|
846
|
+
model: selectedPolicy?.model || null,
|
|
847
|
+
escalationModel: escalatedPolicy?.model || null,
|
|
848
|
+
}, projectRoot)
|
|
849
|
+
|
|
850
|
+
sessionAssignments.delete(created.id)
|
|
851
|
+
runtimeGuard.clear(created.id)
|
|
852
|
+
|
|
853
|
+
if (!decision.retry || physicalAttempt >= 3) throw error
|
|
854
|
+
if (decision.action === "fresh-session-escalated-model") {
|
|
855
|
+
selectedPolicy = escalatedPolicy
|
|
856
|
+
}
|
|
857
|
+
await tool.progress({
|
|
858
|
+
status:
|
|
859
|
+
"provider recovery " + decision.action +
|
|
860
|
+
" after " + failureKind.toLowerCase().replaceAll("_", "-"),
|
|
861
|
+
})
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (!created || !messages) {
|
|
866
|
+
throw new Error("UES provider recovery exhausted without a completed executor session")
|
|
198
867
|
}
|
|
199
868
|
|
|
200
|
-
|
|
869
|
+
const afterWait = runOcskillJSON(["work", "status", input.slug, projectRoot], projectRoot)
|
|
870
|
+
const activeAttempt = (afterWait.running || []).find(
|
|
871
|
+
(item) => item.taskID === input.task && (!runId || item.runId === runId),
|
|
872
|
+
)
|
|
873
|
+
if (!activeAttempt) {
|
|
874
|
+
throw new Error("UES executor attempt is no longer active; refusing post-cancel integration or completion handoff")
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
appendTrace(traceID, "dispatch.completed", {
|
|
201
878
|
sessionID: created.id,
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
|
|
879
|
+
task: input.task,
|
|
880
|
+
runId,
|
|
881
|
+
messageCount: Array.isArray(messages) ? messages.length : null,
|
|
882
|
+
providerRecovery,
|
|
883
|
+
}, projectRoot)
|
|
884
|
+
let integration = null
|
|
885
|
+
if (sandbox && input.integrate === true) {
|
|
886
|
+
integration = runOcskillJSON(
|
|
887
|
+
["sandbox", "integrate", sandbox.dir, projectRoot],
|
|
888
|
+
projectRoot,
|
|
889
|
+
)
|
|
890
|
+
sandbox = null
|
|
891
|
+
}
|
|
209
892
|
return {
|
|
210
893
|
content: JSON.stringify({
|
|
211
894
|
sessionID: created.id,
|
|
212
895
|
task: input.task,
|
|
213
896
|
attempt,
|
|
214
897
|
runId,
|
|
898
|
+
traceID,
|
|
215
899
|
taskPolicy,
|
|
216
|
-
model:
|
|
900
|
+
model: selectedPolicy,
|
|
901
|
+
timeoutMs,
|
|
902
|
+
stallMs,
|
|
903
|
+
executionDir,
|
|
904
|
+
isolated: executionDir !== projectRoot,
|
|
905
|
+
sandbox,
|
|
906
|
+
integration,
|
|
907
|
+
providerRecovery,
|
|
217
908
|
messages: messageExcerpt(messages),
|
|
218
|
-
next:
|
|
909
|
+
next: sandbox
|
|
910
|
+
? "Inspect and verify the isolated worktree first. If accepted, integrate it with ocskill sandbox integrate <worktree> . before recording work complete."
|
|
911
|
+
: "Inspect the child diff and verification, then call ocskill work complete or fail.",
|
|
219
912
|
}, null, 2),
|
|
220
913
|
}
|
|
221
914
|
} catch (error) {
|
|
915
|
+
appendTrace(traceID, "dispatch.failed", {
|
|
916
|
+
task: input.task,
|
|
917
|
+
runId,
|
|
918
|
+
error: String(error?.message || error),
|
|
919
|
+
}, projectRoot)
|
|
920
|
+
if (sandbox?.dir) {
|
|
921
|
+
try {
|
|
922
|
+
runOcskill(["sandbox", "remove", sandbox.dir, projectRoot, "--force", "--delete-branch"], projectRoot)
|
|
923
|
+
} catch {}
|
|
924
|
+
}
|
|
222
925
|
try {
|
|
223
926
|
const failArgs = [
|
|
224
927
|
"work", "fail", input.slug, input.task, projectRoot,
|
|
@@ -235,19 +938,44 @@ export default Plugin.define({
|
|
|
235
938
|
})
|
|
236
939
|
})
|
|
237
940
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
941
|
+
const routingFacts = projectRoutingFacts(projectRoot)
|
|
942
|
+
|
|
943
|
+
if (capabilities.sessionHook) {
|
|
944
|
+
await ctx.session.hook("context", (event) => {
|
|
945
|
+
event.system.push({
|
|
946
|
+
type: "text",
|
|
947
|
+
text: "UES: use the minimum context that preserves correctness. FAST reads the target and nearest evidence with direct skills only; STANDARD/DEEP expand when risk or evidence requires it. Preserve exact contracts, verify fresh behavior, and escalate after failed attempts instead of stacking patches.",
|
|
948
|
+
})
|
|
949
|
+
const assignment = sessionAssignments.get(event.sessionID)
|
|
950
|
+
if (assignment?.resumeRequired && assignment.checkpoint) {
|
|
951
|
+
event.system.push({
|
|
952
|
+
type: "text",
|
|
953
|
+
text:
|
|
954
|
+
"UES post-compaction resume is mandatory. Before explanatory prose, execute this deterministic nextAction now: " +
|
|
955
|
+
JSON.stringify(assignment.checkpoint.nextAction) +
|
|
956
|
+
". Resume only from .ues-work state/evidence and the current workspace; do not replay external/destructive side effects.",
|
|
957
|
+
})
|
|
958
|
+
}
|
|
243
959
|
})
|
|
244
|
-
})
|
|
245
960
|
|
|
246
|
-
|
|
961
|
+
await ctx.session.hook("prompt", (event) => {
|
|
247
962
|
const config = routerConfig()
|
|
248
963
|
if (!config.enabled) return
|
|
249
964
|
|
|
250
|
-
|
|
965
|
+
let policy = null
|
|
966
|
+
try {
|
|
967
|
+
policy = runOcskillJSON(["task-policy", event.prompt.text], projectRoot)
|
|
968
|
+
} catch {}
|
|
969
|
+
const intent = classifyIntent(event.prompt.text, routingFacts)
|
|
970
|
+
const effectiveMaxSkills = Math.max(
|
|
971
|
+
1,
|
|
972
|
+
Math.min(config.maxSkills, Number(policy?.maxSkills || config.maxSkills)),
|
|
973
|
+
)
|
|
974
|
+
const selected = []
|
|
975
|
+
for (const id of [...routeSkillsForPolicy(event.prompt.text, policy, effectiveMaxSkills, routingFacts), ...policySkills(policy)]) {
|
|
976
|
+
if (!selected.includes(id)) selected.push(id)
|
|
977
|
+
if (selected.length >= effectiveMaxSkills) break
|
|
978
|
+
}
|
|
251
979
|
if (selected.length === 0) return
|
|
252
980
|
|
|
253
981
|
event.prompt.skills ??= []
|
|
@@ -261,17 +989,36 @@ export default Plugin.define({
|
|
|
261
989
|
...event.metadata,
|
|
262
990
|
uesRouter: {
|
|
263
991
|
selected,
|
|
264
|
-
|
|
992
|
+
policy,
|
|
993
|
+
intent,
|
|
994
|
+
facts: {
|
|
995
|
+
repoStacks: routingFacts.repoStacks,
|
|
996
|
+
feedbackDomains: routingFacts.feedbackDomains,
|
|
997
|
+
acceptedLearningCount: routingFacts.acceptedLearningCount,
|
|
998
|
+
},
|
|
999
|
+
version: 10,
|
|
1000
|
+
effectiveMaxSkills,
|
|
265
1001
|
},
|
|
266
1002
|
}
|
|
267
1003
|
})
|
|
268
1004
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (capabilities.permissionHook) {
|
|
1008
|
+
await ctx.permission.hook("evaluate", (event) => {
|
|
1009
|
+
if (event.action !== "shell") return
|
|
1010
|
+
const risk = destructiveShellRisk(event.resources.join("\n"))
|
|
1011
|
+
if (!risk.risky) return
|
|
1012
|
+
event.effect = "ask"
|
|
1013
|
+
event.message = "UES safety gate: confirm destructive/high-impact shell action (" + risk.id + ")."
|
|
1014
|
+
})
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
return () => {
|
|
1018
|
+
eventController.abort()
|
|
1019
|
+
clearInterval(leaseSupervisor)
|
|
1020
|
+
for (const sessionID of sessionAssignments.keys()) runtimeGuard.clear(sessionID)
|
|
1021
|
+
sessionAssignments.clear()
|
|
1022
|
+
}
|
|
276
1023
|
},
|
|
277
1024
|
})
|