opencode-agent-skill 12.0.0-beta.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.
- package/CHANGELOG.md +37 -0
- package/README.md +110 -11
- package/bin/ocskill.mjs +394 -84
- package/docs/NPM-PUBLISH.md +20 -6
- package/docs/OPENCODE-COMPAT.md +12 -2
- package/docs/V13-PARALLEL-WEAK-MODEL-RUNTIME.md +75 -0
- package/global-config/AGENTS.md +4 -0
- package/global-config/commands/resume.md +4 -1
- package/global-config/commands/run.md +9 -7
- package/global-config/plugins/ues-router/capabilities.js +1 -0
- package/global-config/plugins/ues-router/command-runtime.js +79 -0
- package/global-config/plugins/ues-router/index.js +364 -43
- package/global-config/plugins/ues-router/parallel-runtime.js +271 -0
- package/global-config/plugins/ues-router/text-runtime.js +115 -0
- package/global-config/plugins/ues-router/verifier-runtime.js +33 -0
- package/global-config/skills/dynamic-workflow/SKILL.md +7 -6
- package/global-config/skills/dynamic-workflow/references/workflow.md +8 -6
- package/global-config/skills/engineering-orchestrator/references/long-horizon.md +7 -5
- package/lib/cli-utils.mjs +17 -4
- package/lib/installer.mjs +49 -16
- package/lib/task-engine.mjs +19 -9
- package/lib/task-graph.mjs +32 -0
- package/lib/text-encoding.mjs +74 -0
- package/lib/worktree-sandbox.mjs +141 -12
- package/package.json +5 -4
- package/scripts/check-release-consistency.mjs +33 -1
- package/scripts/smoke-packed-install.mjs +39 -3
- package/scripts/smoke-plain-install.mjs +27 -7
- 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:
|
|
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
|
|
13
|
-
3.
|
|
14
|
-
4.
|
|
15
|
-
5.
|
|
16
|
-
6. integrate
|
|
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
|
|
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
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
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
|
|
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
|
-
-
|
|
53
|
-
-
|
|
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
|
-
|
|
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(
|
|
17
|
+
return JSON.parse(readTextAuto(path.resolve(file)))
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function readTextFile(file) {
|
|
21
|
-
return
|
|
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
|
+
}
|