opencode-agent-skill 7.7.0 → 9.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 (63) hide show
  1. package/CHANGELOG.md +63 -3
  2. package/README.md +675 -581
  3. package/bin/ocskill.mjs +358 -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 +10 -7
  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 +414 -21
  33. package/global-config/plugins/ues-router/router.js +140 -23
  34. package/global-config/skills/engineering-orchestrator/references/long-horizon.md +6 -4
  35. package/lib/aci.mjs +128 -0
  36. package/lib/benchmark-confidence.mjs +135 -0
  37. package/lib/cli-utils.mjs +41 -0
  38. package/lib/container-sandbox.mjs +102 -0
  39. package/lib/context-manifest.mjs +300 -22
  40. package/lib/control-center.mjs +36 -3
  41. package/lib/eval-order.mjs +9 -0
  42. package/lib/eval-telemetry.mjs +5 -2
  43. package/lib/gate-receipt.mjs +52 -0
  44. package/lib/installer.mjs +39 -25
  45. package/lib/learning-engine.mjs +236 -38
  46. package/lib/opencode-compat.mjs +25 -10
  47. package/lib/orchestrator-policy.mjs +101 -20
  48. package/lib/process-runner.mjs +30 -9
  49. package/lib/runtime-events.mjs +31 -0
  50. package/lib/semantic-index.mjs +318 -0
  51. package/lib/task-engine.mjs +416 -24
  52. package/lib/trajectory.mjs +89 -0
  53. package/lib/windows-shim.mjs +227 -0
  54. package/lib/worktree-sandbox.mjs +85 -3
  55. package/package.json +10 -4
  56. package/scripts/check-release-tag.mjs +22 -0
  57. package/scripts/control-center.mjs +25 -0
  58. package/scripts/eval-live.mjs +39 -58
  59. package/scripts/eval-matrix.mjs +155 -0
  60. package/scripts/smoke-packed-install.mjs +138 -4
  61. package/scripts/smoke-plain-install.mjs +91 -0
  62. package/scripts/validate-live-suite.mjs +3 -3
  63. package/scripts/validate.mjs +27 -5
@@ -1,9 +1,9 @@
1
1
  import { Plugin } from "@opencode/plugin"
2
- import { readFileSync } from "node:fs"
2
+ import { existsSync, readFileSync } 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, routeSkills } from "./router.js"
7
7
  import { destructiveShellRisk } from "./safety.js"
8
8
  import { runtimeCapabilities } from "./capabilities.js"
9
9
 
@@ -28,13 +28,43 @@ function routerConfig() {
28
28
  }
29
29
 
30
30
 
31
+ function findWindowsCommand(name) {
32
+ const result = spawnSync("where", [name], { encoding: "utf8" })
33
+ if (result.status !== 0 || !result.stdout) return null
34
+ const matches = result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
35
+ return matches.find((item) => /\.(cmd|bat)$/i.test(item)) || matches[0] || null
36
+ }
37
+
38
+ function findNodeShimEntry(cmdPath) {
39
+ const dir = path.dirname(cmdPath)
40
+ let shim = ""
41
+ try { shim = readFileSync(cmdPath, "utf8") } catch {}
42
+ const match = shim.match(/node_modules[\\/][^\s"]+?\.(?:js|mjs)/gi)?.at(-1)
43
+ if (!match) return null
44
+ const entry = path.resolve(dir, match)
45
+ return existsSync(entry) ? entry : null
46
+ }
47
+
31
48
  function runOcskill(args, cwd) {
32
- const result = spawnSync("ocskill", args, {
49
+ const common = {
33
50
  cwd,
34
51
  encoding: "utf8",
35
- shell: process.platform === "win32",
36
52
  maxBuffer: 4 * 1024 * 1024,
37
- })
53
+ }
54
+ let result
55
+ if (process.platform !== "win32") {
56
+ result = spawnSync("ocskill", args, common)
57
+ } else {
58
+ const resolved = findWindowsCommand("ocskill")
59
+ if (!resolved) throw new Error("ocskill command was not found on PATH")
60
+ if (/\.(cmd|bat)$/i.test(resolved)) {
61
+ const entry = findNodeShimEntry(resolved)
62
+ if (!entry) throw new Error("refusing to execute an unrecognized ocskill batch shim through cmd.exe")
63
+ result = spawnSync(process.execPath, [entry, ...args], common)
64
+ } else {
65
+ result = spawnSync(resolved, args, common)
66
+ }
67
+ }
38
68
  if (result.status !== 0) {
39
69
  throw new Error((result.stderr || result.stdout || "ocskill command failed").trim())
40
70
  }
@@ -50,6 +80,13 @@ function runOcskillJSON(args, cwd) {
50
80
  }
51
81
  }
52
82
 
83
+ function appendTrace(traceID, type, payload, cwd) {
84
+ try {
85
+ const encoded = Buffer.from(JSON.stringify(payload || {}), "utf8").toString("base64")
86
+ runOcskill(["trace", "append", traceID, cwd, "--type", type, "--payload-b64", encoded], cwd)
87
+ } catch {}
88
+ }
89
+
53
90
  function modelRef(value) {
54
91
  if (!value || typeof value !== "string") return null
55
92
  const [base, variant] = value.split("#", 2)
@@ -68,6 +105,62 @@ function messageExcerpt(messages) {
68
105
  return text.length <= 24000 ? text : text.slice(-24000)
69
106
  }
70
107
 
108
+ function taskHasWrites(task) {
109
+ const files = task?.files
110
+ if (Array.isArray(files)) return files.length > 0
111
+ if (!files || typeof files !== "object") return false
112
+ return ["create", "modify", "test", "delete"].some(
113
+ (key) => Array.isArray(files[key]) && files[key].length > 0,
114
+ )
115
+ }
116
+
117
+ function policySkills(policy) {
118
+ const selected = []
119
+ const add = (id) => { if (id && !selected.includes(id)) selected.push(id) }
120
+ if (policy?.mode === "long-horizon") {
121
+ add("ues-engineering-orchestrator")
122
+ add("ues-long-task-state")
123
+ add("ues-task-planner")
124
+ } else if (policy?.mode === "standard" || policy?.risk === "high") {
125
+ add("ues-engineering-orchestrator")
126
+ }
127
+ const map = {
128
+ "auth-security": "ues-auth-security",
129
+ payment: "ues-payment-engineering",
130
+ database: "ues-database-engineering",
131
+ "api-contract": "ues-api-contract",
132
+ "react-native": "ues-react-native-engineering",
133
+ nextjs: "ues-nextjs-engineering",
134
+ react: "ues-react-engineering",
135
+ devops: "ues-devops-engineering",
136
+ }
137
+ for (const domain of policy?.domains || []) add(map[domain])
138
+ if (policy?.risk === "high") add("ues-change-impact-analysis")
139
+ return selected
140
+ }
141
+
142
+ function projectRoutingFacts(projectRoot) {
143
+ let inspected = null
144
+ let learning = null
145
+ try { inspected = runOcskillJSON(["inspect", projectRoot], projectRoot) } catch {}
146
+ try { learning = runOcskillJSON(["learn", "status", projectRoot], projectRoot) } catch {}
147
+
148
+ const feedbackDomains = []
149
+ for (const item of learning?.accepted || []) {
150
+ if (item.status !== "promoted") continue
151
+ const learned = classifyIntent(item.candidateRule || item.recommendation || item.title || "")
152
+ for (const domain of learned.domains || []) {
153
+ if (!feedbackDomains.includes(domain)) feedbackDomains.push(domain)
154
+ }
155
+ }
156
+
157
+ return {
158
+ repoStacks: inspected?.stack?.stacks || [],
159
+ feedbackDomains,
160
+ acceptedLearningCount: (learning?.accepted || []).filter((item) => item.status === "promoted").length,
161
+ }
162
+ }
163
+
71
164
  export default Plugin.define({
72
165
  id: "ues-router",
73
166
  async setup(ctx) {
@@ -104,6 +197,75 @@ export default Plugin.define({
104
197
  content: runOcskill(["task-policy", input.text], projectRoot),
105
198
  }),
106
199
  })
200
+ editor.add({
201
+ name: "semantic_search",
202
+ description: "Search the persistent incremental semantic index. Returns bounded path/symbol/reference evidence; never treats lexical evidence as semantic proof.",
203
+ input: {
204
+ type: "object",
205
+ properties: {
206
+ query: { type: "string" },
207
+ limit: { type: "integer", minimum: 1, maximum: 50 },
208
+ },
209
+ required: ["query"],
210
+ additionalProperties: false,
211
+ },
212
+ options: { namespace: "ues", codemode: true },
213
+ execute: async (input) => ({
214
+ content: runOcskill(["aci", "search", input.query, projectRoot, "--limit", String(input.limit || 20)], projectRoot),
215
+ }),
216
+ })
217
+ editor.add({
218
+ name: "references",
219
+ description: "Find bounded syntax-aware lexical references and concrete definition lines for one identifier.",
220
+ input: {
221
+ type: "object",
222
+ properties: {
223
+ symbol: { type: "string" },
224
+ limit: { type: "integer", minimum: 1, maximum: 100 },
225
+ },
226
+ required: ["symbol"],
227
+ additionalProperties: false,
228
+ },
229
+ options: { namespace: "ues", codemode: true },
230
+ execute: async (input) => ({
231
+ content: runOcskill(["aci", "refs", input.symbol, projectRoot, "--limit", String(input.limit || 40)], projectRoot),
232
+ }),
233
+ })
234
+ editor.add({
235
+ name: "view_file",
236
+ description: "Read a bounded line-numbered window from a repository file; refuses root escapes and oversized/binary files.",
237
+ input: {
238
+ type: "object",
239
+ properties: {
240
+ file: { type: "string" },
241
+ line: { type: "integer", minimum: 1 },
242
+ lines: { type: "integer", minimum: 1, maximum: 240 },
243
+ },
244
+ required: ["file"],
245
+ additionalProperties: false,
246
+ },
247
+ options: { namespace: "ues", codemode: true },
248
+ execute: async (input) => ({
249
+ content: runOcskill([
250
+ "aci", "view", input.file, projectRoot,
251
+ "--line", String(input.line || 1),
252
+ "--lines", String(input.lines || 120),
253
+ ], projectRoot),
254
+ }),
255
+ })
256
+ editor.add({
257
+ name: "sandbox_capability",
258
+ description: "Report whether fail-closed Docker/Podman verification isolation is actually available. Never assumes a container runtime exists.",
259
+ input: {
260
+ type: "object",
261
+ properties: {},
262
+ additionalProperties: false,
263
+ },
264
+ options: { namespace: "ues", codemode: true },
265
+ execute: async () => ({
266
+ content: runOcskill(["sandbox", "capability"], projectRoot),
267
+ }),
268
+ })
107
269
  editor.add({
108
270
  name: "work_status",
109
271
  description: "Read durable status for a UES .ues-work item without modifying it.",
@@ -149,14 +311,97 @@ export default Plugin.define({
149
311
  content: runOcskill(["context-pack", input.slug, input.task, projectRoot], projectRoot),
150
312
  }),
151
313
  })
314
+ editor.add({
315
+ name: "recover_task",
316
+ description: "Safely recover one stale UES task attempt. If an attached executor session still exists, interrupt it before releasing the durable lease.",
317
+ input: {
318
+ type: "object",
319
+ properties: {
320
+ slug: { type: "string" },
321
+ task: { type: "string" },
322
+ force: { type: "boolean" },
323
+ reason: { type: "string" },
324
+ },
325
+ required: ["slug", "task"],
326
+ additionalProperties: false,
327
+ },
328
+ options: { namespace: "ues", codemode: true },
329
+ execute: async (input) => {
330
+ const status = runOcskillJSON(["work", "status", input.slug, projectRoot], projectRoot)
331
+ const running = (status.running || []).find((item) => item.taskID === input.task)
332
+ if (!running) throw new Error("task is not currently running: " + input.task)
333
+
334
+ let interrupted = false
335
+ if (running.sessionID && capabilities.sessionInterrupt) {
336
+ try {
337
+ await ctx.session.interrupt({ sessionID: running.sessionID, continue: false })
338
+ interrupted = true
339
+ } catch {}
340
+ }
341
+
342
+ const recoverArgs = ["work", "recover-task", input.slug, input.task, projectRoot]
343
+ if (input.force) recoverArgs.push("--force")
344
+ if (input.reason) recoverArgs.push("--reason", input.reason)
345
+ const recovered = runOcskillJSON(recoverArgs, projectRoot)
346
+ return {
347
+ content: JSON.stringify({
348
+ interrupted,
349
+ sessionID: running.sessionID || null,
350
+ recovered,
351
+ }, null, 2),
352
+ }
353
+ },
354
+ })
355
+ editor.add({
356
+ name: "cancel_task",
357
+ description: "Interrupt a running UES executor session and mark its durable task attempt failed.",
358
+ input: {
359
+ type: "object",
360
+ properties: {
361
+ slug: { type: "string" },
362
+ task: { type: "string" },
363
+ reason: { type: "string" },
364
+ },
365
+ required: ["slug", "task"],
366
+ additionalProperties: false,
367
+ },
368
+ options: { namespace: "ues", codemode: true },
369
+ execute: async (input) => {
370
+ if (!capabilities.sessionInterrupt) {
371
+ throw new Error("OpenCode runtime does not expose session.interrupt")
372
+ }
373
+ const status = runOcskillJSON(["work", "status", input.slug, projectRoot], projectRoot)
374
+ const running = (status.running || []).find((item) => item.taskID === input.task)
375
+ if (!running) throw new Error("task is not currently running: " + input.task)
376
+ if (!running.sessionID) throw new Error("running task has no attached executor session")
377
+ await ctx.session.interrupt({ sessionID: running.sessionID, continue: false })
378
+ const failArgs = [
379
+ "work", "fail", input.slug, input.task, projectRoot,
380
+ "--reason", input.reason || "cancelled by user/runtime",
381
+ ]
382
+ if (running.runId) failArgs.push("--run-id", running.runId)
383
+ const failed = runOcskillJSON(failArgs, projectRoot)
384
+ return {
385
+ content: JSON.stringify({
386
+ interrupted: true,
387
+ sessionID: running.sessionID,
388
+ task: input.task,
389
+ state: failed,
390
+ }, null, 2),
391
+ }
392
+ },
393
+ })
152
394
  editor.add({
153
395
  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.",
396
+ 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
397
  input: {
156
398
  type: "object",
157
399
  properties: {
158
400
  slug: { type: "string" },
159
401
  task: { type: "string" },
402
+ timeoutMs: { type: "integer", minimum: 30000, maximum: 3600000 },
403
+ isolate: { type: "boolean" },
404
+ integrate: { type: "boolean" },
160
405
  },
161
406
  required: ["slug", "task"],
162
407
  additionalProperties: false,
@@ -170,15 +415,57 @@ export default Plugin.define({
170
415
  const started = runOcskillJSON(["work", "start", input.slug, input.task, projectRoot], projectRoot)
171
416
  const attempt = started?.record?.attempts || 1
172
417
  const runId = started?.record?.runId || null
418
+ const traceID = "dispatch-" + input.slug + "-" + input.task + "-" + String(runId || Date.now()).replace(/[^a-zA-Z0-9._-]+/g, "-")
173
419
  const taskText = [
174
420
  started?.contextPack?.task?.title,
175
421
  started?.contextPack?.task?.summary,
176
422
  ...(started?.contextPack?.task?.acceptance || []),
423
+ started?.contextPack?.task?.risk ? "risk: " + started.contextPack.task.risk : null,
177
424
  ].filter(Boolean).join(" ")
178
425
  const taskPolicy = runOcskillJSON(["task-policy", taskText], projectRoot)
426
+ appendTrace(traceID, "dispatch.started", {
427
+ slug: input.slug,
428
+ task: input.task,
429
+ runId,
430
+ attempt,
431
+ taskText,
432
+ taskPolicy,
433
+ context: {
434
+ files: started?.contextPack?.contextManifest?.files || [],
435
+ strategy: started?.contextPack?.contextManifest?.strategy || null,
436
+ used: started?.contextPack?.contextManifest?.used || 0,
437
+ budget: started?.contextPack?.contextManifest?.budget || 0,
438
+ },
439
+ }, projectRoot)
440
+ const timeoutMs = Math.max(
441
+ 30_000,
442
+ Math.min(Number(input.timeoutMs || (taskPolicy.mode === "long-horizon" ? 20 * 60_000 : 10 * 60_000)), 60 * 60_000),
443
+ )
179
444
  const policyArgs = ["model-policy", "executor", "--attempt", String(attempt)]
180
445
  if (taskText) policyArgs.push("--text", taskText)
181
446
  const policy = runOcskillJSON(policyArgs, projectRoot)
447
+ const workStatus = runOcskillJSON(["work", "status", input.slug, projectRoot], projectRoot)
448
+ const workingTree = runOcskillJSON(["working-tree", projectRoot], projectRoot)
449
+ const rootClean = workingTree?.git === true && workingTree?.clean === true
450
+ const writerTask = taskHasWrites(started?.contextPack?.task)
451
+ if (input.isolate === true && !rootClean) {
452
+ throw new Error("explicit sandbox isolation requires a clean root working tree; commit/stash or integrate existing changes first")
453
+ }
454
+ if (input.isolate !== false && writerTask && !rootClean) {
455
+ 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")
456
+ }
457
+ const autoIsolate =
458
+ input.isolate === true ||
459
+ (input.isolate !== false && rootClean && writerTask)
460
+ let sandbox = null
461
+ let executionDir = projectRoot
462
+ if (autoIsolate) {
463
+ sandbox = runOcskillJSON(
464
+ ["sandbox", "create", input.slug, input.task, projectRoot],
465
+ projectRoot,
466
+ )
467
+ executionDir = sandbox.dir
468
+ }
182
469
  const heartbeatStarted = Date.now()
183
470
  const heartbeat = setInterval(() => {
184
471
  try {
@@ -190,7 +477,25 @@ export default Plugin.define({
190
477
  }, 30_000)
191
478
 
192
479
  try {
193
- const created = await ctx.session.create({ title: "UES " + input.slug + " " + input.task })
480
+ const created = await ctx.session.create({
481
+ title: "UES " + input.slug + " " + input.task,
482
+ location: { directory: executionDir },
483
+ })
484
+ appendTrace(traceID, "dispatch.session-created", {
485
+ sessionID: created.id,
486
+ executionDir,
487
+ isolated: executionDir !== projectRoot,
488
+ }, projectRoot)
489
+ {
490
+ const attachArgs = [
491
+ "work", "attach-session", input.slug, input.task, projectRoot,
492
+ "--session-id", created.id,
493
+ ]
494
+ if (runId) attachArgs.push("--run-id", runId)
495
+ attachArgs.push("--execution-dir", executionDir)
496
+ if (sandbox?.dir) attachArgs.push("--sandbox-dir", sandbox.dir)
497
+ runOcskill(attachArgs, projectRoot)
498
+ }
194
499
  await ctx.session.switchAgent({ sessionID: created.id, agent: "ues-executor" })
195
500
  const selectedModel = modelRef(policy?.model)
196
501
  if (selectedModel) {
@@ -204,21 +509,81 @@ export default Plugin.define({
204
509
  "Do not broaden scope or launch child agents. Run the declared verification and return the executor report.\n\n" +
205
510
  JSON.stringify(started.contextPack, null, 2),
206
511
  })
207
- await ctx.session.wait({ sessionID: created.id })
512
+
513
+ let timer = null
514
+ try {
515
+ await Promise.race([
516
+ ctx.session.wait({ sessionID: created.id }),
517
+ new Promise((_, reject) => {
518
+ timer = setTimeout(
519
+ () => reject(new Error("UES executor timed out after " + timeoutMs + "ms")),
520
+ timeoutMs,
521
+ )
522
+ }),
523
+ ])
524
+ } catch (error) {
525
+ try {
526
+ await ctx.session.interrupt({ sessionID: created.id, continue: false })
527
+ } catch {}
528
+ throw error
529
+ } finally {
530
+ if (timer) clearTimeout(timer)
531
+ }
532
+
533
+ const afterWait = runOcskillJSON(["work", "status", input.slug, projectRoot], projectRoot)
534
+ const activeAttempt = (afterWait.running || []).find(
535
+ (item) => item.taskID === input.task && (!runId || item.runId === runId),
536
+ )
537
+ if (!activeAttempt) {
538
+ throw new Error("UES executor attempt is no longer active; refusing post-cancel integration or completion handoff")
539
+ }
540
+
208
541
  const messages = await ctx.session.context({ sessionID: created.id })
542
+ appendTrace(traceID, "dispatch.completed", {
543
+ sessionID: created.id,
544
+ task: input.task,
545
+ runId,
546
+ messageCount: Array.isArray(messages) ? messages.length : null,
547
+ }, projectRoot)
548
+ let integration = null
549
+ if (sandbox && input.integrate === true) {
550
+ integration = runOcskillJSON(
551
+ ["sandbox", "integrate", sandbox.dir, projectRoot],
552
+ projectRoot,
553
+ )
554
+ sandbox = null
555
+ }
209
556
  return {
210
557
  content: JSON.stringify({
211
558
  sessionID: created.id,
212
559
  task: input.task,
213
560
  attempt,
214
561
  runId,
562
+ traceID,
215
563
  taskPolicy,
216
564
  model: policy,
565
+ timeoutMs,
566
+ executionDir,
567
+ isolated: executionDir !== projectRoot,
568
+ sandbox,
569
+ integration,
217
570
  messages: messageExcerpt(messages),
218
- next: "Inspect the child diff and verification, then call ocskill work complete or fail.",
571
+ next: sandbox
572
+ ? "Inspect and verify the isolated worktree first. If accepted, integrate it with ocskill sandbox integrate <worktree> . before recording work complete."
573
+ : "Inspect the child diff and verification, then call ocskill work complete or fail.",
219
574
  }, null, 2),
220
575
  }
221
576
  } catch (error) {
577
+ appendTrace(traceID, "dispatch.failed", {
578
+ task: input.task,
579
+ runId,
580
+ error: String(error?.message || error),
581
+ }, projectRoot)
582
+ if (sandbox?.dir) {
583
+ try {
584
+ runOcskill(["sandbox", "remove", sandbox.dir, projectRoot, "--force", "--delete-branch"], projectRoot)
585
+ } catch {}
586
+ }
222
587
  try {
223
588
  const failArgs = [
224
589
  "work", "fail", input.slug, input.task, projectRoot,
@@ -235,19 +600,35 @@ export default Plugin.define({
235
600
  })
236
601
  })
237
602
 
238
- await ctx.session.hook("context", (event) => {
603
+ const routingFacts = projectRoutingFacts(projectRoot)
604
+
605
+ if (capabilities.sessionHook) {
606
+ await ctx.session.hook("context", (event) => {
239
607
  if (event.agent === "title" || event.agent === "summary" || event.agent === "compaction") return
240
608
  event.system.push({
241
609
  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.",
610
+ text: "UES runtime: choose FAST/STANDARD/DEEP from deterministic task policy, prefer bounded semantic/ACI evidence over broad scans, trust durable .ues-work state and EVENTS.jsonl over conversation memory, never treat lexical matches as semantic proof, require fresh verification for non-trivial completion, and fail closed when a high-risk capability is missing.",
243
611
  })
244
612
  })
245
613
 
246
- await ctx.session.hook("prompt", (event) => {
614
+ await ctx.session.hook("prompt", (event) => {
247
615
  const config = routerConfig()
248
616
  if (!config.enabled) return
249
617
 
250
- const selected = routeSkills(event.prompt.text, config.maxSkills)
618
+ let policy = null
619
+ try {
620
+ policy = runOcskillJSON(["task-policy", event.prompt.text], projectRoot)
621
+ } catch {}
622
+ const intent = classifyIntent(event.prompt.text, routingFacts)
623
+ const effectiveMaxSkills = Math.max(
624
+ 1,
625
+ Math.min(config.maxSkills, Number(policy?.maxSkills || config.maxSkills)),
626
+ )
627
+ const selected = []
628
+ for (const id of [...routeSkills(event.prompt.text, effectiveMaxSkills, routingFacts), ...policySkills(policy)]) {
629
+ if (!selected.includes(id)) selected.push(id)
630
+ if (selected.length >= effectiveMaxSkills) break
631
+ }
251
632
  if (selected.length === 0) return
252
633
 
253
634
  event.prompt.skills ??= []
@@ -261,17 +642,29 @@ export default Plugin.define({
261
642
  ...event.metadata,
262
643
  uesRouter: {
263
644
  selected,
264
- version: 3,
645
+ policy,
646
+ intent,
647
+ facts: {
648
+ repoStacks: routingFacts.repoStacks,
649
+ feedbackDomains: routingFacts.feedbackDomains,
650
+ acceptedLearningCount: routingFacts.acceptedLearningCount,
651
+ },
652
+ version: 6,
653
+ effectiveMaxSkills,
265
654
  },
266
655
  }
267
656
  })
268
657
 
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
- })
658
+ }
659
+
660
+ if (capabilities.permissionHook) {
661
+ await ctx.permission.hook("evaluate", (event) => {
662
+ if (event.action !== "shell") return
663
+ const risk = destructiveShellRisk(event.resources.join("\n"))
664
+ if (!risk.risky) return
665
+ event.effect = "ask"
666
+ event.message = "UES safety gate: confirm destructive/high-impact shell action (" + risk.id + ")."
667
+ })
668
+ }
276
669
  },
277
670
  })