opencode-agent-skill 15.0.0 → 15.1.1
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/README.md +5 -5
- package/docs/V15-MANAGED-RUNTIME.md +18 -0
- package/global-config/agents/verifier.md +6 -2
- package/lib/affected-tests.mjs +58 -8
- package/lib/capability-fabric.mjs +107 -48
- package/lib/code-intelligence/index.mjs +4 -15
- package/lib/code-intelligence/lsp-pool.mjs +694 -0
- package/lib/code-intelligence/lsp-provider.mjs +481 -67
- package/lib/compaction-resume-guard.mjs +327 -0
- package/lib/completion-auditor.mjs +20 -0
- package/lib/context-engine-v11.mjs +20 -7
- package/lib/context-manifest.mjs +38 -21
- package/lib/decision-policy.mjs +40 -2
- package/lib/evidence-store.mjs +121 -31
- package/lib/executable-probe.mjs +69 -0
- package/lib/execution-contract.mjs +29 -16
- package/lib/hierarchical-context.mjs +31 -3
- package/lib/learning-engine.mjs +164 -65
- package/lib/memory-engine.mjs +348 -98
- package/lib/model-config.mjs +86 -7
- package/lib/performance-fabric.mjs +354 -107
- package/lib/permission-policy.mjs +232 -0
- package/lib/pi-rpc-pool.mjs +170 -5
- package/lib/provider-recovery.mjs +51 -0
- package/lib/repo-graph.mjs +38 -5
- package/lib/runtime-events.mjs +31 -10
- package/lib/semantic-index.mjs +67 -20
- package/lib/skill-compiler.mjs +84 -18
- package/lib/subagent-artifacts.mjs +182 -0
- package/lib/task-policy.mjs +80 -7
- package/lib/trajectory.mjs +25 -1
- package/lib/untrusted-output.mjs +106 -0
- package/lib/verification-broker.mjs +32 -27
- package/lib/workspace-fingerprint.mjs +43 -8
- package/lib/workspace-hygiene.mjs +481 -0
- package/package.json +8 -6
- package/pi/extensions/ues-child-runtime.ts +103 -5
- package/pi/extensions/ues.ts +984 -95
- package/scripts/benchmark-lsp-pool.mjs +128 -0
- package/scripts/check-release-consistency.mjs +16 -4
- package/scripts/check-source-integrity.mjs +60 -1
- package/scripts/run-test-suite.mjs +143 -0
- package/scripts/smoke-package-closure.mjs +4 -0
package/lib/semantic-index.mjs
CHANGED
|
@@ -45,6 +45,28 @@ async function atomicJson(file, value) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function indexIoConcurrency(value) {
|
|
49
|
+
const fallback = process.platform === "win32" ? 12 : 16
|
|
50
|
+
const parsed = Number(value)
|
|
51
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback
|
|
52
|
+
return Math.max(1, Math.min(32, Math.trunc(parsed)))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function mapLimit(items, limit, worker) {
|
|
56
|
+
if (!items.length) return []
|
|
57
|
+
const results = new Array(items.length)
|
|
58
|
+
let next = 0
|
|
59
|
+
const runners = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, async () => {
|
|
60
|
+
while (true) {
|
|
61
|
+
const index = next++
|
|
62
|
+
if (index >= items.length) return
|
|
63
|
+
results[index] = await worker(items[index], index)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
await Promise.all(runners)
|
|
67
|
+
return results
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
async function walk(root, options = {}) {
|
|
49
71
|
const maxFiles = Math.max(100, Number(options.maxFiles || 6000))
|
|
50
72
|
const maxDepth = Math.max(2, Number(options.maxDepth || 14))
|
|
@@ -164,32 +186,55 @@ export async function buildSemanticIndex(root = process.cwd(), options = {}) {
|
|
|
164
186
|
let reparsed = 0
|
|
165
187
|
let skippedLarge = 0
|
|
166
188
|
const maxFileBytes = Math.max(64 * 1024, Number(options.maxFileBytes || 1024 * 1024))
|
|
189
|
+
const ioConcurrency = indexIoConcurrency(options.ioConcurrency)
|
|
167
190
|
|
|
168
|
-
|
|
191
|
+
const indexed = await mapLimit(discovered.files, ioConcurrency, async (full) => {
|
|
169
192
|
const info = await stat(full).catch(() => null)
|
|
170
|
-
if (!info?.isFile())
|
|
193
|
+
if (!info?.isFile()) return null
|
|
171
194
|
const relative = rel(root, full)
|
|
172
195
|
const signature = info.size + ":" + Math.trunc(info.mtimeMs)
|
|
173
196
|
const old = previous?.files?.[relative]
|
|
197
|
+
const tooLarge = info.size > maxFileBytes
|
|
198
|
+
|
|
199
|
+
// Reuse only when the current size policy makes the same parse/skip choice.
|
|
174
200
|
if (old?.signature === signature) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
201
|
+
const wasTooLarge = old?.skipped === "too-large"
|
|
202
|
+
if (wasTooLarge === tooLarge) {
|
|
203
|
+
return { relative, entry: old, outcome: "reused" }
|
|
204
|
+
}
|
|
178
205
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
206
|
+
|
|
207
|
+
if (tooLarge) {
|
|
208
|
+
return {
|
|
209
|
+
relative,
|
|
210
|
+
entry: { signature, bytes: info.size, skipped: "too-large", symbols: [], identifiers: {} },
|
|
211
|
+
outcome: "skipped-large",
|
|
212
|
+
}
|
|
183
213
|
}
|
|
214
|
+
|
|
184
215
|
const source = await readFile(full, "utf8").catch(() => "")
|
|
185
|
-
const parsed = source
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
216
|
+
const parsed = source
|
|
217
|
+
? parseSource(source, path.extname(relative).toLowerCase())
|
|
218
|
+
: { symbols: [], identifiers: {} }
|
|
219
|
+
return {
|
|
220
|
+
relative,
|
|
221
|
+
entry: {
|
|
222
|
+
signature,
|
|
223
|
+
bytes: info.size,
|
|
224
|
+
symbols: parsed.symbols,
|
|
225
|
+
identifiers: parsed.identifiers,
|
|
226
|
+
},
|
|
227
|
+
outcome: "reparsed",
|
|
191
228
|
}
|
|
192
|
-
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// Keep cache JSON deterministic even though file I/O is concurrent.
|
|
232
|
+
for (const row of indexed) {
|
|
233
|
+
if (!row) continue
|
|
234
|
+
nextFiles[row.relative] = row.entry
|
|
235
|
+
if (row.outcome === "reused") reused += 1
|
|
236
|
+
else if (row.outcome === "skipped-large") skippedLarge += 1
|
|
237
|
+
else reparsed += 1
|
|
193
238
|
}
|
|
194
239
|
|
|
195
240
|
const removed = previous
|
|
@@ -217,6 +262,7 @@ export async function buildSemanticIndex(root = process.cwd(), options = {}) {
|
|
|
217
262
|
truncated: discovered.truncated,
|
|
218
263
|
cacheFile: rel(root, cachePath(root)),
|
|
219
264
|
durationMs: Date.now() - started,
|
|
265
|
+
ioConcurrency,
|
|
220
266
|
},
|
|
221
267
|
}
|
|
222
268
|
}
|
|
@@ -323,14 +369,15 @@ const RUNTIME_SEMANTIC_INFLIGHT = new Map()
|
|
|
323
369
|
export async function buildSemanticIndexCached(root = process.cwd(), options = {}) {
|
|
324
370
|
root = path.resolve(root)
|
|
325
371
|
const fingerprint = String(options.workspaceFingerprint || "")
|
|
326
|
-
if (!fingerprint || fingerprint === "unknown") {
|
|
327
|
-
|
|
372
|
+
if (!fingerprint || fingerprint === "unknown" || options.rebuild === true) {
|
|
373
|
+
const value = await buildSemanticIndex(root, options)
|
|
374
|
+
return { ...value, runtimeCacheHit: false }
|
|
328
375
|
}
|
|
329
376
|
|
|
330
377
|
const maxFiles = Number(options.maxFiles ?? 6000)
|
|
331
378
|
const maxDepth = Number(options.maxDepth ?? 14)
|
|
332
|
-
const
|
|
333
|
-
const key = [root, fingerprint, maxFiles, maxDepth,
|
|
379
|
+
const maxFileBytes = Math.max(64 * 1024, Number(options.maxFileBytes ?? 1024 * 1024))
|
|
380
|
+
const key = [root, fingerprint, maxFiles, maxDepth, maxFileBytes].join("\u0000")
|
|
334
381
|
if (RUNTIME_SEMANTIC_CACHE.has(key)) {
|
|
335
382
|
const value = RUNTIME_SEMANTIC_CACHE.get(key)
|
|
336
383
|
RUNTIME_SEMANTIC_CACHE.delete(key)
|
package/lib/skill-compiler.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url"
|
|
|
6
6
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
7
7
|
const SKILLS_ROOT = path.join(PACKAGE_ROOT, "global-config", "skills")
|
|
8
8
|
const SKILL_SOURCE_CACHE = new Map()
|
|
9
|
+
const SKILL_SOURCE_INFLIGHT = new Map()
|
|
9
10
|
const COMPILED_SKILL_CACHE = new Map()
|
|
10
11
|
|
|
11
12
|
const DOMAIN_SKILLS = Object.freeze({
|
|
@@ -66,27 +67,87 @@ function compileText(raw, maxChars) {
|
|
|
66
67
|
return selected.join("\n").slice(0, maxChars)
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
function skillNameTokens(name) {
|
|
71
|
+
return String(name || "")
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.split(/[^a-z0-9]+/)
|
|
74
|
+
.filter((token) => token.length >= 4 && !["engineering", "engineer"].includes(token))
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
export function selectSkillNames(taskPolicy = {}, role = "executor", options = {}) {
|
|
70
78
|
const maxSkills = Math.max(1, Math.min(5, Number(options.maxSkills || taskPolicy.maxSkills || 3)))
|
|
71
|
-
const
|
|
72
|
-
|
|
79
|
+
const roleNames = [...(ROLE_SKILLS[role] || [])]
|
|
80
|
+
const domainNames = []
|
|
73
81
|
for (const domain of taskPolicy.domains || []) {
|
|
74
82
|
const mapped = DOMAIN_SKILLS[domain]
|
|
75
|
-
if (mapped)
|
|
83
|
+
if (mapped) domainNames.push(mapped)
|
|
76
84
|
}
|
|
77
|
-
|
|
85
|
+
const names = [...new Set([...roleNames, ...domainNames])]
|
|
86
|
+
const taskText = String(options.taskText || "").toLowerCase()
|
|
87
|
+
if (!taskText) return names.slice(0, maxSkills)
|
|
88
|
+
|
|
89
|
+
const primaryRole = roleNames[0] || null
|
|
90
|
+
const scored = names.map((name, index) => {
|
|
91
|
+
let score = Math.max(0, 40 - index)
|
|
92
|
+
const reasons = []
|
|
93
|
+
if (name === primaryRole) {
|
|
94
|
+
score += 120
|
|
95
|
+
reasons.push("primary-role")
|
|
96
|
+
} else if (roleNames.includes(name)) {
|
|
97
|
+
score += 55
|
|
98
|
+
reasons.push("role")
|
|
99
|
+
}
|
|
100
|
+
if (domainNames.includes(name)) {
|
|
101
|
+
score += 75
|
|
102
|
+
reasons.push("domain")
|
|
103
|
+
}
|
|
104
|
+
const overlaps = skillNameTokens(name).filter((token) => taskText.includes(token))
|
|
105
|
+
if (overlaps.length) {
|
|
106
|
+
score += Math.min(60, overlaps.length * 30)
|
|
107
|
+
reasons.push("task-text")
|
|
108
|
+
}
|
|
109
|
+
if (name === "task-planner" && /\b(plan|planning|architecture|migration|multi[- ]step|phase)\b/i.test(taskText)) {
|
|
110
|
+
score += 45
|
|
111
|
+
reasons.push("planning-signal")
|
|
112
|
+
}
|
|
113
|
+
if (name === "test-verification" && /\b(test|verify|verification|regression|failing|failure)\b/i.test(taskText)) {
|
|
114
|
+
score += 45
|
|
115
|
+
reasons.push("verification-signal")
|
|
116
|
+
}
|
|
117
|
+
if (name === "bug-diagnosis" && /\b(debug|bug|error|failure|crash|regression)\b/i.test(taskText)) {
|
|
118
|
+
score += 45
|
|
119
|
+
reasons.push("debug-signal")
|
|
120
|
+
}
|
|
121
|
+
return { name, score, index, reasons }
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
return scored
|
|
125
|
+
.sort((a, b) => b.score - a.score || a.index - b.index || a.name.localeCompare(b.name))
|
|
126
|
+
.slice(0, maxSkills)
|
|
127
|
+
.map((item) => item.name)
|
|
78
128
|
}
|
|
79
129
|
|
|
80
130
|
async function skillSource(name) {
|
|
81
131
|
if (SKILL_SOURCE_CACHE.has(name)) return SKILL_SOURCE_CACHE.get(name)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
132
|
+
if (SKILL_SOURCE_INFLIGHT.has(name)) return SKILL_SOURCE_INFLIGHT.get(name)
|
|
133
|
+
|
|
134
|
+
const load = (async () => {
|
|
135
|
+
const file = path.join(SKILLS_ROOT, name, "SKILL.md")
|
|
136
|
+
if (!existsSync(file)) {
|
|
137
|
+
SKILL_SOURCE_CACHE.set(name, "")
|
|
138
|
+
return ""
|
|
139
|
+
}
|
|
140
|
+
const raw = await readFile(file, "utf8").catch(() => "")
|
|
141
|
+
SKILL_SOURCE_CACHE.set(name, raw)
|
|
142
|
+
return raw
|
|
143
|
+
})()
|
|
144
|
+
|
|
145
|
+
SKILL_SOURCE_INFLIGHT.set(name, load)
|
|
146
|
+
try {
|
|
147
|
+
return await load
|
|
148
|
+
} finally {
|
|
149
|
+
SKILL_SOURCE_INFLIGHT.delete(name)
|
|
86
150
|
}
|
|
87
|
-
const raw = await readFile(file, "utf8").catch(() => "")
|
|
88
|
-
SKILL_SOURCE_CACHE.set(name, raw)
|
|
89
|
-
return raw
|
|
90
151
|
}
|
|
91
152
|
|
|
92
153
|
export async function compileSkillContext(taskPolicy = {}, role = "executor", options = {}) {
|
|
@@ -98,13 +159,16 @@ export async function compileSkillContext(taskPolicy = {}, role = "executor", op
|
|
|
98
159
|
return { ...COMPILED_SKILL_CACHE.get(cacheKey), cacheHit: true }
|
|
99
160
|
}
|
|
100
161
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
162
|
+
const loadedSources = await Promise.all(
|
|
163
|
+
names.map(async (name) => ({ name, raw: await skillSource(name) })),
|
|
164
|
+
)
|
|
165
|
+
const skills = loadedSources
|
|
166
|
+
.map(({ name, raw }) => {
|
|
167
|
+
if (!raw) return null
|
|
168
|
+
const text = compileText(raw, perSkill)
|
|
169
|
+
return text ? { name, text } : null
|
|
170
|
+
})
|
|
171
|
+
.filter(Boolean)
|
|
108
172
|
|
|
109
173
|
const result = {
|
|
110
174
|
schemaVersion: 1,
|
|
@@ -116,6 +180,7 @@ export async function compileSkillContext(taskPolicy = {}, role = "executor", op
|
|
|
116
180
|
.map((item) => `### Skill: ${item.name}\n${item.text}`)
|
|
117
181
|
.join("\n\n")
|
|
118
182
|
.slice(0, totalChars),
|
|
183
|
+
selectionMode: options.taskText ? "relevance-ranked" : "role-domain-order",
|
|
119
184
|
cacheHit: false,
|
|
120
185
|
}
|
|
121
186
|
COMPILED_SKILL_CACHE.set(cacheKey, result)
|
|
@@ -124,5 +189,6 @@ export async function compileSkillContext(taskPolicy = {}, role = "executor", op
|
|
|
124
189
|
|
|
125
190
|
export function clearSkillCompilerCache() {
|
|
126
191
|
SKILL_SOURCE_CACHE.clear()
|
|
192
|
+
SKILL_SOURCE_INFLIGHT.clear()
|
|
127
193
|
COMPILED_SKILL_CACHE.clear()
|
|
128
194
|
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto"
|
|
2
|
+
import { existsSync } from "node:fs"
|
|
3
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { putEvidence } from "./evidence-store.mjs"
|
|
6
|
+
|
|
7
|
+
const ARTIFACT_DIR = path.join(".ues-work", ".subagents")
|
|
8
|
+
const HANDLE_PATTERN = /^sa-[a-z0-9]+-[a-f0-9]{8}$/
|
|
9
|
+
|
|
10
|
+
function now() {
|
|
11
|
+
return new Date().toISOString()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function hash(value) {
|
|
15
|
+
return createHash("sha256").update(String(value || "")).digest("hex").slice(0, 20)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function artifactDir(root) {
|
|
19
|
+
return path.join(path.resolve(root), ARTIFACT_DIR)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function artifactPath(root, handle) {
|
|
23
|
+
if (!HANDLE_PATTERN.test(String(handle || ""))) throw new Error("invalid subagent handle")
|
|
24
|
+
return path.join(artifactDir(root), handle + ".json")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function atomicJson(file, value) {
|
|
28
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
29
|
+
const temp = file + "." + process.pid + "." + Date.now() + ".tmp"
|
|
30
|
+
await writeFile(temp, JSON.stringify(value, null, 2) + "\n", "utf8")
|
|
31
|
+
try {
|
|
32
|
+
await rename(temp, file)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
await rm(temp, { force: true }).catch(() => {})
|
|
35
|
+
throw error
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function publicArtifact(root, artifact) {
|
|
40
|
+
return {
|
|
41
|
+
...artifact,
|
|
42
|
+
file: path.relative(path.resolve(root), artifactPath(root, artifact.handle)).replaceAll("\\", "/"),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function createSubagentArtifact(root, input = {}) {
|
|
47
|
+
const handle = "sa-" + Date.now().toString(36) + "-" + randomUUID().replaceAll("-", "").slice(0, 8)
|
|
48
|
+
const task = String(input.task || "")
|
|
49
|
+
const taskEvidence = await putEvidence(root, task, {
|
|
50
|
+
kind: "subagent-task",
|
|
51
|
+
source: handle,
|
|
52
|
+
summary: `Exact delegated task for ${input.agent || "UES child"}`,
|
|
53
|
+
})
|
|
54
|
+
const timestamp = now()
|
|
55
|
+
const artifact = {
|
|
56
|
+
schemaVersion: 1,
|
|
57
|
+
handle,
|
|
58
|
+
status: "running",
|
|
59
|
+
agent: input.agent || null,
|
|
60
|
+
role: input.role || null,
|
|
61
|
+
attempt: Math.max(1, Number(input.attempt || 1)),
|
|
62
|
+
traceID: input.traceID || null,
|
|
63
|
+
model: input.model || null,
|
|
64
|
+
modelTier: input.modelTier || null,
|
|
65
|
+
workspaceFingerprint: input.workspaceFingerprint || null,
|
|
66
|
+
taskHash: hash(task),
|
|
67
|
+
taskPreview: task.replace(/\s+/g, " ").trim().slice(0, 1200),
|
|
68
|
+
taskRef: taskEvidence.ref,
|
|
69
|
+
startedAt: timestamp,
|
|
70
|
+
updatedAt: timestamp,
|
|
71
|
+
completedAt: null,
|
|
72
|
+
result: null,
|
|
73
|
+
resume: {
|
|
74
|
+
state: "running",
|
|
75
|
+
taskRef: taskEvidence.ref,
|
|
76
|
+
outputRef: null,
|
|
77
|
+
instruction: "Inspect this handle for status. Do not infer completion until status and verification evidence say so.",
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
await atomicJson(artifactPath(root, handle), artifact)
|
|
81
|
+
return publicArtifact(root, artifact)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function readSubagentArtifact(root, handle) {
|
|
85
|
+
const file = artifactPath(root, handle)
|
|
86
|
+
const parsed = JSON.parse(await readFile(file, "utf8"))
|
|
87
|
+
return publicArtifact(root, parsed)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function finalizeSubagentArtifact(root, handle, result = {}) {
|
|
91
|
+
const current = await readSubagentArtifact(root, handle)
|
|
92
|
+
const output = String(result.output || "")
|
|
93
|
+
const outputEvidence = output
|
|
94
|
+
? await putEvidence(root, output, {
|
|
95
|
+
kind: "subagent-output",
|
|
96
|
+
source: handle,
|
|
97
|
+
summary: `Exact bounded child output for ${current.agent || "UES child"}`,
|
|
98
|
+
})
|
|
99
|
+
: null
|
|
100
|
+
const aborted = Number(result.exitCode) === 130 || String(result.stopReason || "").toLowerCase() === "aborted"
|
|
101
|
+
const status = aborted ? "aborted" : Number(result.exitCode || 0) === 0 ? "completed" : "failed"
|
|
102
|
+
const timestamp = now()
|
|
103
|
+
const artifact = {
|
|
104
|
+
...current,
|
|
105
|
+
file: undefined,
|
|
106
|
+
status,
|
|
107
|
+
updatedAt: timestamp,
|
|
108
|
+
completedAt: timestamp,
|
|
109
|
+
result: {
|
|
110
|
+
exitCode: Number(result.exitCode ?? 1),
|
|
111
|
+
verdict: result.verdict || null,
|
|
112
|
+
stopReason: result.stopReason || null,
|
|
113
|
+
durationMs: Number(result.durationMs || 0),
|
|
114
|
+
toolCalls: Number(result.toolCalls || 0),
|
|
115
|
+
toolNames: Array.isArray(result.toolNames) ? result.toolNames.slice(0, 40) : [],
|
|
116
|
+
childRuntime: result.childRuntime || null,
|
|
117
|
+
workerReused: result.workerReused === true,
|
|
118
|
+
outputRef: outputEvidence?.ref || null,
|
|
119
|
+
outputPreview: output.replace(/\s+/g, " ").trim().slice(0, 1000),
|
|
120
|
+
},
|
|
121
|
+
resume: {
|
|
122
|
+
state: status,
|
|
123
|
+
taskRef: current.taskRef,
|
|
124
|
+
outputRef: outputEvidence?.ref || null,
|
|
125
|
+
instruction: status === "completed"
|
|
126
|
+
? "Reuse the output evidence by reference; do not replay the child unless new evidence or a new task requires it."
|
|
127
|
+
: "Inspect task/output evidence and dispatch a fresh bounded child attempt from this handle's exact task context; do not trust partial narrative state.",
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
await atomicJson(artifactPath(root, handle), artifact)
|
|
131
|
+
return publicArtifact(root, artifact)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function failSubagentArtifact(root, handle, error) {
|
|
135
|
+
const current = await readSubagentArtifact(root, handle)
|
|
136
|
+
const timestamp = now()
|
|
137
|
+
const artifact = {
|
|
138
|
+
...current,
|
|
139
|
+
file: undefined,
|
|
140
|
+
status: "failed",
|
|
141
|
+
updatedAt: timestamp,
|
|
142
|
+
completedAt: timestamp,
|
|
143
|
+
result: {
|
|
144
|
+
exitCode: 1,
|
|
145
|
+
verdict: "FAIL",
|
|
146
|
+
stopReason: "launch-error",
|
|
147
|
+
error: String(error instanceof Error ? error.message : error).slice(0, 2000),
|
|
148
|
+
outputRef: null,
|
|
149
|
+
},
|
|
150
|
+
resume: {
|
|
151
|
+
state: "failed",
|
|
152
|
+
taskRef: current.taskRef,
|
|
153
|
+
outputRef: null,
|
|
154
|
+
instruction: "The child did not complete. Re-dispatch from taskRef after diagnosing the launch/runtime failure.",
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
await atomicJson(artifactPath(root, handle), artifact)
|
|
158
|
+
return publicArtifact(root, artifact)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function listSubagentArtifacts(root, options = {}) {
|
|
162
|
+
const dir = artifactDir(root)
|
|
163
|
+
if (!existsSync(dir)) return { schemaVersion: 1, root: dir, count: 0, artifacts: [] }
|
|
164
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
|
|
165
|
+
const rows = []
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue
|
|
168
|
+
try {
|
|
169
|
+
const parsed = JSON.parse(await readFile(path.join(dir, entry.name), "utf8"))
|
|
170
|
+
if (!HANDLE_PATTERN.test(String(parsed.handle || ""))) continue
|
|
171
|
+
rows.push(publicArtifact(root, parsed))
|
|
172
|
+
} catch {}
|
|
173
|
+
}
|
|
174
|
+
rows.sort((a, b) => Date.parse(b.updatedAt || b.startedAt || "") - Date.parse(a.updatedAt || a.startedAt || "") || a.handle.localeCompare(b.handle))
|
|
175
|
+
const limit = Math.max(1, Math.min(100, Number(options.limit || 20)))
|
|
176
|
+
return {
|
|
177
|
+
schemaVersion: 1,
|
|
178
|
+
root: path.relative(path.resolve(root), dir).replaceAll("\\", "/"),
|
|
179
|
+
count: rows.length,
|
|
180
|
+
artifacts: rows.slice(0, limit),
|
|
181
|
+
}
|
|
182
|
+
}
|
package/lib/task-policy.mjs
CHANGED
|
@@ -172,6 +172,28 @@ function signal(name, matched, weight) {
|
|
|
172
172
|
return matched ? { name, weight } : null
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
function taskDecisionConfidence(input = {}) {
|
|
176
|
+
if (
|
|
177
|
+
input.declaredHighRisk === true ||
|
|
178
|
+
input.sensitiveMutation === true ||
|
|
179
|
+
input.explicitLongHorizon === true ||
|
|
180
|
+
input.readOnly === true ||
|
|
181
|
+
input.singleFileBounded === true
|
|
182
|
+
) return 0.96
|
|
183
|
+
if (input.debugging === true && input.boundedDebugHint === true) return 0.84
|
|
184
|
+
if (input.debugging === true && input.diagnosisEvidence !== true) return 0.68
|
|
185
|
+
if (Number(input.score || 0) >= 3) return 0.86
|
|
186
|
+
if (Number(input.score || 0) >= 2) return 0.80
|
|
187
|
+
return 0.74
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function confidenceBand(value) {
|
|
191
|
+
const score = Number(value || 0)
|
|
192
|
+
if (score >= 0.90) return "high"
|
|
193
|
+
if (score >= 0.72) return "medium"
|
|
194
|
+
return "low"
|
|
195
|
+
}
|
|
196
|
+
|
|
175
197
|
function profileFor(mode, risk) {
|
|
176
198
|
if (mode === "inline" && risk === "low") {
|
|
177
199
|
return {
|
|
@@ -348,6 +370,7 @@ export function classifyEngineeringTask(text, facts = {}) {
|
|
|
348
370
|
/\blong\s*[/|,+]\s*(?:high|critical)[-\s]?risk\b/i.test(value) ||
|
|
349
371
|
/\b(?:high|critical)[-\s]?risk\s*[/|,+]\s*long\b/i.test(value)
|
|
350
372
|
const explicitLongHorizon = declaredLongHorizon || compoundLongRisk || LONG.test(value)
|
|
373
|
+
const debugging = DEBUG.test(value)
|
|
351
374
|
const signals = [
|
|
352
375
|
signal("long-request-text", value.length > 700, 1),
|
|
353
376
|
signal("medium-request-text", value.length > 250, 1),
|
|
@@ -355,7 +378,7 @@ export function classifyEngineeringTask(text, facts = {}) {
|
|
|
355
378
|
signal("high-risk-operation", sensitiveMutation, 2),
|
|
356
379
|
signal("declared-high-risk", declaredHighRisk, 2),
|
|
357
380
|
signal("explicit-long-horizon", explicitLongHorizon, 2),
|
|
358
|
-
signal("debugging",
|
|
381
|
+
signal("debugging", debugging, 1),
|
|
359
382
|
signal("read-only", readOnly, -1),
|
|
360
383
|
signal("single-file-bounded", singleFileBounded, -2),
|
|
361
384
|
signal("public-contract", CONTRACT.test(value) || facts.hasPublicContract, 2),
|
|
@@ -369,18 +392,46 @@ export function classifyEngineeringTask(text, facts = {}) {
|
|
|
369
392
|
const score = Math.max(0, signals.reduce((sum, item) => sum + item.weight, 0))
|
|
370
393
|
const highRisk = declaredHighRisk || sensitiveMutation || Boolean(facts.hasMigration) || Boolean(facts.hasPublicContract)
|
|
371
394
|
const diagnosisEvidence = facts.failureEvidence === true || CONCRETE_DIAGNOSIS.test(value)
|
|
395
|
+
const boundedDebugHint =
|
|
396
|
+
/\b(?:local|helper|parser|function|method|hook|useeffect|component|single|one)\b/i.test(value) &&
|
|
397
|
+
!BROAD_FILE_SCOPE.test(value)
|
|
398
|
+
const ambiguousDebug =
|
|
399
|
+
debugging &&
|
|
400
|
+
!diagnosisEvidence &&
|
|
401
|
+
!singleFileBounded &&
|
|
402
|
+
!boundedDebugHint &&
|
|
403
|
+
!explicitLongHorizon &&
|
|
404
|
+
!highRisk
|
|
372
405
|
const risk = highRisk ? "high" : singleFileBounded ? "low" : score >= 3 ? "medium" : "low"
|
|
373
406
|
const mode =
|
|
374
407
|
explicitLongHorizon
|
|
375
408
|
? "long-horizon"
|
|
376
409
|
: singleFileBounded && !highRisk
|
|
377
410
|
? "inline"
|
|
378
|
-
:
|
|
379
|
-
? "
|
|
380
|
-
: score >=
|
|
381
|
-
? "
|
|
382
|
-
:
|
|
383
|
-
|
|
411
|
+
: ambiguousDebug
|
|
412
|
+
? "standard"
|
|
413
|
+
: score >= 4
|
|
414
|
+
? "long-horizon"
|
|
415
|
+
: score >= 2
|
|
416
|
+
? "standard"
|
|
417
|
+
: "inline"
|
|
418
|
+
const modelTier =
|
|
419
|
+
risk === "high" || mode === "long-horizon"
|
|
420
|
+
? "heavy"
|
|
421
|
+
: ambiguousDebug || score >= 2
|
|
422
|
+
? "standard"
|
|
423
|
+
: "light"
|
|
424
|
+
const decisionConfidence = taskDecisionConfidence({
|
|
425
|
+
declaredHighRisk,
|
|
426
|
+
sensitiveMutation,
|
|
427
|
+
explicitLongHorizon,
|
|
428
|
+
readOnly,
|
|
429
|
+
singleFileBounded,
|
|
430
|
+
debugging,
|
|
431
|
+
diagnosisEvidence,
|
|
432
|
+
boundedDebugHint,
|
|
433
|
+
score,
|
|
434
|
+
})
|
|
384
435
|
const maxAttempts = risk === "high" ? 2 : 3
|
|
385
436
|
const profile = profileFor(mode, risk)
|
|
386
437
|
|
|
@@ -412,6 +463,28 @@ export function classifyEngineeringTask(text, facts = {}) {
|
|
|
412
463
|
readOnly,
|
|
413
464
|
singleFileBounded,
|
|
414
465
|
diagnosisEvidence,
|
|
466
|
+
ambiguousDebug,
|
|
467
|
+
boundedDebugHint,
|
|
468
|
+
decision: {
|
|
469
|
+
schemaVersion: 1,
|
|
470
|
+
kind: "task-route",
|
|
471
|
+
source: "deterministic",
|
|
472
|
+
confidence: decisionConfidence,
|
|
473
|
+
confidenceBand: confidenceBand(decisionConfidence),
|
|
474
|
+
value: { mode, risk, modelTier },
|
|
475
|
+
crossCheckRecommended: decisionConfidence < 0.72,
|
|
476
|
+
reason: ambiguousDebug
|
|
477
|
+
? "debug-task-not-yet-bounded-by-file-or-concrete-failure-evidence"
|
|
478
|
+
: singleFileBounded
|
|
479
|
+
? "single-file-bounded"
|
|
480
|
+
: boundedDebugHint
|
|
481
|
+
? "bounded-debug-hint"
|
|
482
|
+
: explicitLongHorizon
|
|
483
|
+
? "explicit-long-horizon"
|
|
484
|
+
: highRisk
|
|
485
|
+
? "high-risk-signal"
|
|
486
|
+
: "weighted-task-signals",
|
|
487
|
+
},
|
|
415
488
|
profile: readOnly
|
|
416
489
|
? {
|
|
417
490
|
...profile,
|
package/lib/trajectory.mjs
CHANGED
|
@@ -9,6 +9,25 @@ const DEFAULT_MAX_TRACE_TOTAL_BYTES = 24 * 1024 * 1024
|
|
|
9
9
|
const DEFAULT_MAX_TRACE_FILES = 32
|
|
10
10
|
const DEFAULT_MAX_TRACE_AGE_MS = 7 * 86_400_000
|
|
11
11
|
|
|
12
|
+
const TRACE_WRITE_TAILS = new Map()
|
|
13
|
+
|
|
14
|
+
async function withTraceWriteLock(root, fn) {
|
|
15
|
+
const key = path.resolve(root)
|
|
16
|
+
const previous = TRACE_WRITE_TAILS.get(key) || Promise.resolve()
|
|
17
|
+
let release
|
|
18
|
+
const barrier = new Promise((resolve) => { release = resolve })
|
|
19
|
+
const tail = previous.catch(() => {}).then(() => barrier)
|
|
20
|
+
TRACE_WRITE_TAILS.set(key, tail)
|
|
21
|
+
|
|
22
|
+
await previous.catch(() => {})
|
|
23
|
+
try {
|
|
24
|
+
return await fn()
|
|
25
|
+
} finally {
|
|
26
|
+
release()
|
|
27
|
+
if (TRACE_WRITE_TAILS.get(key) === tail) TRACE_WRITE_TAILS.delete(key)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
function positiveInt(value, fallback, min, max) {
|
|
13
32
|
const parsed = Number(value)
|
|
14
33
|
if (!Number.isFinite(parsed)) return fallback
|
|
@@ -154,7 +173,7 @@ export function trajectoryFile(root, traceID) {
|
|
|
154
173
|
return path.join(path.resolve(root), TRACE_DIR, cleanID(traceID) + ".jsonl")
|
|
155
174
|
}
|
|
156
175
|
|
|
157
|
-
|
|
176
|
+
async function appendTrajectoryEventUnlocked(root, traceID, type, payload = {}) {
|
|
158
177
|
root = path.resolve(root)
|
|
159
178
|
const file = trajectoryFile(root, traceID)
|
|
160
179
|
await mkdir(path.dirname(file), { recursive: true })
|
|
@@ -182,6 +201,11 @@ export async function appendTrajectoryEvent(root, traceID, type, payload = {}) {
|
|
|
182
201
|
return { file: path.relative(path.resolve(root), file).replaceAll("\\", "/"), event }
|
|
183
202
|
}
|
|
184
203
|
|
|
204
|
+
export async function appendTrajectoryEvent(root, traceID, type, payload = {}) {
|
|
205
|
+
root = path.resolve(root)
|
|
206
|
+
return withTraceWriteLock(root, () => appendTrajectoryEventUnlocked(root, traceID, type, payload))
|
|
207
|
+
}
|
|
208
|
+
|
|
185
209
|
export async function readTrajectory(root, traceID, options = {}) {
|
|
186
210
|
const file = trajectoryFile(root, traceID)
|
|
187
211
|
const source = await readFile(file, "utf8").catch(() => "")
|