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.
Files changed (68) hide show
  1. package/CHANGELOG.md +112 -3
  2. package/README.md +396 -281
  3. package/bin/ocskill.mjs +382 -156
  4. package/docs/DETERMINISTIC-TOOLS.md +25 -8
  5. package/docs/ENGINEERING-DESIGN.md +31 -13
  6. package/docs/EVALS.md +34 -12
  7. package/docs/NPM-PUBLISH.md +6 -6
  8. package/docs/OPENCODE-COMPAT.md +11 -8
  9. package/docs/TRACE-SCHEMA.md +15 -2
  10. package/docs/V8-INTELLIGENCE-RELIABILITY.md +206 -0
  11. package/docs/V9-SPEED-INTELLIGENCE.md +102 -0
  12. package/evals/live/tasks.json +6 -6
  13. package/evals/polyglot/fixtures/polyglot-bench/api/generated/client.ts +2 -0
  14. package/evals/polyglot/fixtures/polyglot-bench/api/openapi.json +25 -0
  15. package/evals/polyglot/fixtures/polyglot-bench/db/migrations/20260920_add_order_key.sql +1 -0
  16. package/evals/polyglot/fixtures/polyglot-bench/dotnet/OrderService.cs +8 -0
  17. package/evals/polyglot/fixtures/polyglot-bench/java/PriceService.java +5 -0
  18. package/evals/polyglot/fixtures/polyglot-bench/monorepo/package.json +6 -0
  19. package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/api/package.json +4 -0
  20. package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/web/package.json +7 -0
  21. package/evals/polyglot/fixtures/polyglot-bench/monorepo/pnpm-lock.yaml +5 -0
  22. package/evals/polyglot/fixtures/polyglot-bench/next/app/api/products/route.ts +7 -0
  23. package/evals/polyglot/fixtures/polyglot-bench/python/tenant_auth.py +4 -0
  24. package/evals/polyglot/fixtures/polyglot-bench/react-native/keyboard.ts +3 -0
  25. package/evals/polyglot/graders/polyglot-bench.mjs +101 -0
  26. package/evals/polyglot/tasks.json +54 -0
  27. package/global-config/AGENTS.md +78 -160
  28. package/global-config/agents/integration-verifier.md +1 -1
  29. package/global-config/agents/plan-checker.md +1 -1
  30. package/global-config/commands/run.md +9 -5
  31. package/global-config/plugins/ues-router/capabilities.js +4 -0
  32. package/global-config/plugins/ues-router/index.js +784 -37
  33. package/global-config/plugins/ues-router/router.js +175 -23
  34. package/global-config/plugins/ues-router/runtime-guard.js +265 -0
  35. package/global-config/skills/engineering-orchestrator/references/long-horizon.md +6 -4
  36. package/lib/aci.mjs +128 -0
  37. package/lib/benchmark-confidence.mjs +173 -0
  38. package/lib/cli-utils.mjs +41 -0
  39. package/lib/container-sandbox.mjs +102 -0
  40. package/lib/context-manifest.mjs +300 -22
  41. package/lib/control-center.mjs +36 -3
  42. package/lib/eval-ablation.mjs +104 -0
  43. package/lib/eval-order.mjs +9 -0
  44. package/lib/eval-report.mjs +11 -0
  45. package/lib/eval-telemetry.mjs +8 -2
  46. package/lib/gate-receipt.mjs +52 -0
  47. package/lib/installer.mjs +39 -25
  48. package/lib/learning-engine.mjs +236 -38
  49. package/lib/model-policy.mjs +6 -0
  50. package/lib/opencode-compat.mjs +25 -10
  51. package/lib/orchestrator-policy.mjs +195 -21
  52. package/lib/process-runner.mjs +30 -9
  53. package/lib/runtime-events.mjs +31 -0
  54. package/lib/semantic-index.mjs +318 -0
  55. package/lib/task-engine.mjs +557 -28
  56. package/lib/trajectory.mjs +89 -0
  57. package/lib/windows-shim.mjs +227 -0
  58. package/lib/worktree-sandbox.mjs +85 -3
  59. package/package.json +11 -4
  60. package/scripts/check-release-tag.mjs +22 -0
  61. package/scripts/control-center.mjs +25 -0
  62. package/scripts/eval-ablation.mjs +44 -0
  63. package/scripts/eval-live.mjs +39 -58
  64. package/scripts/eval-matrix.mjs +166 -0
  65. package/scripts/smoke-packed-install.mjs +138 -4
  66. package/scripts/smoke-plain-install.mjs +91 -0
  67. package/scripts/validate-live-suite.mjs +3 -3
  68. 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 { routeSkills } from "./router.js"
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 result = spawnSync("ocskill", args, {
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, applying configured attempt-based model escalation when available. The parent must inspect the diff and record completion evidence separately.",
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 created = await ctx.session.create({ title: "UES " + input.slug + " " + input.task })
194
- await ctx.session.switchAgent({ sessionID: created.id, agent: "ues-executor" })
195
- const selectedModel = modelRef(policy?.model)
196
- if (selectedModel) {
197
- await ctx.session.switchModel({ sessionID: created.id, model: selectedModel })
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
- await ctx.session.prompt({
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
- text:
203
- "Implement exactly this approved UES task in the current repository. " +
204
- "Do not broaden scope or launch child agents. Run the declared verification and return the executor report.\n\n" +
205
- JSON.stringify(started.contextPack, null, 2),
206
- })
207
- await ctx.session.wait({ sessionID: created.id })
208
- const messages = await ctx.session.context({ sessionID: created.id })
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: policy,
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: "Inspect the child diff and verification, then call ocskill work complete or fail.",
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
- await ctx.session.hook("context", (event) => {
239
- if (event.agent === "title" || event.agent === "summary" || event.agent === "compaction") return
240
- event.system.push({
241
- type: "text",
242
- text: "UES V7 runtime: classify task complexity/risk, trust durable .ues-work state over conversation memory, use lease-backed fresh execution when supported, prefer structured verification receipts, recover stale work after interruption, and require integration evidence before completion.",
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
- await ctx.session.hook("prompt", (event) => {
961
+ await ctx.session.hook("prompt", (event) => {
247
962
  const config = routerConfig()
248
963
  if (!config.enabled) return
249
964
 
250
- const selected = routeSkills(event.prompt.text, config.maxSkills)
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
- version: 3,
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
- await ctx.permission.hook("evaluate", (event) => {
270
- if (event.action !== "shell") return
271
- const risk = destructiveShellRisk(event.resources.join("\n"))
272
- if (!risk.risky) return
273
- event.effect = "ask"
274
- event.message = "UES safety gate: confirm destructive/high-impact shell action (" + risk.id + ")."
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
  })