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.
@@ -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 uniqueSignalLines(text, limit = 48) {
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(text || "").split(/\r?\n/)) {
16
- const line = raw.trimEnd()
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 testSummaryLines(source, limit = 80) {
26
- const rows = []
27
- const seen = new Set()
28
- const patterns = [
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
- for (const raw of String(source || "").split(/\r?\n/)) {
43
- const line = raw.trimEnd()
44
- if (!line || seen.has(line) || !patterns.some((re) => re.test(line))) continue
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 gitSummaryLines(source, limit = 120) {
53
- const rows = []
54
- for (const raw of String(source || "").split(/\r?\n/)) {
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
- /^diff --git /.test(raw) ||
57
- /^@@ /.test(raw) ||
58
- /^(?:---|\+\+\+) /.test(raw) ||
59
- /^fatal:/i.test(raw) ||
60
- /^error:/i.test(raw) ||
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
- return rows
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 lintSummaryLines(source, limit = 100) {
72
- const rows = []
73
- const seen = new Set()
74
- const patterns = [
75
- /\b(?:error|warning)\b/i,
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
- for (const raw of String(source || "").split(/\r?\n/)) {
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 buildSummaryLines(source, limit = 100) {
94
- const rows = []
95
- const seen = new Set()
96
- const patterns = [
97
- /\berror\s+TS\d+\b/i,
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 jsonSummaryLines(source, limit = 80) {
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
- const rendered = JSON.stringify(current)
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
- function structuredSignalLines(source, options = {}) {
145
- const hint = String(options.kind || options.source || options.command || "").toLowerCase()
146
- if (/(jest|vitest|pytest|test|spec)/.test(hint)) {
147
- const rows = testSummaryLines(source)
148
- if (rows.length) return { kind: "test", rows }
149
- }
150
- if (/(eslint|lint|ruff|flake8|pylint|clippy)/.test(hint)) {
151
- const rows = lintSummaryLines(source)
152
- if (rows.length) return { kind: "lint", rows }
153
- }
154
- if (/(typecheck|tsc|compile|build|gradle|maven|cargo check|dotnet build)/.test(hint)) {
155
- const rows = buildSummaryLines(source)
156
- if (rows.length) return { kind: "build", rows }
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
- if (/(git|diff|status)/.test(hint)) {
159
- const rows = gitSummaryLines(source)
160
- if (rows.length) return { kind: "git", rows }
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
- if (/(json|--json)/.test(hint) || /^[\s]*[\[{]/.test(String(source || ""))) {
163
- const rows = jsonSummaryLines(source)
164
- if (rows.length) return { kind: "json", rows }
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
- const rows = uniqueSignalLines(source)
167
- return { kind: rows.length ? "signal" : "generic", rows }
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(2048, Math.floor(maxChars * 0.24))
173
- const tailBudget = Math.max(4096, Math.floor(maxChars * 0.30))
174
- const signalBudget = Math.max(2048, maxChars - headBudget - tailBudget - 1800)
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 structured = structuredSignalLines(source, options)
178
- let signalText = structured.rows.join("\n")
179
- if (signalText.length > signalBudget) signalText = signalText.slice(0, signalBudget) + "\n...[signal lines truncated]"
180
- return { head, tail, signalText, signalKind: structured.kind }
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: 1,
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, signalKind } = boundedPreview(source, maxChars, options)
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 output compaction: ${source.length} -> <=${maxChars} chars; reducer=${signalKind}]`,
353
+ `[UES command-aware reversible compaction v2: ${source.length} -> <=${maxChars} chars; reducer=${reducer.family}]`,
207
354
  recovery,
208
355
  "",
209
- "--- head ---",
356
+ "--- reducer summary ---",
357
+ signalText,
358
+ "",
359
+ "--- bounded head ---",
210
360
  head,
211
- signalText ? "\n--- high-signal lines ---\n" + signalText : "",
212
- "\n--- tail ---",
361
+ "",
362
+ "--- bounded tail ---",
213
363
  tail,
214
364
  ].filter(Boolean).join("\n")
215
365
  if (preview.length > maxChars) {
216
- const reserve = Math.min(1400, recovery.length + 160)
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: 1,
372
+ schemaVersion: 2,
222
373
  compacted: true,
223
- strategy: `reversible-head-${signalKind}-tail`,
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
+ }