opencode-agent-skill 15.0.0 → 15.1.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/README.md +5 -5
- package/lib/affected-tests.mjs +37 -4
- package/lib/code-intelligence/index.mjs +3 -2
- package/lib/code-intelligence/lsp-provider.mjs +326 -52
- package/lib/compaction-resume-guard.mjs +255 -0
- package/lib/context-engine-v11.mjs +20 -7
- package/lib/execution-contract.mjs +29 -16
- package/lib/memory-engine.mjs +189 -16
- package/lib/performance-fabric.mjs +262 -107
- package/lib/subagent-artifacts.mjs +182 -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 +1 -1
- package/pi/extensions/ues-child-runtime.ts +25 -2
- package/pi/extensions/ues.ts +290 -15
|
@@ -9,23 +9,41 @@ function clampInt(value, fallback, min, max) {
|
|
|
9
9
|
return Math.max(min, Math.min(max, Math.trunc(parsed)))
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function
|
|
12
|
+
function hintFor(options = {}) {
|
|
13
|
+
return String(options.command || options.source || options.kind || "")
|
|
14
|
+
.trim()
|
|
15
|
+
.replace(/\s+/g, " ")
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function uniqRows(rows, limit = 100) {
|
|
13
20
|
const seen = new Set()
|
|
21
|
+
const result = []
|
|
22
|
+
for (const value of rows || []) {
|
|
23
|
+
const row = String(value || "").trimEnd()
|
|
24
|
+
if (!row || seen.has(row)) continue
|
|
25
|
+
seen.add(row)
|
|
26
|
+
result.push(row)
|
|
27
|
+
if (result.length >= limit) break
|
|
28
|
+
}
|
|
29
|
+
return result
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function matchingLines(source, patterns, limit = 100) {
|
|
14
33
|
const rows = []
|
|
15
|
-
for (const raw of String(
|
|
16
|
-
|
|
17
|
-
if (!line || !SIGNAL_LINE.test(line) || seen.has(line)) continue
|
|
18
|
-
seen.add(line)
|
|
19
|
-
rows.push(line)
|
|
34
|
+
for (const raw of String(source || "").split(/\r?\n/)) {
|
|
35
|
+
if (patterns.some((pattern) => pattern.test(raw))) rows.push(raw)
|
|
20
36
|
if (rows.length >= limit) break
|
|
21
37
|
}
|
|
22
|
-
return rows
|
|
38
|
+
return uniqRows(rows, limit)
|
|
23
39
|
}
|
|
24
40
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
41
|
+
function uniqueSignalLines(text, limit = 48) {
|
|
42
|
+
return matchingLines(text, [SIGNAL_LINE], limit)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function npmTestRows(source, limit = 100) {
|
|
46
|
+
return matchingLines(source, [
|
|
29
47
|
/^FAIL\b/i,
|
|
30
48
|
/^PASS\b/i,
|
|
31
49
|
/^Test Suites:/i,
|
|
@@ -38,80 +56,124 @@ function testSummaryLines(source, limit = 80) {
|
|
|
38
56
|
/^\s*●\s+/,
|
|
39
57
|
/^\s*at\s+.+:\d+:\d+/,
|
|
40
58
|
/(?:AssertionError|Expected|Received|Traceback|panic:|FAILED\s)/i,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
seen.add(line)
|
|
46
|
-
rows.push(line)
|
|
47
|
-
if (rows.length >= limit) break
|
|
48
|
-
}
|
|
49
|
-
return rows
|
|
59
|
+
/^not ok\b/i,
|
|
60
|
+
/^ok\s+\d+\b/i,
|
|
61
|
+
/^#\s+(?:tests|suites|pass|fail|cancelled|skipped|todo|duration_ms)\b/i,
|
|
62
|
+
], limit)
|
|
50
63
|
}
|
|
51
64
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
function pytestRows(source, limit = 100) {
|
|
66
|
+
return matchingLines(source, [
|
|
67
|
+
/^(?:FAILED|ERROR)\b/,
|
|
68
|
+
/^=+\s+.*(?:passed|failed|error|skipped|xfailed|xpassed).*\s+=+$/i,
|
|
69
|
+
/^E\s{2,}/,
|
|
70
|
+
/^_+\s+.+\s+_+$/,
|
|
71
|
+
/^Traceback \(most recent call last\):/,
|
|
72
|
+
/^\s*File ".+", line \d+/,
|
|
73
|
+
/(?:AssertionError|ImportError|ModuleNotFoundError|pytest\.)/,
|
|
74
|
+
/collected\s+\d+\s+items?/i,
|
|
75
|
+
], limit)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function gitDiffRows(source, limit = 140) {
|
|
79
|
+
return matchingLines(source, [
|
|
80
|
+
/^diff --git /,
|
|
81
|
+
/^index [0-9a-f.]+/i,
|
|
82
|
+
/^@@ /,
|
|
83
|
+
/^(?:---|\+\+\+) /,
|
|
84
|
+
/^fatal:/i,
|
|
85
|
+
/^error:/i,
|
|
86
|
+
/^CONFLICT\b/i,
|
|
87
|
+
/^rename (?:from|to) /,
|
|
88
|
+
/^new file mode /,
|
|
89
|
+
/^deleted file mode /,
|
|
90
|
+
/^\s*\d+ files? changed/i,
|
|
91
|
+
], limit)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function rgRows(source, limit = 100) {
|
|
95
|
+
const lines = String(source || "").split(/\r?\n/).filter(Boolean)
|
|
96
|
+
const matched = []
|
|
97
|
+
for (const line of lines) {
|
|
55
98
|
if (
|
|
56
|
-
/^
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
/^CONFLICT\b/i.test(raw)
|
|
62
|
-
) {
|
|
63
|
-
rows.push(raw)
|
|
64
|
-
if (rows.length >= limit) break
|
|
65
|
-
}
|
|
99
|
+
/^(?:[^:\n]+:)?\d+(?::\d+)?:/.test(line) ||
|
|
100
|
+
/^\.?[^:\n]+:\s/.test(line) ||
|
|
101
|
+
/(?:error|permission denied|binary file|no such file)/i.test(line)
|
|
102
|
+
) matched.push(line)
|
|
103
|
+
if (matched.length >= limit) break
|
|
66
104
|
}
|
|
67
|
-
|
|
105
|
+
const rows = matched.length ? matched : lines.slice(0, limit)
|
|
106
|
+
return uniqRows(rows, limit)
|
|
68
107
|
}
|
|
69
108
|
|
|
109
|
+
function treeRows(source, limit = 120) {
|
|
110
|
+
const lines = String(source || "").split(/\r?\n/).filter(Boolean)
|
|
111
|
+
if (lines.length <= limit) return uniqRows(lines, limit)
|
|
112
|
+
const head = lines.slice(0, Math.max(1, limit - 12))
|
|
113
|
+
const tail = lines.slice(-12)
|
|
114
|
+
return uniqRows([...head, `...[tree omitted ${Math.max(0, lines.length - head.length - tail.length)} rows]...`, ...tail], limit)
|
|
115
|
+
}
|
|
70
116
|
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
117
|
+
function tscRows(source, limit = 120) {
|
|
118
|
+
return matchingLines(source, [
|
|
119
|
+
/\berror\s+TS\d+\b/i,
|
|
120
|
+
/^.+\(\d+,\d+\):\s+error\s+TS\d+/i,
|
|
121
|
+
/^.+:\d+:\d+\s+-\s+error\s+TS\d+/i,
|
|
122
|
+
/^Found\s+\d+\s+errors?/i,
|
|
123
|
+
/\b(?:ELIFECYCLE|ERR_|exit code)\b/i,
|
|
124
|
+
], limit)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function eslintRows(source, limit = 120) {
|
|
128
|
+
return matchingLines(source, [
|
|
129
|
+
/^\/?[^\n]+\.[cm]?[jt]sx?$/i,
|
|
130
|
+
/^\s*\d+:\d+\s+(?:error|warning)\s+/i,
|
|
76
131
|
/\b\d+\s+problems?\b/i,
|
|
77
132
|
/\b\d+\s+errors?\b/i,
|
|
78
133
|
/\b\d+\s+warnings?\b/i,
|
|
79
|
-
/\b(?:eslint|ruff|flake8|pylint|clippy)\b/i,
|
|
80
|
-
/:\d+:\d+\b/,
|
|
81
134
|
/^✖\s+/,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const line = raw.trimEnd()
|
|
85
|
-
if (!line || seen.has(line) || !patterns.some((pattern) => pattern.test(line))) continue
|
|
86
|
-
seen.add(line)
|
|
87
|
-
rows.push(line)
|
|
88
|
-
if (rows.length >= limit) break
|
|
89
|
-
}
|
|
90
|
-
return rows
|
|
135
|
+
/\beslint\b.*\b(?:error|warning|failed)\b/i,
|
|
136
|
+
], limit)
|
|
91
137
|
}
|
|
92
138
|
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
/\
|
|
139
|
+
function dockerRows(source, limit = 120) {
|
|
140
|
+
return matchingLines(source, [
|
|
141
|
+
/^#\d+\s+/,
|
|
142
|
+
/^\s*=>\s+/,
|
|
143
|
+
/\b(?:error|failed|failure|fatal|warning|unhealthy|exited)\b/i,
|
|
144
|
+
/^NAME\s+IMAGE\s+COMMAND/i,
|
|
145
|
+
/^NAME\s+STATUS/i,
|
|
146
|
+
/^\[?\+\]?\s+(?:Running|Started|Stopped|Created|Built|Recreated)/i,
|
|
147
|
+
/(?:container|service|network|volume).*(?:created|started|stopped|failed|error)/i,
|
|
148
|
+
], limit)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function prismaRows(source, limit = 120) {
|
|
152
|
+
return matchingLines(source, [
|
|
153
|
+
/Prisma schema loaded from/i,
|
|
154
|
+
/Datasource\s+".+"/i,
|
|
155
|
+
/Applying migration/i,
|
|
156
|
+
/migrations? found/i,
|
|
157
|
+
/Generated Prisma Client/i,
|
|
158
|
+
/Your database is now in sync/i,
|
|
159
|
+
/The following migration/i,
|
|
160
|
+
/\bP\d{4}\b/,
|
|
161
|
+
/\b(?:error|failed|warning)\b/i,
|
|
162
|
+
/schema\.prisma:\d+/i,
|
|
163
|
+
], limit)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function genericBuildRows(source, limit = 100) {
|
|
167
|
+
return matchingLines(source, [
|
|
98
168
|
/\b(?:compile|compiled|compilation|build)\b.*\b(?:failed|success|succeeded|complete|completed)\b/i,
|
|
99
169
|
/\b(?:failed|fatal|exception|panic)\b/i,
|
|
100
170
|
/\bwarning\b/i,
|
|
101
171
|
/:\d+:\d+\b/,
|
|
102
172
|
/\b(?:ELIFECYCLE|ERR_|exit code)\b/i,
|
|
103
|
-
]
|
|
104
|
-
for (const raw of String(source || "").split(/\r?\n/)) {
|
|
105
|
-
const line = raw.trimEnd()
|
|
106
|
-
if (!line || seen.has(line) || !patterns.some((pattern) => pattern.test(line))) continue
|
|
107
|
-
seen.add(line)
|
|
108
|
-
rows.push(line)
|
|
109
|
-
if (rows.length >= limit) break
|
|
110
|
-
}
|
|
111
|
-
return rows
|
|
173
|
+
], limit)
|
|
112
174
|
}
|
|
113
175
|
|
|
114
|
-
function
|
|
176
|
+
function jsonRows(source, limit = 80) {
|
|
115
177
|
const text = String(source || "").trim()
|
|
116
178
|
if (!text || text.length > 4 * 1024 * 1024 || !/^[\[{]/.test(text)) return []
|
|
117
179
|
let value
|
|
@@ -134,50 +196,132 @@ function jsonSummaryLines(source, limit = 80) {
|
|
|
134
196
|
}
|
|
135
197
|
return
|
|
136
198
|
}
|
|
137
|
-
|
|
138
|
-
rows.push(`${prefix || "$"}: ${String(rendered).slice(0, 160)}`)
|
|
199
|
+
rows.push(`${prefix || "$"}: ${String(JSON.stringify(current)).slice(0, 160)}`)
|
|
139
200
|
}
|
|
140
201
|
visit(value, "", 0)
|
|
141
202
|
return rows
|
|
142
203
|
}
|
|
143
204
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
205
|
+
const REDUCERS = Object.freeze([
|
|
206
|
+
{
|
|
207
|
+
family: "npm-test",
|
|
208
|
+
match: (hint) => /(?:^|\s)(?:npm|pnpm|yarn)\s+(?:run\s+)?test\b|node\s+--test\b|\b(?:jest|vitest)\b/.test(hint),
|
|
209
|
+
rows: npmTestRows,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
family: "pytest",
|
|
213
|
+
match: (hint) => /\b(?:pytest|py\.test)\b/.test(hint),
|
|
214
|
+
rows: pytestRows,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
family: "git-diff",
|
|
218
|
+
match: (hint) => /\bgit\s+(?:diff|show)\b/.test(hint),
|
|
219
|
+
rows: gitDiffRows,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
family: "ripgrep",
|
|
223
|
+
match: (hint) => /(?:^|\s)(?:rg|ripgrep)(?:\s|$)|\bgrep\b/.test(hint),
|
|
224
|
+
rows: rgRows,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
family: "tree",
|
|
228
|
+
match: (hint) => /(?:^|[;&|]\s*|\s)(?:tree)(?:\s|$)/.test(hint) || hint === "tree",
|
|
229
|
+
rows: treeRows,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
family: "tsc",
|
|
233
|
+
match: (hint) => /\b(?:tsc|typescript|typecheck)\b/.test(hint),
|
|
234
|
+
rows: tscRows,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
family: "eslint",
|
|
238
|
+
match: (hint) => /\b(?:eslint|lint)\b/.test(hint),
|
|
239
|
+
rows: eslintRows,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
family: "docker",
|
|
243
|
+
match: (hint) => /\bdocker(?:\s+compose)?\b/.test(hint),
|
|
244
|
+
rows: dockerRows,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
family: "prisma",
|
|
248
|
+
match: (hint) => /\bprisma\b|schema\.prisma/.test(hint),
|
|
249
|
+
rows: prismaRows,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
family: "build",
|
|
253
|
+
match: (hint) => /\b(?:compile|build|gradle|maven|mvn|cargo\s+check|dotnet\s+build)\b/.test(hint),
|
|
254
|
+
rows: genericBuildRows,
|
|
255
|
+
},
|
|
256
|
+
])
|
|
257
|
+
|
|
258
|
+
export const COMMAND_REDUCER_FAMILIES = Object.freeze(REDUCERS.map((item) => item.family))
|
|
259
|
+
|
|
260
|
+
export function reduceCommandOutput(source, options = {}) {
|
|
261
|
+
const text = String(source || "")
|
|
262
|
+
const hint = hintFor(options)
|
|
263
|
+
const reducer = REDUCERS.find((item) => item.match(hint))
|
|
264
|
+
if (reducer) {
|
|
265
|
+
const rows = reducer.rows(text)
|
|
266
|
+
if (rows.length) {
|
|
267
|
+
return {
|
|
268
|
+
schemaVersion: 2,
|
|
269
|
+
family: reducer.family,
|
|
270
|
+
hint,
|
|
271
|
+
rows,
|
|
272
|
+
summary: `command-aware reducer=${reducer.family}; selected=${rows.length}; totalLines=${text.split(/\r?\n/).length}`,
|
|
273
|
+
}
|
|
274
|
+
}
|
|
157
275
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
276
|
+
|
|
277
|
+
if (/(?:--json|\bjson\b)/.test(hint) || /^[\s]*[\[{]/.test(text)) {
|
|
278
|
+
const rows = jsonRows(text)
|
|
279
|
+
if (rows.length) {
|
|
280
|
+
return {
|
|
281
|
+
schemaVersion: 2,
|
|
282
|
+
family: "json",
|
|
283
|
+
hint,
|
|
284
|
+
rows,
|
|
285
|
+
summary: `command-aware reducer=json; selected=${rows.length}`,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
161
288
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
289
|
+
|
|
290
|
+
const rows = uniqueSignalLines(text)
|
|
291
|
+
return {
|
|
292
|
+
schemaVersion: 2,
|
|
293
|
+
family: rows.length ? "signal" : "generic",
|
|
294
|
+
hint,
|
|
295
|
+
rows,
|
|
296
|
+
summary: rows.length
|
|
297
|
+
? `generic signal reducer; selected=${rows.length}`
|
|
298
|
+
: "generic head/tail fallback; no structured signal reducer matched",
|
|
165
299
|
}
|
|
166
|
-
|
|
167
|
-
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function legacyStrategyKind(source, options = {}) {
|
|
303
|
+
const hint = hintFor(options)
|
|
304
|
+
if (/(jest|vitest|pytest|test|spec)/.test(hint) && npmTestRows(source).length) return "test"
|
|
305
|
+
if (/(eslint|lint|ruff|flake8|pylint|clippy)/.test(hint) && eslintRows(source).length) return "lint"
|
|
306
|
+
if (/(typecheck|tsc|compile|build|gradle|maven|cargo check|dotnet build)/.test(hint) && genericBuildRows(source).length) return "build"
|
|
307
|
+
if (/(git|diff|status)/.test(hint) && gitDiffRows(source).length) return "git"
|
|
308
|
+
if ((/(json|--json)/.test(hint) || /^[\s]*[\[{]/.test(String(source || ""))) && jsonRows(source).length) return "json"
|
|
309
|
+
return uniqueSignalLines(source).length ? "signal" : "generic"
|
|
168
310
|
}
|
|
169
311
|
|
|
170
312
|
function boundedPreview(text, maxChars, options = {}) {
|
|
171
313
|
const source = String(text || "")
|
|
172
|
-
const headBudget = Math.max(
|
|
173
|
-
const tailBudget = Math.max(
|
|
174
|
-
const signalBudget = Math.max(2048, maxChars - headBudget - tailBudget -
|
|
314
|
+
const headBudget = Math.max(1536, Math.floor(maxChars * 0.18))
|
|
315
|
+
const tailBudget = Math.max(2048, Math.floor(maxChars * 0.24))
|
|
316
|
+
const signalBudget = Math.max(2048, maxChars - headBudget - tailBudget - 1900)
|
|
175
317
|
const head = source.slice(0, headBudget)
|
|
176
318
|
const tail = source.slice(Math.max(head.length, source.length - tailBudget))
|
|
177
|
-
const
|
|
178
|
-
let signalText =
|
|
179
|
-
if (signalText.length > signalBudget)
|
|
180
|
-
|
|
319
|
+
const reduced = reduceCommandOutput(source, options)
|
|
320
|
+
let signalText = [reduced.summary, ...reduced.rows].join("\n")
|
|
321
|
+
if (signalText.length > signalBudget) {
|
|
322
|
+
signalText = signalText.slice(0, signalBudget) + "\n...[reducer output truncated]"
|
|
323
|
+
}
|
|
324
|
+
return { head, tail, signalText, reducer: reduced }
|
|
181
325
|
}
|
|
182
326
|
|
|
183
327
|
export async function compactReversibleOutput(root, text, options = {}) {
|
|
@@ -185,9 +329,10 @@ export async function compactReversibleOutput(root, text, options = {}) {
|
|
|
185
329
|
const maxChars = clampInt(options.maxChars, DEFAULT_LIMIT, 8 * 1024, 256 * 1024)
|
|
186
330
|
if (source.length <= maxChars) {
|
|
187
331
|
return {
|
|
188
|
-
schemaVersion:
|
|
332
|
+
schemaVersion: 2,
|
|
189
333
|
compacted: false,
|
|
190
334
|
strategy: "raw",
|
|
335
|
+
commandFamily: null,
|
|
191
336
|
originalChars: source.length,
|
|
192
337
|
returnedChars: source.length,
|
|
193
338
|
evidenceRef: null,
|
|
@@ -195,35 +340,45 @@ export async function compactReversibleOutput(root, text, options = {}) {
|
|
|
195
340
|
}
|
|
196
341
|
}
|
|
197
342
|
|
|
343
|
+
// Raw bytes are persisted before any model-visible reduction. Reducers never
|
|
344
|
+
// become the verification source of truth; verifiers can recover exact data.
|
|
198
345
|
const evidence = await putEvidence(root, source, {
|
|
199
346
|
kind: options.kind || "raw-tool-output",
|
|
200
|
-
source: options.source || "ues-performance-fabric",
|
|
201
|
-
summary: options.summary || `Raw output preserved before model-visible compaction (${source.length} chars)`,
|
|
347
|
+
source: options.source || options.command || "ues-performance-fabric-v2",
|
|
348
|
+
summary: options.summary || `Raw output preserved before command-aware model-visible compaction (${source.length} chars)`,
|
|
202
349
|
})
|
|
203
|
-
const { head, tail, signalText,
|
|
350
|
+
const { head, tail, signalText, reducer } = boundedPreview(source, maxChars, options)
|
|
204
351
|
const recovery = `Raw captured output: ${evidence.ref}. Recover exact captured bytes in slices with: ues store get ${evidence.ref} --start N --max 24000`
|
|
205
352
|
let preview = [
|
|
206
|
-
`[UES reversible
|
|
353
|
+
`[UES command-aware reversible compaction v2: ${source.length} -> <=${maxChars} chars; reducer=${reducer.family}]`,
|
|
207
354
|
recovery,
|
|
208
355
|
"",
|
|
209
|
-
"---
|
|
356
|
+
"--- reducer summary ---",
|
|
357
|
+
signalText,
|
|
358
|
+
"",
|
|
359
|
+
"--- bounded head ---",
|
|
210
360
|
head,
|
|
211
|
-
|
|
212
|
-
"
|
|
361
|
+
"",
|
|
362
|
+
"--- bounded tail ---",
|
|
213
363
|
tail,
|
|
214
364
|
].filter(Boolean).join("\n")
|
|
215
365
|
if (preview.length > maxChars) {
|
|
216
|
-
const reserve = Math.min(
|
|
366
|
+
const reserve = Math.min(1500, recovery.length + 200)
|
|
217
367
|
preview = preview.slice(0, Math.max(0, maxChars - reserve)) + `\n...[model-visible preview truncated]\n${recovery}`
|
|
218
368
|
}
|
|
219
369
|
|
|
370
|
+
const compatibilityKind = legacyStrategyKind(source, options)
|
|
220
371
|
return {
|
|
221
|
-
schemaVersion:
|
|
372
|
+
schemaVersion: 2,
|
|
222
373
|
compacted: true,
|
|
223
|
-
strategy
|
|
374
|
+
// Keep the V14 public strategy contract stable for callers/tests while
|
|
375
|
+
// exposing the richer V2 reducer identity separately.
|
|
376
|
+
strategy: `reversible-head-${compatibilityKind}-tail`,
|
|
377
|
+
strategyV2: `reversible-command-aware-${reducer.family}`,
|
|
378
|
+
commandFamily: reducer.family,
|
|
224
379
|
originalChars: source.length,
|
|
225
380
|
returnedChars: preview.length,
|
|
226
381
|
evidenceRef: evidence.ref,
|
|
227
382
|
text: preview,
|
|
228
383
|
}
|
|
229
|
-
}
|
|
384
|
+
}
|
|
@@ -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
|
+
}
|