opencode-agent-skill 11.0.0 → 13.0.0-beta.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 (47) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +119 -14
  3. package/bin/ocskill.mjs +416 -83
  4. package/docs/DETERMINISTIC-TOOLS.md +1 -1
  5. package/docs/ENGINEERING-DESIGN.md +4 -4
  6. package/docs/EVALS.md +3 -3
  7. package/docs/GITHUB-RULESET.md +50 -0
  8. package/docs/NPM-PUBLISH.md +22 -8
  9. package/docs/OPENCODE-COMPAT.md +14 -4
  10. package/docs/TRACE-SCHEMA.md +1 -1
  11. package/docs/V11-PERCEPTION-ADAPTIVE.md +2 -2
  12. package/docs/V12-WEAK-MODEL-INTELLIGENCE.md +27 -0
  13. package/docs/V13-PARALLEL-WEAK-MODEL-RUNTIME.md +75 -0
  14. package/evals/repo-scale/tasks.json +62 -0
  15. package/global-config/AGENTS.md +4 -0
  16. package/global-config/commands/resume.md +4 -1
  17. package/global-config/commands/run.md +9 -7
  18. package/global-config/plugins/ues-router/capabilities.js +1 -0
  19. package/global-config/plugins/ues-router/command-runtime.js +79 -0
  20. package/global-config/plugins/ues-router/index.js +364 -43
  21. package/global-config/plugins/ues-router/parallel-runtime.js +271 -0
  22. package/global-config/plugins/ues-router/text-runtime.js +115 -0
  23. package/global-config/plugins/ues-router/verifier-runtime.js +33 -0
  24. package/global-config/skills/dynamic-workflow/SKILL.md +7 -6
  25. package/global-config/skills/dynamic-workflow/references/workflow.md +8 -6
  26. package/global-config/skills/engineering-orchestrator/references/long-horizon.md +7 -5
  27. package/lib/cli-utils.mjs +17 -4
  28. package/lib/context-engine-v11.mjs +4 -0
  29. package/lib/context-quality.mjs +59 -0
  30. package/lib/decision-policy.mjs +23 -0
  31. package/lib/installer.mjs +49 -16
  32. package/lib/model-config.mjs +13 -1
  33. package/lib/model-performance.mjs +113 -0
  34. package/lib/model-policy.mjs +9 -2
  35. package/lib/repo-scale-fixture.mjs +45 -0
  36. package/lib/task-engine.mjs +36 -12
  37. package/lib/task-graph.mjs +32 -0
  38. package/lib/text-encoding.mjs +74 -0
  39. package/lib/work-plan-scope.mjs +49 -0
  40. package/lib/worktree-sandbox.mjs +141 -12
  41. package/package.json +8 -4
  42. package/scripts/check-release-consistency.mjs +260 -0
  43. package/scripts/smoke-packed-install.mjs +39 -3
  44. package/scripts/smoke-plain-install.mjs +27 -7
  45. package/scripts/validate-repo-scale-suite.mjs +27 -0
  46. package/scripts/validate-v12-foundation.mjs +24 -0
  47. package/scripts/validate.mjs +18 -1
@@ -0,0 +1,271 @@
1
+ function normalizePath(value) {
2
+ return String(value || "").replaceAll("\\", "/").replace(/^\.\/+/, "")
3
+ }
4
+
5
+ function taskFiles(task, keys) {
6
+ const files = task?.files
7
+ if (Array.isArray(files)) return [...new Set(files.map(normalizePath).filter(Boolean))]
8
+ if (!files || typeof files !== "object") return []
9
+ return [...new Set(keys.flatMap((key) => Array.isArray(files[key]) ? files[key] : []).map(normalizePath).filter(Boolean))]
10
+ }
11
+
12
+ function claimMap(task) {
13
+ const writes = taskFiles(task, ["create", "modify", "test", "delete"])
14
+ const reads = taskFiles(task, ["read"])
15
+ const claims = new Map()
16
+ const add = (resource, mode) => {
17
+ const current = claims.get(resource)
18
+ if (!current || mode === "write") claims.set(resource, mode)
19
+ }
20
+
21
+ for (const file of reads) add("file:" + file, "read")
22
+ for (const file of writes) add("file:" + file, "write")
23
+
24
+ const explicit = Array.isArray(task?.resources) ? task.resources : []
25
+ for (const resource of explicit) add("resource:" + String(resource), "write")
26
+
27
+ if (writes.length) add("@writers", "read")
28
+ const sharedSurface = writes.some((file) =>
29
+ /(^|\/)(package(?:-lock)?\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|tsconfig(?:\.[^/]+)?\.json|schema\.prisma|.*\.csproj|pom\.xml|gradle\.properties)$/i.test(file) ||
30
+ /(^|\/)(migrations?|generated|config)\//i.test(file)
31
+ )
32
+ if (sharedSurface) add("@writers", "write")
33
+
34
+ if (!writes.length && !reads.length && explicit.length === 0) {
35
+ add("@unknown-scope", "write")
36
+ add("@writers", "write")
37
+ }
38
+
39
+ return [...claims].map(([resource, mode]) => ({ resource, mode }))
40
+ }
41
+
42
+ export function taskResourceClaims(task) {
43
+ return claimMap(task)
44
+ }
45
+
46
+ export class ResourceLeaseTable {
47
+ constructor() {
48
+ this.resources = new Map()
49
+ this.byOwner = new Map()
50
+ }
51
+
52
+ canAcquire(owner, claims) {
53
+ for (const claim of claims) {
54
+ const holders = this.resources.get(claim.resource) || []
55
+ for (const holder of holders) {
56
+ if (holder.owner === owner) continue
57
+ if (claim.mode === "write" || holder.mode === "write") return false
58
+ }
59
+ }
60
+ return true
61
+ }
62
+
63
+ acquire(owner, claims) {
64
+ if (!this.canAcquire(owner, claims)) return false
65
+ this.byOwner.set(owner, claims)
66
+ for (const claim of claims) {
67
+ const holders = this.resources.get(claim.resource) || []
68
+ holders.push({ owner, mode: claim.mode })
69
+ this.resources.set(claim.resource, holders)
70
+ }
71
+ return true
72
+ }
73
+
74
+ release(owner) {
75
+ const claims = this.byOwner.get(owner) || []
76
+ this.byOwner.delete(owner)
77
+ for (const claim of claims) {
78
+ const remaining = (this.resources.get(claim.resource) || []).filter((holder) => holder.owner !== owner)
79
+ if (remaining.length) this.resources.set(claim.resource, remaining)
80
+ else this.resources.delete(claim.resource)
81
+ }
82
+ }
83
+
84
+ snapshot() {
85
+ return [...this.resources.entries()].map(([resource, holders]) => ({ resource, holders }))
86
+ }
87
+ }
88
+
89
+ export function adaptiveWorkerCount(input = {}) {
90
+ const requested = Math.max(1, Math.min(16, Number(input.requested || 4)))
91
+ const readyCount = Math.max(0, Number(input.readyCount || 0))
92
+ let limit = Math.min(requested, Math.max(1, readyCount || 1))
93
+ const recentFailures = Math.max(0, Number(input.recentFailures || 0))
94
+ const conflictRate = Math.max(0, Math.min(1, Number(input.conflictRate || 0)))
95
+ if (recentFailures >= 2) limit = Math.max(1, Math.ceil(limit / 2))
96
+ if (conflictRate >= 0.35) limit = Math.max(1, Math.ceil(limit / 2))
97
+ return limit
98
+ }
99
+
100
+ export function parallelRootBaseline(workingTree = {}) {
101
+ if (workingTree?.git !== true) {
102
+ throw new Error("ues.dispatch_parallel requires a Git repository")
103
+ }
104
+ return {
105
+ head: workingTree.head || null,
106
+ branch: workingTree.branch || null,
107
+ clean: workingTree.clean === true,
108
+ inheritedDirtyRoot: workingTree.clean !== true,
109
+ changes: Array.isArray(workingTree.changes) ? [...workingTree.changes] : [],
110
+ }
111
+ }
112
+
113
+ function validateGraph(tasks, completed) {
114
+ const byID = new Map()
115
+ for (const task of tasks) {
116
+ const id = String(task?.id || "")
117
+ if (!id || byID.has(id)) throw new Error("parallel DAG requires unique non-empty task ids")
118
+ byID.set(id, task)
119
+ }
120
+ for (const task of tasks) {
121
+ for (const dep of task.dependsOn || task.dependencies || []) {
122
+ if (!byID.has(dep) && !completed.has(dep)) {
123
+ throw new Error("parallel DAG task " + task.id + " has missing dependency " + dep)
124
+ }
125
+ }
126
+ }
127
+ return byID
128
+ }
129
+
130
+ function errorRecord(error) {
131
+ return {
132
+ message: String(error?.message || error),
133
+ code: error?.code || null,
134
+ }
135
+ }
136
+
137
+ export async function runEventDrivenDAG(tasks = [], options = {}) {
138
+ if (typeof options.worker !== "function") throw new Error("parallel DAG requires a worker function")
139
+ const completed = new Set(options.completedTaskIds || [])
140
+ const byID = validateGraph(tasks, completed)
141
+ const pending = new Set(tasks.map((task) => task.id))
142
+ const running = new Map()
143
+ const failed = new Map()
144
+ const blocked = new Map()
145
+ const results = new Map()
146
+ const leases = options.leases || new ResourceLeaseTable()
147
+ const events = []
148
+ let integrationTail = Promise.resolve()
149
+ let conflictDeferrals = 0
150
+ let schedulingChecks = 0
151
+
152
+ const emit = (type, payload = {}) => {
153
+ const event = { type, at: new Date().toISOString(), ...payload }
154
+ events.push(event)
155
+ options.onEvent?.(event)
156
+ }
157
+
158
+ const enqueueIntegration = (task, result, context) => {
159
+ if (typeof options.integrate !== "function") return Promise.resolve(null)
160
+ const job = integrationTail.then(() => options.integrate(task, result, context))
161
+ integrationTail = job.catch(() => {})
162
+ return job
163
+ }
164
+
165
+ const launch = (task) => {
166
+ const claims = taskResourceClaims(task)
167
+ if (!leases.acquire(task.id, claims)) return false
168
+ pending.delete(task.id)
169
+ const context = {
170
+ model: options.singleModel === false ? null : (options.model || null),
171
+ singleModel: options.singleModel !== false,
172
+ claims,
173
+ }
174
+ emit("task.started", { task: task.id, model: context.model, claims })
175
+ const promise = (async () => {
176
+ try {
177
+ const result = await options.worker(task, context)
178
+ const integration = await enqueueIntegration(task, result, context)
179
+ return { ok: true, result, integration }
180
+ } catch (error) {
181
+ return { ok: false, error }
182
+ } finally {
183
+ leases.release(task.id)
184
+ }
185
+ })()
186
+ running.set(task.id, promise)
187
+ return true
188
+ }
189
+
190
+ while (pending.size || running.size) {
191
+ for (const id of [...pending]) {
192
+ const task = byID.get(id)
193
+ const badDep = (task.dependsOn || task.dependencies || []).find((dep) => failed.has(dep) || blocked.has(dep))
194
+ if (badDep) {
195
+ pending.delete(id)
196
+ blocked.set(id, { dependency: badDep })
197
+ emit("task.blocked", { task: id, dependency: badDep })
198
+ }
199
+ }
200
+
201
+ const ready = [...pending]
202
+ .map((id) => byID.get(id))
203
+ .filter((task) => (task.dependsOn || task.dependencies || []).every((dep) => completed.has(dep)))
204
+ .sort((a, b) => String(a.id).localeCompare(String(b.id)))
205
+
206
+ schedulingChecks += Math.max(1, ready.length)
207
+ const conflictRate = schedulingChecks ? conflictDeferrals / schedulingChecks : 0
208
+ const limit = adaptiveWorkerCount({
209
+ requested: options.maxConcurrent || 4,
210
+ readyCount: ready.length + running.size,
211
+ recentFailures: failed.size,
212
+ conflictRate,
213
+ })
214
+
215
+ for (const task of ready) {
216
+ if (running.size >= limit) break
217
+ const claims = taskResourceClaims(task)
218
+ if (!leases.canAcquire(task.id, claims)) {
219
+ conflictDeferrals += 1
220
+ continue
221
+ }
222
+ launch(task)
223
+ }
224
+
225
+ if (!running.size) {
226
+ if (pending.size) {
227
+ for (const id of pending) {
228
+ blocked.set(id, { dependency: null, reason: "no-runnable-task" })
229
+ emit("task.blocked", { task: id, reason: "no-runnable-task" })
230
+ }
231
+ pending.clear()
232
+ }
233
+ break
234
+ }
235
+
236
+ const settled = await Promise.race(
237
+ [...running.entries()].map(([id, promise]) => promise.then((value) => ({ id, ...value }))),
238
+ )
239
+ running.delete(settled.id)
240
+ if (settled.ok) {
241
+ completed.add(settled.id)
242
+ results.set(settled.id, { result: settled.result, integration: settled.integration })
243
+ emit("task.completed", { task: settled.id })
244
+ } else {
245
+ failed.set(settled.id, errorRecord(settled.error))
246
+ emit("task.failed", { task: settled.id, error: errorRecord(settled.error) })
247
+ if (options.failFast === true) {
248
+ for (const id of pending) {
249
+ blocked.set(id, { dependency: settled.id, reason: "fail-fast" })
250
+ emit("task.blocked", { task: id, dependency: settled.id, reason: "fail-fast" })
251
+ }
252
+ pending.clear()
253
+ }
254
+ }
255
+ }
256
+
257
+ await integrationTail
258
+ return {
259
+ schemaVersion: 1,
260
+ model: options.singleModel === false ? null : (options.model || null),
261
+ singleModel: options.singleModel !== false,
262
+ requestedConcurrency: Math.max(1, Number(options.maxConcurrent || 4)),
263
+ completed: [...completed].filter((id) => byID.has(id)),
264
+ failed: Object.fromEntries(failed),
265
+ blocked: Object.fromEntries(blocked),
266
+ results: Object.fromEntries(results),
267
+ conflictDeferrals,
268
+ events,
269
+ leaseSnapshot: leases.snapshot(),
270
+ }
271
+ }
@@ -0,0 +1,115 @@
1
+ import { readFileSync, realpathSync } from "node:fs"
2
+ import path from "node:path"
3
+
4
+ function swapUtf16(buffer) {
5
+ const evenLength = buffer.length - (buffer.length % 2)
6
+ const swapped = Buffer.allocUnsafe(evenLength)
7
+ for (let index = 0; index < evenLength; index += 2) {
8
+ swapped[index] = buffer[index + 1]
9
+ swapped[index + 1] = buffer[index]
10
+ }
11
+ return swapped.toString("utf16le")
12
+ }
13
+
14
+ function utf16Guess(buffer) {
15
+ if (buffer.length < 4) return null
16
+ let evenNull = 0
17
+ let oddNull = 0
18
+ let even = 0
19
+ let odd = 0
20
+ for (let index = 0; index < buffer.length; index += 1) {
21
+ if (index % 2 === 0) {
22
+ even += 1
23
+ if (buffer[index] === 0) evenNull += 1
24
+ } else {
25
+ odd += 1
26
+ if (buffer[index] === 0) oddNull += 1
27
+ }
28
+ }
29
+ const er = even ? evenNull / even : 0
30
+ const or = odd ? oddNull / odd : 0
31
+ if (or >= 0.25 && er <= 0.05) return "utf16le"
32
+ if (er >= 0.25 && or <= 0.05) return "utf16be"
33
+ return null
34
+ }
35
+
36
+ function decode(buffer) {
37
+ if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
38
+ return { text: buffer.subarray(3).toString("utf8"), encoding: "utf8-bom" }
39
+ }
40
+ if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
41
+ return { text: buffer.subarray(2).toString("utf16le"), encoding: "utf16le" }
42
+ }
43
+ if (buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) {
44
+ return { text: swapUtf16(buffer.subarray(2)), encoding: "utf16be" }
45
+ }
46
+ const guess = utf16Guess(buffer)
47
+ if (guess === "utf16le") return { text: buffer.toString("utf16le"), encoding: guess }
48
+ if (guess === "utf16be") return { text: swapUtf16(buffer), encoding: guess }
49
+
50
+ let nul = 0
51
+ for (const byte of buffer) if (byte === 0) nul += 1
52
+ if (nul > Math.max(2, Math.floor(buffer.length * 0.01))) {
53
+ const error = new Error("file appears to be binary rather than UTF text")
54
+ error.code = "UES_BINARY_TEXT"
55
+ throw error
56
+ }
57
+ return { text: buffer.toString("utf8"), encoding: "utf8" }
58
+ }
59
+
60
+ export function readProjectText(root, relative, options = {}) {
61
+ const raw = String(relative || "").trim()
62
+ if (!raw) {
63
+ const error = new Error("text_read requires a non-empty file path")
64
+ error.code = "UES_USAGE"
65
+ throw error
66
+ }
67
+ root = path.resolve(root)
68
+ const file = path.resolve(root, raw)
69
+ if (file !== root && !file.startsWith(root + path.sep)) {
70
+ throw new Error("text_read path must stay inside the project root")
71
+ }
72
+ const realRoot = realpathSync(root)
73
+ const realFile = realpathSync(file)
74
+ if (realFile !== realRoot && !realFile.startsWith(realRoot + path.sep)) {
75
+ const error = new Error("text_read refuses a symlink that escapes the project root")
76
+ error.code = "UES_PATH_ESCAPE"
77
+ throw error
78
+ }
79
+ const buffer = readFileSync(realFile)
80
+ const decoded = decode(buffer)
81
+ const start = Math.max(0, Number(options.start || 0))
82
+ const maxChars = Math.max(256, Math.min(Number(options.maxChars || 12000), 1_000_000))
83
+ const text = decoded.text.slice(start, start + maxChars)
84
+ return {
85
+ schemaVersion: 1,
86
+ file: path.relative(root, file).replaceAll("\\", "/") || ".",
87
+ encoding: decoded.encoding,
88
+ bytes: buffer.length,
89
+ chars: decoded.text.length,
90
+ start,
91
+ returnedChars: text.length,
92
+ truncated: start + text.length < decoded.text.length,
93
+ text,
94
+ }
95
+ }
96
+
97
+ export function readProjectJson(root, relative, options = {}) {
98
+ const result = readProjectText(root, relative, {
99
+ start: 0,
100
+ maxChars: Math.max(1024, Math.min(Number(options.maxChars || 1_000_000), 1_000_000)),
101
+ })
102
+ if (result.truncated) {
103
+ const error = new Error("JSON file exceeds the bounded V13 project reader limit")
104
+ error.code = "UES_TEXT_TRUNCATED"
105
+ throw error
106
+ }
107
+ try {
108
+ return { ...result, value: JSON.parse(result.text) }
109
+ } catch (cause) {
110
+ const error = new Error("invalid JSON in " + result.file + ": " + String(cause?.message || cause))
111
+ error.code = "UES_INVALID_JSON"
112
+ throw error
113
+ }
114
+ }
115
+
@@ -0,0 +1,33 @@
1
+ function assistantTextParts(message) {
2
+ const role = String(message?.info?.role || message?.role || "").toLowerCase()
3
+ if (role !== "assistant") return []
4
+ const values = []
5
+ for (const part of message?.parts || []) {
6
+ if (part?.type === "text" && typeof part.text === "string") values.push(part.text)
7
+ }
8
+ if (values.length === 0 && typeof message?.content === "string") values.push(message.content)
9
+ return values
10
+ }
11
+
12
+ export function extractVerifierVerdict(messages) {
13
+ const list = Array.isArray(messages) ? messages : []
14
+ for (let messageIndex = list.length - 1; messageIndex >= 0; messageIndex -= 1) {
15
+ const parts = assistantTextParts(list[messageIndex])
16
+ for (let partIndex = parts.length - 1; partIndex >= 0; partIndex -= 1) {
17
+ const lines = parts[partIndex].split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
18
+ for (let lineIndex = lines.length - 1; lineIndex >= 0; lineIndex -= 1) {
19
+ const line = lines[lineIndex]
20
+ if (!line.startsWith("UES_VERDICT_JSON:")) continue
21
+ const raw = line.slice("UES_VERDICT_JSON:".length).trim()
22
+ try {
23
+ const parsed = JSON.parse(raw)
24
+ const verdict = String(parsed.verdict || "").toUpperCase()
25
+ const evidence = String(parsed.evidence || "").trim().slice(0, 4000)
26
+ if (verdict === "PASS" && evidence.length < 3) continue
27
+ if (["PASS", "FAIL"].includes(verdict)) return { verdict, evidence }
28
+ } catch {}
29
+ }
30
+ }
31
+ }
32
+ return null
33
+ }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: dynamic-workflow
3
- description: Plan large fan-out engineering campaigns into bounded dependency-safe waves, separating deterministic work from LLM judgment, limiting concurrency, isolating writers and verifying integrated results before the next wave.
3
+ description: Execute large fan-out engineering campaigns with an event-driven dependency DAG, bounded same-model workers, resource leases, isolated writers and transactional verification/integration.
4
4
  ---
5
5
 
6
6
  # Dynamic Workflow
@@ -9,10 +9,11 @@ Do not spawn agents for work a deterministic script/tool can do. Do not fan out
9
9
 
10
10
  For a large task:
11
11
  1. classify each unit as deterministic, LLM judgment, or visual judgment;
12
- 2. build dependency-safe waves;
13
- 3. serialize overlapping writers and isolate independent writers;
14
- 4. bound concurrency by provider/machine capacity;
15
- 5. keep large intermediate results on disk/evidence references;
16
- 6. integrate and verify each wave before later waves branch from it.
12
+ 2. build an explicit dependency DAG and declare read/write file scope;
13
+ 3. use one shared model across fresh worker sessions unless the user explicitly requests otherwise;
14
+ 4. serialize resource conflicts and isolate independent writers in worktrees;
15
+ 5. bound/adapt concurrency instead of spawning as many agents as possible;
16
+ 6. integrate one verified result at a time and immediately unlock newly ready dependencies;
17
+ 7. keep large intermediate results on disk/evidence references instead of relaying them through parent chat context.
17
18
 
18
19
  Read [workflow.md](references/workflow.md) for campaign and recovery rules.
@@ -1,6 +1,6 @@
1
1
  # Dynamic workflow
2
2
 
3
- Use ocskill workflow-plan PLAN.json to produce a bounded schedule.
3
+ Use `ocskill workflow-plan PLAN.json` for deterministic preview, but on OpenCode V2 prefer `ues.dispatch_parallel` for actual event-driven execution when multiple dependency-ready tasks can run safely.
4
4
 
5
5
  Good fan-out units have independent inputs/outputs and clear ownership. Mechanical indexing, parsing, formatting, builds and test commands stay deterministic.
6
6
 
@@ -10,10 +10,12 @@ For LLM/vision units:
10
10
  - do not depend on sibling conversational output;
11
11
  - verify task ownership before integration.
12
12
 
13
- After each wave:
14
- - ensure every expected result exists;
15
- - integrate verified branches/worktrees;
16
- - run combined verification;
17
- - branch the next wave from the integrated state.
13
+ During execution:
14
+ - keep one shared model across worker/verifier sessions unless explicitly overridden;
15
+ - isolate writers and hold resource leases until verification plus integration completes;
16
+ - independently verify each task result;
17
+ - integrate one accepted worktree transactionally;
18
+ - record deterministic verification commands when the plan provides them;
19
+ - immediately schedule dependencies unlocked by that completed task rather than waiting for unrelated siblings.
18
20
 
19
21
  On restart, resume from .ues-work, evidence references and the last verified integrated state rather than replaying the entire conversation.
@@ -17,7 +17,7 @@ When the user explicitly chooses the long-horizon workflow (for example with `/u
17
17
  reports/
18
18
  ```
19
19
 
20
- The directory is git-ignored execution state, not hidden reasoning. Store requirements, decisions, task status, reports and fresh verification evidence. Never store secrets or chain-of-thought.
20
+ The directory is git-ignored execution state, not hidden reasoning. Store requirements, decisions, task status, reports and fresh verification evidence. Never store secrets or chain-of-thought. Never substitute a top-level `ues-work/` directory for `.ues-work/<slug>/`; old/manual `ues-work/` files are non-canonical unless explicitly migrated.
21
21
 
22
22
  ## Machine-enforced gates
23
23
 
@@ -36,7 +36,8 @@ The V6 engine enforces three boundaries:
36
36
  5. Run `ues-plan-checker`. If it returns PASS, create `work gate-receipt <slug> plan` and persist approval with `work approve-plan --receipt-file ...`.
37
37
  6. Use `ocskill task-graph` to compute dependency-safe waves.
38
38
  7. For each ready task:
39
- - on OpenCode V2 prefer `ues.dispatch_task`, which performs `work start`, creates a fresh `ues-executor` session, selects the configured attempt-based model tier, prompts it with a bounded context pack and waits for completion;
39
+ - on OpenCode V2, if two or more approved tasks are independent, prefer `ues.dispatch_parallel` so one shared model can execute them concurrently with isolated worktrees, leases, independent verifier sessions and serialized integration;
40
+ - otherwise prefer `ues.dispatch_task`, which performs `work start`, creates a fresh `ues-executor` session, selects the configured attempt-based model tier, prompts it with a bounded context pack and waits for completion;
40
41
  - inspect the child diff and verification;
41
42
  - record at least one successful `work verify-command` receipt for long/high-risk work;
42
43
  - persist `work complete --evidence ...` or `work fail --reason ...`.
@@ -49,10 +50,11 @@ The V6 engine enforces three boundaries:
49
50
 
50
51
  Parallel execution is allowed only when:
51
52
  - dependencies are satisfied;
52
- - safe-wave analysis does not detect declared-file overlap;
53
- - executors do not write shared implicit files or interfaces.
53
+ - declared file/resource scopes are independent or protected by leases;
54
+ - the runtime exposes the fresh-dispatch capability surface;
55
+ - the root state satisfies the parallel runtime safety preconditions.
54
56
 
55
- UES serializes durable state writes, but it cannot make conflicting source-code edits safe. When write-surface independence is uncertain, execute sequentially.
57
+ V13 uses event-driven scheduling rather than fixed wave barriers: a dependency may start as soon as its prerequisite has been independently verified, integrated and durably completed. Shared configuration surfaces and unknown scopes serialize conservatively. When write-surface independence is uncertain, execute sequentially.
56
58
 
57
59
  ## Resume
58
60
 
package/lib/cli-utils.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { readFileSync } from "node:fs"
2
1
  import path from "node:path"
2
+ import { readTextAuto } from "./text-encoding.mjs"
3
3
 
4
4
  export function clipOutput(value, { head = 8000, tail = 8000 } = {}) {
5
5
  const text = String(value ?? "")
@@ -14,11 +14,11 @@ export function positionalArg(args, index) {
14
14
  }
15
15
 
16
16
  export function readJsonFile(file) {
17
- return JSON.parse(readFileSync(path.resolve(file), "utf8"))
17
+ return JSON.parse(readTextAuto(path.resolve(file)))
18
18
  }
19
19
 
20
20
  export function readTextFile(file) {
21
- return readFileSync(path.resolve(file), "utf8")
21
+ return readTextAuto(path.resolve(file))
22
22
  }
23
23
 
24
24
  export function optionInt(args, name, fallback) {
@@ -38,4 +38,17 @@ export function optionValue(args, name) {
38
38
 
39
39
  export function errorMessage(error) {
40
40
  return error instanceof Error ? error.message : error
41
- }
41
+ }
42
+ export function cliErrorPayload(error, options = {}) {
43
+ const message = error instanceof Error ? error.message : String(error)
44
+ const code = String(error?.code || (message.startsWith("Usage:") ? "UES_USAGE" : "UES_ERROR"))
45
+ const exitCode = Number.isInteger(error?.exitCode) ? error.exitCode : code === "UES_USAGE" ? 2 : 1
46
+ return { ok: false, error: { code, message, command: options.command || null, usage: options.usage || (message.startsWith("Usage:") ? message : null), hint: options.hint || null, recoverable: options.recoverable ?? (code === "UES_USAGE" || code === "UES_BINARY_TEXT") }, exitCode }
47
+ }
48
+
49
+ export function usageError(message) {
50
+ const error = new Error(String(message || "invalid command usage"))
51
+ error.code = "UES_USAGE"
52
+ error.exitCode = 2
53
+ return error
54
+ }
@@ -3,6 +3,7 @@ import { planEvidenceBudget, evidenceValueScore } from "./evidence-budget.mjs"
3
3
  import { putEvidence } from "./evidence-store.mjs"
4
4
  import { inferTaskCapabilities } from "./capability-registry.mjs"
5
5
  import { buildPromptEnvelope, comparePromptEnvelopes } from "./prompt-cache.mjs"
6
+ import { measureContextQuality } from "./context-quality.mjs"
6
7
 
7
8
  function taskText(task = {}) {
8
9
  return [
@@ -118,6 +119,8 @@ export async function buildAdaptiveTaskContext(root, task, options = {}) {
118
119
  recentMessages: options.recentMessages || [],
119
120
  })
120
121
 
122
+ const contextQuality = measureContextQuality(task, externalized.manifest, { minRequiredRecall: options.minRequiredRecall })
123
+
121
124
  const cache = options.previousPromptEnvelope
122
125
  ? comparePromptEnvelopes(options.previousPromptEnvelope, promptEnvelope)
123
126
  : null
@@ -128,6 +131,7 @@ export async function buildAdaptiveTaskContext(root, task, options = {}) {
128
131
  capabilities,
129
132
  evidenceBudget,
130
133
  contextManifest: externalized.manifest,
134
+ contextQuality,
131
135
  evidenceStore: {
132
136
  refs: externalized.externalized.length,
133
137
  externalizedBytes: externalized.externalizedBytes,
@@ -0,0 +1,59 @@
1
+ function normalizePath(value) {
2
+ return String(value || "").replaceAll("\\", "/").replace(/^\.\//, "")
3
+ }
4
+
5
+ function declaredTaskFiles(task = {}) {
6
+ const files = task.files
7
+ const values = []
8
+ if (Array.isArray(files)) values.push(...files)
9
+ else if (files && typeof files === "object") {
10
+ for (const [kind, list] of Object.entries(files)) {
11
+ if (kind === "create") continue
12
+ if (Array.isArray(list)) values.push(...list)
13
+ else if (typeof list === "string") values.push(list)
14
+ }
15
+ }
16
+ if (Array.isArray(task.requiredFiles)) values.push(...task.requiredFiles)
17
+ return [...new Set(values.map(normalizePath).filter(Boolean))]
18
+ }
19
+
20
+ function manifestPaths(manifest = {}) {
21
+ const values = []
22
+ for (const item of manifest.excerpts || []) if (item?.path) values.push(item.path)
23
+ for (const item of manifest.rankedReferences || []) if (item?.path) values.push(item.path)
24
+ for (const item of manifest.instructions || []) {
25
+ if (typeof item === "string") values.push(item)
26
+ else if (item?.path) values.push(item.path)
27
+ }
28
+ return [...new Set(values.map(normalizePath).filter(Boolean))]
29
+ }
30
+
31
+ export function measureContextQuality(task = {}, manifest = {}, options = {}) {
32
+ const required = declaredTaskFiles(task)
33
+ const included = manifestPaths(manifest)
34
+ const includedSet = new Set(included)
35
+ const hits = required.filter((file) => includedSet.has(file))
36
+ const requiredFileRecall = required.length ? hits.length / required.length : null
37
+ const relevant = new Set(required)
38
+ for (const item of manifest.instructions || []) {
39
+ if (typeof item === "string") relevant.add(normalizePath(item))
40
+ else if (item?.path) relevant.add(normalizePath(item.path))
41
+ }
42
+ for (const item of manifest.excerpts || []) {
43
+ if (["declared", "test", "instruction"].includes(item?.role) && item?.path) relevant.add(normalizePath(item.path))
44
+ }
45
+ const irrelevant = included.filter((file) => !relevant.has(file))
46
+ const irrelevantRatio = included.length ? irrelevant.length / included.length : 0
47
+ const minRequiredRecall = Number.isFinite(Number(options.minRequiredRecall))
48
+ ? Math.max(0, Math.min(1, Number(options.minRequiredRecall))) : 1
49
+ return {
50
+ schemaVersion: 1,
51
+ requiredFiles: required,
52
+ includedFiles: included,
53
+ requiredFileHits: hits,
54
+ requiredFileRecall,
55
+ irrelevantFiles: irrelevant,
56
+ irrelevantRatio,
57
+ checks: { requiredRecallAcceptable: requiredFileRecall == null ? true : requiredFileRecall >= minRequiredRecall },
58
+ }
59
+ }
@@ -0,0 +1,23 @@
1
+ const HIGH_RISK = /(npm publish|publish package|deploy|production|git push|force push|reset --hard|git clean|delete branch|drop table|truncate|rotate secret|credential|api key|purchase|payment|irreversible|public api break)/i
2
+ const MEDIUM_RISK = /(migration|schema|dependency upgrade|lockfile|generated code|shared config|auth|permission|security)/i
3
+ const REVERSIBLE = /(local|test|rename|format|refactor|temporary|fixture|internal|reversible)/i
4
+
5
+ export function classifyDecisionPolicy(text = "", facts = {}) {
6
+ const value = String(text || "").trim()
7
+ const explicitlyIrreversible = facts.irreversible === true
8
+ const externalSideEffect = facts.externalSideEffect === true
9
+ const destructive = facts.destructive === true || HIGH_RISK.test(value)
10
+ const medium = MEDIUM_RISK.test(value)
11
+ const reversible = facts.reversible === true || (!destructive && REVERSIBLE.test(value))
12
+ const risk = destructive || explicitlyIrreversible || externalSideEffect ? "high" : medium ? "medium" : "low"
13
+ const requiresUser = risk === "high" || facts.productDecision === true
14
+ return {
15
+ schemaVersion: 1, risk, reversible, destructive, externalSideEffect, requiresUser,
16
+ autoResolvable: !requiresUser && (reversible || risk === "low"),
17
+ reason: requiresUser ? "human-approval-required" : reversible ? "reversible-local-decision" : "bounded-engineering-decision",
18
+ }
19
+ }
20
+
21
+ export function canAutoResolveDecision(text = "", facts = {}) {
22
+ return classifyDecisionPolicy(text, facts).autoResolvable
23
+ }