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.
Files changed (43) hide show
  1. package/README.md +5 -5
  2. package/docs/V15-MANAGED-RUNTIME.md +18 -0
  3. package/global-config/agents/verifier.md +6 -2
  4. package/lib/affected-tests.mjs +58 -8
  5. package/lib/capability-fabric.mjs +107 -48
  6. package/lib/code-intelligence/index.mjs +4 -15
  7. package/lib/code-intelligence/lsp-pool.mjs +694 -0
  8. package/lib/code-intelligence/lsp-provider.mjs +481 -67
  9. package/lib/compaction-resume-guard.mjs +327 -0
  10. package/lib/completion-auditor.mjs +20 -0
  11. package/lib/context-engine-v11.mjs +20 -7
  12. package/lib/context-manifest.mjs +38 -21
  13. package/lib/decision-policy.mjs +40 -2
  14. package/lib/evidence-store.mjs +121 -31
  15. package/lib/executable-probe.mjs +69 -0
  16. package/lib/execution-contract.mjs +29 -16
  17. package/lib/hierarchical-context.mjs +31 -3
  18. package/lib/learning-engine.mjs +164 -65
  19. package/lib/memory-engine.mjs +348 -98
  20. package/lib/model-config.mjs +86 -7
  21. package/lib/performance-fabric.mjs +354 -107
  22. package/lib/permission-policy.mjs +232 -0
  23. package/lib/pi-rpc-pool.mjs +170 -5
  24. package/lib/provider-recovery.mjs +51 -0
  25. package/lib/repo-graph.mjs +38 -5
  26. package/lib/runtime-events.mjs +31 -10
  27. package/lib/semantic-index.mjs +67 -20
  28. package/lib/skill-compiler.mjs +84 -18
  29. package/lib/subagent-artifacts.mjs +182 -0
  30. package/lib/task-policy.mjs +80 -7
  31. package/lib/trajectory.mjs +25 -1
  32. package/lib/untrusted-output.mjs +106 -0
  33. package/lib/verification-broker.mjs +32 -27
  34. package/lib/workspace-fingerprint.mjs +43 -8
  35. package/lib/workspace-hygiene.mjs +481 -0
  36. package/package.json +8 -6
  37. package/pi/extensions/ues-child-runtime.ts +103 -5
  38. package/pi/extensions/ues.ts +984 -95
  39. package/scripts/benchmark-lsp-pool.mjs +128 -0
  40. package/scripts/check-release-consistency.mjs +16 -4
  41. package/scripts/check-source-integrity.mjs +60 -1
  42. package/scripts/run-test-suite.mjs +143 -0
  43. package/scripts/smoke-package-closure.mjs +4 -0
@@ -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,191 @@ 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 gitStatusRows(source, limit = 120) {
95
+ return matchingLines(source, [
96
+ /^##\s+/,
97
+ /^On branch\s+/i,
98
+ /^Your branch\s+/i,
99
+ /^Changes to be committed:/i,
100
+ /^Changes not staged for commit:/i,
101
+ /^Untracked files:/i,
102
+ /^Unmerged paths:/i,
103
+ /^\s*(?:nothing to commit|working tree clean)/i,
104
+ /^[ MARCUD?!]{1,2}\s+\S+/,
105
+ ], limit)
106
+ }
107
+
108
+ function gitLogRows(source, limit = 100) {
109
+ return matchingLines(source, [
110
+ /^commit\s+[0-9a-f]{7,40}\b/i,
111
+ /^Author:\s+/i,
112
+ /^Date:\s+/i,
113
+ /^Merge:\s+/i,
114
+ /^\s{4}\S+/,
115
+ /^fatal:/i,
116
+ /^error:/i,
117
+ ], limit)
118
+ }
119
+
120
+ function verificationTestRows(source, limit = 120) {
121
+ return matchingLines(source, [
122
+ /^(?:ok|FAIL)\s+\S+/,
123
+ /^---\s+(?:PASS|FAIL):\s+/,
124
+ /^test result:\s+(?:ok|FAILED)\b/i,
125
+ /^failures?:/i,
126
+ /^running\s+\d+\s+tests?/i,
127
+ /^Finished\s+test\b/i,
128
+ /^Total tests:\s*\d+/i,
129
+ /^\s*(?:Passed|Failed|Skipped):\s*\d+/i,
130
+ /\b(?:panic|assertion failed|FAILED|FAILURE|ERROR)\b/i,
131
+ /\berror\[E\d+\]/,
132
+ /\bBUILD\s+(?:SUCCESS|FAILURE)\b/i,
133
+ /\bTests run:\s*\d+/i,
134
+ ], limit)
135
+ }
136
+
137
+ function pythonLintRows(source, limit = 120) {
138
+ return matchingLines(source, [
139
+ /^.+:\d+:\d+:\s+[A-Z]+\d+\s+/,
140
+ /^.+:\d+:\d+:\s+(?:error|warning)\b/i,
141
+ /^Found\s+\d+\s+errors?/i,
142
+ /^All checks passed!?$/i,
143
+ /^\d+\s+files?\s+(?:reformatted|left unchanged)/i,
144
+ /\b(?:error|warning|failed)\b/i,
145
+ ], limit)
146
+ }
147
+
148
+ function packageInstallRows(source, limit = 120) {
149
+ return matchingLines(source, [
150
+ /\b(?:added|removed|changed)\s+\d+\s+packages?\b/i,
151
+ /\baudited\s+\d+\s+packages?\b/i,
152
+ /\b\d+\s+packages?\s+(?:are\s+)?looking for funding\b/i,
153
+ /\b\d+\s+vulnerabilit(?:y|ies)\b/i,
154
+ /\b(?:deprecated|warning|warn|error|ERR!|ELIFECYCLE|ERR_)\b/i,
155
+ /^Packages:\s+[+-]?\d+/i,
156
+ /^Progress:\s+/i,
157
+ /^Done in\s+/i,
158
+ ], limit)
159
+ }
160
+
161
+ function rgRows(source, limit = 100) {
162
+ const lines = String(source || "").split(/\r?\n/).filter(Boolean)
163
+ const matched = []
164
+ for (const line of lines) {
55
165
  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
- }
166
+ /^(?:[^:\n]+:)?\d+(?::\d+)?:/.test(line) ||
167
+ /^\.?[^:\n]+:\s/.test(line) ||
168
+ /(?:error|permission denied|binary file|no such file)/i.test(line)
169
+ ) matched.push(line)
170
+ if (matched.length >= limit) break
66
171
  }
67
- return rows
172
+ const rows = matched.length ? matched : lines.slice(0, limit)
173
+ return uniqRows(rows, limit)
68
174
  }
69
175
 
176
+ function treeRows(source, limit = 120) {
177
+ const lines = String(source || "").split(/\r?\n/).filter(Boolean)
178
+ if (lines.length <= limit) return uniqRows(lines, limit)
179
+ const head = lines.slice(0, Math.max(1, limit - 12))
180
+ const tail = lines.slice(-12)
181
+ return uniqRows([...head, `...[tree omitted ${Math.max(0, lines.length - head.length - tail.length)} rows]...`, ...tail], limit)
182
+ }
70
183
 
71
- function lintSummaryLines(source, limit = 100) {
72
- const rows = []
73
- const seen = new Set()
74
- const patterns = [
75
- /\b(?:error|warning)\b/i,
184
+ function tscRows(source, limit = 120) {
185
+ return matchingLines(source, [
186
+ /\berror\s+TS\d+\b/i,
187
+ /^.+\(\d+,\d+\):\s+error\s+TS\d+/i,
188
+ /^.+:\d+:\d+\s+-\s+error\s+TS\d+/i,
189
+ /^Found\s+\d+\s+errors?/i,
190
+ /\b(?:ELIFECYCLE|ERR_|exit code)\b/i,
191
+ ], limit)
192
+ }
193
+
194
+ function eslintRows(source, limit = 120) {
195
+ return matchingLines(source, [
196
+ /^\/?[^\n]+\.[cm]?[jt]sx?$/i,
197
+ /^\s*\d+:\d+\s+(?:error|warning)\s+/i,
76
198
  /\b\d+\s+problems?\b/i,
77
199
  /\b\d+\s+errors?\b/i,
78
200
  /\b\d+\s+warnings?\b/i,
79
- /\b(?:eslint|ruff|flake8|pylint|clippy)\b/i,
80
- /:\d+:\d+\b/,
81
201
  /^✖\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
202
+ /\beslint\b.*\b(?:error|warning|failed)\b/i,
203
+ ], limit)
91
204
  }
92
205
 
93
- function buildSummaryLines(source, limit = 100) {
94
- const rows = []
95
- const seen = new Set()
96
- const patterns = [
97
- /\berror\s+TS\d+\b/i,
206
+ function dockerRows(source, limit = 120) {
207
+ return matchingLines(source, [
208
+ /^#\d+\s+/,
209
+ /^\s*=>\s+/,
210
+ /\b(?:error|failed|failure|fatal|warning|unhealthy|exited)\b/i,
211
+ /^NAME\s+IMAGE\s+COMMAND/i,
212
+ /^NAME\s+STATUS/i,
213
+ /^\[?\+\]?\s+(?:Running|Started|Stopped|Created|Built|Recreated)/i,
214
+ /(?:container|service|network|volume).*(?:created|started|stopped|failed|error)/i,
215
+ ], limit)
216
+ }
217
+
218
+ function prismaRows(source, limit = 120) {
219
+ return matchingLines(source, [
220
+ /Prisma schema loaded from/i,
221
+ /Datasource\s+".+"/i,
222
+ /Applying migration/i,
223
+ /migrations? found/i,
224
+ /Generated Prisma Client/i,
225
+ /Your database is now in sync/i,
226
+ /The following migration/i,
227
+ /\bP\d{4}\b/,
228
+ /\b(?:error|failed|warning)\b/i,
229
+ /schema\.prisma:\d+/i,
230
+ ], limit)
231
+ }
232
+
233
+ function genericBuildRows(source, limit = 100) {
234
+ return matchingLines(source, [
98
235
  /\b(?:compile|compiled|compilation|build)\b.*\b(?:failed|success|succeeded|complete|completed)\b/i,
99
236
  /\b(?:failed|fatal|exception|panic)\b/i,
100
237
  /\bwarning\b/i,
101
238
  /:\d+:\d+\b/,
102
239
  /\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
240
+ ], limit)
112
241
  }
113
242
 
114
- function jsonSummaryLines(source, limit = 80) {
243
+ function jsonRows(source, limit = 80) {
115
244
  const text = String(source || "").trim()
116
245
  if (!text || text.length > 4 * 1024 * 1024 || !/^[\[{]/.test(text)) return []
117
246
  let value
@@ -134,50 +263,157 @@ function jsonSummaryLines(source, limit = 80) {
134
263
  }
135
264
  return
136
265
  }
137
- const rendered = JSON.stringify(current)
138
- rows.push(`${prefix || "$"}: ${String(rendered).slice(0, 160)}`)
266
+ rows.push(`${prefix || "$"}: ${String(JSON.stringify(current)).slice(0, 160)}`)
139
267
  }
140
268
  visit(value, "", 0)
141
269
  return rows
142
270
  }
143
271
 
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 }
272
+ const REDUCERS = Object.freeze([
273
+ {
274
+ family: "npm-test",
275
+ match: (hint) => /(?:^|\s)(?:npm|pnpm|yarn)\s+(?:run\s+)?test\b|node\s+--test\b|\b(?:jest|vitest)\b/.test(hint),
276
+ rows: npmTestRows,
277
+ },
278
+ {
279
+ family: "pytest",
280
+ match: (hint) => /\b(?:pytest|py\.test)\b/.test(hint),
281
+ rows: pytestRows,
282
+ },
283
+ {
284
+ family: "git-status",
285
+ match: (hint) => /\bgit\s+status\b/.test(hint),
286
+ rows: gitStatusRows,
287
+ },
288
+ {
289
+ family: "git-log",
290
+ match: (hint) => /\bgit\s+log\b/.test(hint),
291
+ rows: gitLogRows,
292
+ },
293
+ {
294
+ family: "git-diff",
295
+ match: (hint) => /\bgit\s+(?:diff|show)\b/.test(hint),
296
+ rows: gitDiffRows,
297
+ },
298
+ {
299
+ family: "ripgrep",
300
+ match: (hint) => /(?:^|\s)(?:rg|ripgrep)(?:\s|$)|\bgrep\b/.test(hint),
301
+ rows: rgRows,
302
+ },
303
+ {
304
+ family: "tree",
305
+ match: (hint) => /(?:^|[;&|]\s*|\s)(?:tree)(?:\s|$)/.test(hint) || hint === "tree",
306
+ rows: treeRows,
307
+ },
308
+ {
309
+ family: "tsc",
310
+ match: (hint) => /\b(?:tsc|typescript|typecheck)\b/.test(hint),
311
+ rows: tscRows,
312
+ },
313
+ {
314
+ family: "eslint",
315
+ match: (hint) => /\b(?:eslint|lint)\b/.test(hint),
316
+ rows: eslintRows,
317
+ },
318
+ {
319
+ family: "docker",
320
+ match: (hint) => /\bdocker(?:\s+compose)?\b/.test(hint),
321
+ rows: dockerRows,
322
+ },
323
+ {
324
+ family: "prisma",
325
+ match: (hint) => /\bprisma\b|schema\.prisma/.test(hint),
326
+ rows: prismaRows,
327
+ },
328
+ {
329
+ family: "package-install",
330
+ match: (hint) => /(?:^|\s)(?:npm\s+(?:i|install|ci)|pnpm\s+(?:i|install|add)|yarn\s+(?:install|add))(?:\s|$)/.test(hint),
331
+ rows: packageInstallRows,
332
+ },
333
+ {
334
+ family: "verification-test",
335
+ match: (hint) => /\b(?:go\s+test|cargo\s+test|dotnet\s+test|mvn\s+test|gradle\s+test)\b/.test(hint),
336
+ rows: verificationTestRows,
337
+ },
338
+ {
339
+ family: "python-lint",
340
+ match: (hint) => /\b(?:ruff|flake8|pylint)\b/.test(hint),
341
+ rows: pythonLintRows,
342
+ },
343
+ {
344
+ family: "build",
345
+ match: (hint) => /\b(?:compile|build|gradle|maven|mvn|cargo\s+check|dotnet\s+build)\b/.test(hint),
346
+ rows: genericBuildRows,
347
+ },
348
+ ])
349
+
350
+ export const COMMAND_REDUCER_FAMILIES = Object.freeze(REDUCERS.map((item) => item.family))
351
+
352
+ export function reduceCommandOutput(source, options = {}) {
353
+ const text = String(source || "")
354
+ const hint = hintFor(options)
355
+ const reducer = REDUCERS.find((item) => item.match(hint))
356
+ if (reducer) {
357
+ const rows = reducer.rows(text)
358
+ if (rows.length) {
359
+ return {
360
+ schemaVersion: 2,
361
+ family: reducer.family,
362
+ hint,
363
+ rows,
364
+ summary: `command-aware reducer=${reducer.family}; selected=${rows.length}; totalLines=${text.split(/\r?\n/).length}`,
365
+ }
366
+ }
157
367
  }
158
- if (/(git|diff|status)/.test(hint)) {
159
- const rows = gitSummaryLines(source)
160
- if (rows.length) return { kind: "git", rows }
368
+
369
+ if (/(?:--json|\bjson\b)/.test(hint) || /^[\s]*[\[{]/.test(text)) {
370
+ const rows = jsonRows(text)
371
+ if (rows.length) {
372
+ return {
373
+ schemaVersion: 2,
374
+ family: "json",
375
+ hint,
376
+ rows,
377
+ summary: `command-aware reducer=json; selected=${rows.length}`,
378
+ }
379
+ }
161
380
  }
162
- if (/(json|--json)/.test(hint) || /^[\s]*[\[{]/.test(String(source || ""))) {
163
- const rows = jsonSummaryLines(source)
164
- if (rows.length) return { kind: "json", rows }
381
+
382
+ const rows = uniqueSignalLines(text)
383
+ return {
384
+ schemaVersion: 2,
385
+ family: rows.length ? "signal" : "generic",
386
+ hint,
387
+ rows,
388
+ summary: rows.length
389
+ ? `generic signal reducer; selected=${rows.length}`
390
+ : "generic head/tail fallback; no structured signal reducer matched",
165
391
  }
166
- const rows = uniqueSignalLines(source)
167
- return { kind: rows.length ? "signal" : "generic", rows }
392
+ }
393
+
394
+ function legacyStrategyKind(source, options = {}) {
395
+ const hint = hintFor(options)
396
+ if (/(jest|vitest|pytest|test|spec)/.test(hint) && npmTestRows(source).length) return "test"
397
+ if (/(eslint|lint|ruff|flake8|pylint|clippy)/.test(hint) && eslintRows(source).length) return "lint"
398
+ if (/(typecheck|tsc|compile|build|gradle|maven|cargo check|dotnet build)/.test(hint) && genericBuildRows(source).length) return "build"
399
+ if (/(git|diff|status)/.test(hint) && gitDiffRows(source).length) return "git"
400
+ if ((/(json|--json)/.test(hint) || /^[\s]*[\[{]/.test(String(source || ""))) && jsonRows(source).length) return "json"
401
+ return uniqueSignalLines(source).length ? "signal" : "generic"
168
402
  }
169
403
 
170
404
  function boundedPreview(text, maxChars, options = {}) {
171
405
  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)
406
+ const headBudget = Math.max(1536, Math.floor(maxChars * 0.18))
407
+ const tailBudget = Math.max(2048, Math.floor(maxChars * 0.24))
408
+ const signalBudget = Math.max(2048, maxChars - headBudget - tailBudget - 1900)
175
409
  const head = source.slice(0, headBudget)
176
410
  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 }
411
+ const reduced = reduceCommandOutput(source, options)
412
+ let signalText = [reduced.summary, ...reduced.rows].join("\n")
413
+ if (signalText.length > signalBudget) {
414
+ signalText = signalText.slice(0, signalBudget) + "\n...[reducer output truncated]"
415
+ }
416
+ return { head, tail, signalText, reducer: reduced }
181
417
  }
182
418
 
183
419
  export async function compactReversibleOutput(root, text, options = {}) {
@@ -185,9 +421,10 @@ export async function compactReversibleOutput(root, text, options = {}) {
185
421
  const maxChars = clampInt(options.maxChars, DEFAULT_LIMIT, 8 * 1024, 256 * 1024)
186
422
  if (source.length <= maxChars) {
187
423
  return {
188
- schemaVersion: 1,
424
+ schemaVersion: 2,
189
425
  compacted: false,
190
426
  strategy: "raw",
427
+ commandFamily: null,
191
428
  originalChars: source.length,
192
429
  returnedChars: source.length,
193
430
  evidenceRef: null,
@@ -195,35 +432,45 @@ export async function compactReversibleOutput(root, text, options = {}) {
195
432
  }
196
433
  }
197
434
 
435
+ // Raw bytes are persisted before any model-visible reduction. Reducers never
436
+ // become the verification source of truth; verifiers can recover exact data.
198
437
  const evidence = await putEvidence(root, source, {
199
438
  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)`,
439
+ source: options.source || options.command || "ues-performance-fabric-v2",
440
+ summary: options.summary || `Raw output preserved before command-aware model-visible compaction (${source.length} chars)`,
202
441
  })
203
- const { head, tail, signalText, signalKind } = boundedPreview(source, maxChars, options)
442
+ const { head, tail, signalText, reducer } = boundedPreview(source, maxChars, options)
204
443
  const recovery = `Raw captured output: ${evidence.ref}. Recover exact captured bytes in slices with: ues store get ${evidence.ref} --start N --max 24000`
205
444
  let preview = [
206
- `[UES reversible output compaction: ${source.length} -> <=${maxChars} chars; reducer=${signalKind}]`,
445
+ `[UES command-aware reversible compaction v2: ${source.length} -> <=${maxChars} chars; reducer=${reducer.family}]`,
207
446
  recovery,
208
447
  "",
209
- "--- head ---",
448
+ "--- reducer summary ---",
449
+ signalText,
450
+ "",
451
+ "--- bounded head ---",
210
452
  head,
211
- signalText ? "\n--- high-signal lines ---\n" + signalText : "",
212
- "\n--- tail ---",
453
+ "",
454
+ "--- bounded tail ---",
213
455
  tail,
214
456
  ].filter(Boolean).join("\n")
215
457
  if (preview.length > maxChars) {
216
- const reserve = Math.min(1400, recovery.length + 160)
458
+ const reserve = Math.min(1500, recovery.length + 200)
217
459
  preview = preview.slice(0, Math.max(0, maxChars - reserve)) + `\n...[model-visible preview truncated]\n${recovery}`
218
460
  }
219
461
 
462
+ const compatibilityKind = legacyStrategyKind(source, options)
220
463
  return {
221
- schemaVersion: 1,
464
+ schemaVersion: 2,
222
465
  compacted: true,
223
- strategy: `reversible-head-${signalKind}-tail`,
466
+ // Keep the V14 public strategy contract stable for callers/tests while
467
+ // exposing the richer V2 reducer identity separately.
468
+ strategy: `reversible-head-${compatibilityKind}-tail`,
469
+ strategyV2: `reversible-command-aware-${reducer.family}`,
470
+ commandFamily: reducer.family,
224
471
  originalChars: source.length,
225
472
  returnedChars: preview.length,
226
473
  evidenceRef: evidence.ref,
227
474
  text: preview,
228
475
  }
229
- }
476
+ }