opencode-agent-skill 13.0.0-beta.2 → 14.4.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.
Files changed (102) hide show
  1. package/README.md +1571 -607
  2. package/bin/ocskill.mjs +172 -24
  3. package/docs/OPENCODE-COMPAT.md +34 -97
  4. package/docs/PI-COMPAT.md +203 -0
  5. package/docs/V14-CONTEXT-MEMORY-FABRIC.md +70 -0
  6. package/docs/V14.1-QUALITY-PERFORMANCE-FABRIC.md +114 -0
  7. package/docs/V14.2-TURBO-WEAK-MODEL-RUNTIME.md +448 -0
  8. package/evals/v14/tasks.json +46 -0
  9. package/global-config/agents/executor.md +7 -0
  10. package/global-config/agents/visual-verifier.md +22 -3
  11. package/global-config/plugins/ues-router/index.js +13 -8
  12. package/global-config/plugins/ues-router/policy-runtime.js +7 -0
  13. package/global-config/plugins/ues-router/router.js +12 -2
  14. package/global-config/skills/ecommerce-engineering/SKILL.md +1 -1
  15. package/global-config/skills/file-upload-engineering/SKILL.md +1 -1
  16. package/global-config/skills/git-safety/SKILL.md +1 -1
  17. package/global-config/skills/nestjs-engineering/SKILL.md +1 -1
  18. package/global-config/skills/performance-engineering/SKILL.md +1 -1
  19. package/global-config/skills/react-native-engineering/SKILL.md +1 -1
  20. package/global-config/skills/rest-api-design/SKILL.md +1 -1
  21. package/global-config/skills/ui-ux-engineering/SKILL.md +1 -1
  22. package/lib/adaptive-context-budget.mjs +97 -0
  23. package/lib/affected-tests.mjs +260 -0
  24. package/lib/benchmark-confidence.mjs +41 -2
  25. package/lib/browser-mcp-routing.mjs +166 -0
  26. package/lib/capability-fabric.mjs +359 -0
  27. package/lib/capability-registry.mjs +9 -0
  28. package/lib/code-intelligence/edit-anchor.mjs +102 -0
  29. package/lib/code-intelligence/index.mjs +95 -0
  30. package/lib/code-intelligence/lsp-provider.mjs +189 -0
  31. package/lib/completion-auditor.mjs +82 -0
  32. package/lib/context-engine-v11.mjs +65 -1
  33. package/lib/context-graph-rank.mjs +118 -0
  34. package/lib/context-manifest.mjs +97 -18
  35. package/lib/control-center.mjs +19 -1
  36. package/lib/document-ingestion.mjs +60 -0
  37. package/lib/dynamic-workflow.mjs +3 -1
  38. package/lib/evidence-store.mjs +82 -1
  39. package/lib/fast-verification-gate.mjs +69 -0
  40. package/lib/hierarchical-context.mjs +215 -0
  41. package/lib/mcp-health.mjs +144 -0
  42. package/lib/mcp-tool-policy.mjs +19 -0
  43. package/lib/memory-engine.mjs +493 -0
  44. package/lib/model-performance.mjs +33 -8
  45. package/lib/model-policy.mjs +3 -3
  46. package/lib/orchestrator-policy.mjs +5 -209
  47. package/lib/performance-fabric.mjs +229 -0
  48. package/lib/pi-rpc-pool.mjs +433 -0
  49. package/lib/process-hang-detector.mjs +83 -0
  50. package/lib/process-supervisor.mjs +193 -0
  51. package/lib/prompt-cache.mjs +27 -11
  52. package/lib/repo-graph.mjs +53 -2
  53. package/lib/reversible-context.mjs +49 -0
  54. package/lib/runtime-config.mjs +31 -0
  55. package/lib/safety.mjs +132 -0
  56. package/lib/semantic-index.mjs +52 -3
  57. package/lib/skill-compiler.mjs +128 -0
  58. package/lib/skill-quality.mjs +48 -2
  59. package/lib/task-engine.mjs +62 -5
  60. package/lib/task-policy.mjs +291 -0
  61. package/lib/verification-broker.mjs +284 -0
  62. package/lib/verification-command.mjs +111 -0
  63. package/lib/windows-shim.mjs +35 -0
  64. package/lib/workspace-fingerprint.mjs +198 -0
  65. package/package.json +52 -42
  66. package/pi/extensions/ues-child-runtime.ts +393 -0
  67. package/pi/extensions/ues.ts +3327 -0
  68. package/pi/prompts/ues-audit.md +9 -0
  69. package/pi/prompts/ues-critique.md +9 -0
  70. package/pi/prompts/ues-debug.md +9 -0
  71. package/pi/prompts/ues-feature.md +9 -0
  72. package/pi/prompts/ues-fix.md +9 -0
  73. package/pi/prompts/ues-plan.md +9 -0
  74. package/pi/prompts/ues-research.md +9 -0
  75. package/pi/prompts/ues-resume.md +9 -0
  76. package/pi/prompts/ues-review.md +7 -0
  77. package/pi/prompts/ues-run.md +17 -0
  78. package/pi/prompts/ues-verify.md +9 -0
  79. package/scripts/check-release-consistency.mjs +119 -185
  80. package/scripts/check-runtime-exports.mjs +66 -0
  81. package/scripts/check-source-integrity.mjs +246 -0
  82. package/scripts/eval-pi.mjs +492 -0
  83. package/scripts/install.mjs +16 -0
  84. package/scripts/smoke-package-closure.mjs +110 -0
  85. package/scripts/smoke-packed-install.mjs +24 -11
  86. package/scripts/smoke-pi-extension.mjs +144 -0
  87. package/scripts/uninstall.mjs +44 -0
  88. package/CHANGELOG.md +0 -415
  89. package/docs/DETERMINISTIC-TOOLS.md +0 -105
  90. package/docs/ENGINEERING-DESIGN.md +0 -194
  91. package/docs/EVALS.md +0 -158
  92. package/docs/GITHUB-RULESET.md +0 -50
  93. package/docs/NPM-PUBLISH.md +0 -116
  94. package/docs/RESEARCH-SOURCES.md +0 -37
  95. package/docs/TRACE-SCHEMA.md +0 -122
  96. package/docs/V11-PERCEPTION-ADAPTIVE-EXECUTION.md +0 -75
  97. package/docs/V11-PERCEPTION-ADAPTIVE.md +0 -220
  98. package/docs/V12-WEAK-MODEL-INTELLIGENCE.md +0 -27
  99. package/docs/V13-PARALLEL-WEAK-MODEL-RUNTIME.md +0 -86
  100. package/docs/V7-INTELLIGENCE-RUNTIME.md +0 -166
  101. package/docs/V8-INTELLIGENCE-RELIABILITY.md +0 -206
  102. package/docs/V9-SPEED-INTELLIGENCE.md +0 -102
@@ -1,209 +1,5 @@
1
- const HIGH_RISK = /(\bauth\b|authorization|authentication|security|permission|payment|migration|schema|database|production|deploy|public api|breaking|secret|credential|phân quyền|bảo mật|thanh toán|cơ sở dữ liệu|triển khai|migrate|migration)/i
2
- const LONG = /(whole repo|whole repository|whole project|entire repo|entire project|large refactor|major refactor|long[- ](?:running|horizon)|durable state|dependency graph|integration verification|resume|migration|refactor all|toàn bộ repo|toàn bộ repository|toàn bộ dự án|toàn bộ project|refactor lớn|tác vụ dài|nhiều file|nhiều module|tiếp tục công việc|refactor toàn bộ|xác minh tích hợp|kiểm tra tích hợp|chia (?:công việc|task|tác vụ).*(?:dependency|phụ thuộc))/i
3
- const DEBUG = /(fix|bug|debug|crash|regression|failure|error|broken|sửa lỗi|lỗi|điều tra lỗi|không chạy)/i
4
- const CONTRACT = /(public api|api contract|openapi|response schema|request schema|breaking api|hợp đồng api|api công khai)/i
5
- const DATA = /(database|sql|migration|schema|transaction|index|cơ sở dữ liệu|dữ liệu|migrate)/i
6
-
7
- function signal(name, matched, weight) {
8
- return matched ? { name, weight } : null
9
- }
10
-
11
- function profileFor(mode, risk) {
12
- if (mode === "inline" && risk === "low") {
13
- return {
14
- name: "fast",
15
- maxSkills: 2,
16
- contextBudget: 8_000,
17
- contextStrategy: "incremental-semantic",
18
- skillLoading: "direct-only",
19
- durableState: false,
20
- worktree: "off",
21
- critic: "off",
22
- verification: "targeted",
23
- fullCI: false,
24
- containerVerification: "off",
25
- }
26
- }
27
- if (mode === "long-horizon" || risk === "high") {
28
- return {
29
- name: "deep",
30
- maxSkills: 5,
31
- contextBudget: 48_000,
32
- contextStrategy: "semantic+graph+git",
33
- skillLoading: "orchestrated",
34
- durableState: true,
35
- worktree: "auto-writers",
36
- critic: "required-for-high-risk-or-final",
37
- verification: "targeted+integration",
38
- fullCI: true,
39
- containerVerification: risk === "high" ? "preferred-if-capable" : "optional",
40
- }
41
- }
42
- return {
43
- name: "standard",
44
- maxSkills: 4,
45
- contextBudget: 20_000,
46
- contextStrategy: "incremental-semantic+git",
47
- skillLoading: "selective",
48
- durableState: false,
49
- worktree: "auto-on-conflict",
50
- critic: "on-failure-or-elevated-risk",
51
- verification: "targeted+affected",
52
- fullCI: false,
53
- containerVerification: "off",
54
- }
55
- }
56
-
57
- function boundedInt(value, fallback, min, max) {
58
- const parsed = Number(value)
59
- if (!Number.isFinite(parsed)) return fallback
60
- return Math.min(max, Math.max(min, Math.round(parsed)))
61
- }
62
-
63
- export function recoveryPolicyForAttempt(taskPolicy = {}, attempt = 1) {
64
- const normalizedAttempt = boundedInt(attempt, 1, 1, 99)
65
- const baseBudget = boundedInt(
66
- taskPolicy.contextBudget ?? taskPolicy.profile?.contextBudget,
67
- 20_000,
68
- 4_000,
69
- 48_000,
70
- )
71
- const baseSkills = boundedInt(
72
- taskPolicy.maxSkills ?? taskPolicy.profile?.maxSkills,
73
- 4,
74
- 1,
75
- 5,
76
- )
77
- const baseStrategy = taskPolicy.profile?.contextStrategy || "incremental-semantic+git"
78
-
79
- if (normalizedAttempt <= 1) {
80
- return {
81
- schemaVersion: 1,
82
- stage: "initial",
83
- attempt: normalizedAttempt,
84
- contextBudget: baseBudget,
85
- maxSkills: baseSkills,
86
- contextStrategy: baseStrategy,
87
- requireDiagnosis: false,
88
- requireCritic: false,
89
- modelEscalation: false,
90
- directives: [],
91
- }
92
- }
93
-
94
- if (normalizedAttempt === 2) {
95
- return {
96
- schemaVersion: 1,
97
- stage: "diagnose",
98
- attempt: normalizedAttempt,
99
- contextBudget: Math.min(48_000, Math.max(baseBudget, Math.round(baseBudget * 1.35))),
100
- maxSkills: Math.min(5, baseSkills + 1),
101
- contextStrategy:
102
- taskPolicy.executionProfile === "fast"
103
- ? "incremental-semantic+git"
104
- : baseStrategy,
105
- requireDiagnosis: true,
106
- requireCritic: false,
107
- modelEscalation: true,
108
- directives: [
109
- "reproduce or capture the exact previous failure before editing",
110
- "inspect the direct caller, nearest test and failure-adjacent evidence",
111
- "do not stack another speculative patch on top of the failed attempt",
112
- ],
113
- }
114
- }
115
-
116
- return {
117
- schemaVersion: 1,
118
- stage: "deep-recovery",
119
- attempt: normalizedAttempt,
120
- contextBudget: Math.min(48_000, Math.max(20_000, Math.round(baseBudget * 1.75))),
121
- maxSkills: Math.min(5, baseSkills + 2),
122
- contextStrategy: "semantic+graph+git",
123
- requireDiagnosis: true,
124
- requireCritic: true,
125
- modelEscalation: true,
126
- directives: [
127
- "re-investigate from fresh evidence and explicitly reject the failed hypothesis",
128
- "expand to callers, dependencies, tests and boundary contracts before editing",
129
- "challenge the architecture or coupling if repeated fixes expose a wider problem",
130
- "run an independent critic or review pass before accepting the recovery",
131
- ],
132
- }
133
- }
134
-
135
- export function classifyEngineeringTask(text, facts = {}) {
136
- const value = String(text || "")
137
- const declaredHighRisk =
138
- ["high", "critical"].includes(String(facts.risk || "").toLowerCase()) ||
139
- /\b(?:risk|rủi ro)\s*[:=\/-]?\s*(?:high|critical|cao|nghiêm trọng)\b/i.test(value) ||
140
- /\b(?:high|critical)[-\s]?(?:risk|rủi ro)\b/i.test(value)
141
- const declaredLongHorizon =
142
- facts.longHorizon === true ||
143
- ["long", "long-horizon", "deep"].includes(String(facts.mode || "").toLowerCase())
144
- const compoundLongRisk =
145
- /\blong\s*[/|,+]\s*(?:high|critical)[-\s]?risk\b/i.test(value) ||
146
- /\b(?:high|critical)[-\s]?risk\s*[/|,+]\s*long\b/i.test(value)
147
- const explicitLongHorizon = declaredLongHorizon || compoundLongRisk || LONG.test(value)
148
- const signals = [
149
- signal("long-request-text", value.length > 700, 1),
150
- signal("medium-request-text", value.length > 250, 1),
151
- signal("high-risk-domain", HIGH_RISK.test(value), 2),
152
- signal("declared-high-risk", declaredHighRisk, 2),
153
- signal("explicit-long-horizon", explicitLongHorizon, 2),
154
- signal("debugging", DEBUG.test(value), 1),
155
- signal("public-contract", CONTRACT.test(value) || facts.hasPublicContract, 2),
156
- signal("data-migration", (DATA.test(value) && /migration|schema|migrate|di trú|chuyển đổi/i.test(value)) || facts.hasMigration, 2),
157
- signal("many-changed-files", Number(facts.changedFiles || 0) > 5, 1),
158
- signal("very-many-changed-files", Number(facts.changedFiles || 0) > 12, 1),
159
- signal("large-repository", Number(facts.repoFiles || 0) > 1500, 1),
160
- signal("monorepo", facts.monorepo === true, 1),
161
- ].filter(Boolean)
162
-
163
- const score = signals.reduce((sum, item) => sum + item.weight, 0)
164
- const highRisk = declaredHighRisk || HIGH_RISK.test(value) || Boolean(facts.hasMigration) || Boolean(facts.hasPublicContract)
165
- const risk = highRisk ? "high" : score >= 3 ? "medium" : "low"
166
- const mode = explicitLongHorizon || score >= 4 ? "long-horizon" : score >= 2 ? "standard" : "inline"
167
- const modelTier = risk === "high" || mode === "long-horizon" ? "heavy" : score >= 2 ? "standard" : "light"
168
- const maxAttempts = risk === "high" ? 2 : 3
169
- const profile = profileFor(mode, risk)
170
-
171
- const domains = []
172
- if (/(auth|permission|tenant|token|session|phân quyền|xác thực)/i.test(value)) domains.push("auth-security")
173
- if (/(payment|checkout|refund|webhook|thanh toán|hoàn tiền)/i.test(value)) domains.push("payment")
174
- if (DATA.test(value)) domains.push("database")
175
- if (CONTRACT.test(value)) domains.push("api-contract")
176
- if (/(react native|expo|android|ios|gradle|xcode)/i.test(value)) domains.push("react-native")
177
- else if (/(next\.js|nextjs|app router|server component)/i.test(value)) domains.push("nextjs")
178
- else if (/\breact\b|useeffect|usestate|component/i.test(value)) domains.push("react")
179
- if (/(docker|kubernetes|terraform|github actions|ci\/cd|deploy|triển khai)/i.test(value)) domains.push("devops")
180
-
181
- return {
182
- schemaVersion: 4,
183
- score,
184
- signals,
185
- risk,
186
- mode,
187
- executionProfile: profile.name,
188
- modelTier,
189
- maxAttempts,
190
- contextBudget: profile.contextBudget,
191
- maxSkills: profile.maxSkills,
192
- requirePlanCheck: profile.durableState || risk === "high",
193
- requireIntegrationVerification: mode !== "inline" || risk === "high",
194
- requireFreshEvidence: true,
195
- profile,
196
- domains: [...new Set(domains)],
197
- recovery: {
198
- escalateAfterFailure: true,
199
- diagnosisBeforePatch: true,
200
- deepRecoveryFromAttempt: 3,
201
- },
202
- antiHallucination: {
203
- evidenceFirst: true,
204
- noCompletionWithoutVerification: mode !== "inline" || risk === "high",
205
- noUnsupportedSemanticClaims: true,
206
- failClosedOnMissingCapability: risk === "high",
207
- },
208
- }
209
- }
1
+ export {
2
+ classifyEngineeringTask,
3
+ recoveryPolicyForAttempt,
4
+ shouldRunDedicatedDiagnosis,
5
+ } from "./task-policy.mjs"
@@ -0,0 +1,229 @@
1
+ import { putEvidence } from "./evidence-store.mjs"
2
+
3
+ const DEFAULT_LIMIT = 64 * 1024
4
+ const SIGNAL_LINE = /\b(error|errors|failed|failure|fail|fatal|exception|warning|warn|assert|timeout|timed out|panic|traceback|mismatch|conflict|rejected|passed|pass|tests?|exit(?: code)?|changed)\b/i
5
+
6
+ function clampInt(value, fallback, min, max) {
7
+ const parsed = Number(value)
8
+ if (!Number.isFinite(parsed)) return fallback
9
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
10
+ }
11
+
12
+ function uniqueSignalLines(text, limit = 48) {
13
+ const seen = new Set()
14
+ 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)
20
+ if (rows.length >= limit) break
21
+ }
22
+ return rows
23
+ }
24
+
25
+ function testSummaryLines(source, limit = 80) {
26
+ const rows = []
27
+ const seen = new Set()
28
+ const patterns = [
29
+ /^FAIL\b/i,
30
+ /^PASS\b/i,
31
+ /^Test Suites:/i,
32
+ /^Tests:/i,
33
+ /^Snapshots:/i,
34
+ /^Time:/i,
35
+ /^Ran all test suites/i,
36
+ /Jest did not exit/i,
37
+ /Exceeded timeout/i,
38
+ /^\s*●\s+/,
39
+ /^\s*at\s+.+:\d+:\d+/,
40
+ /(?: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
50
+ }
51
+
52
+ function gitSummaryLines(source, limit = 120) {
53
+ const rows = []
54
+ for (const raw of String(source || "").split(/\r?\n/)) {
55
+ 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
+ }
66
+ }
67
+ return rows
68
+ }
69
+
70
+
71
+ function lintSummaryLines(source, limit = 100) {
72
+ const rows = []
73
+ const seen = new Set()
74
+ const patterns = [
75
+ /\b(?:error|warning)\b/i,
76
+ /\b\d+\s+problems?\b/i,
77
+ /\b\d+\s+errors?\b/i,
78
+ /\b\d+\s+warnings?\b/i,
79
+ /\b(?:eslint|ruff|flake8|pylint|clippy)\b/i,
80
+ /:\d+:\d+\b/,
81
+ /^✖\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
91
+ }
92
+
93
+ function buildSummaryLines(source, limit = 100) {
94
+ const rows = []
95
+ const seen = new Set()
96
+ const patterns = [
97
+ /\berror\s+TS\d+\b/i,
98
+ /\b(?:compile|compiled|compilation|build)\b.*\b(?:failed|success|succeeded|complete|completed)\b/i,
99
+ /\b(?:failed|fatal|exception|panic)\b/i,
100
+ /\bwarning\b/i,
101
+ /:\d+:\d+\b/,
102
+ /\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
112
+ }
113
+
114
+ function jsonSummaryLines(source, limit = 80) {
115
+ const text = String(source || "").trim()
116
+ if (!text || text.length > 4 * 1024 * 1024 || !/^[\[{]/.test(text)) return []
117
+ let value
118
+ try { value = JSON.parse(text) } catch { return [] }
119
+
120
+ const rows = []
121
+ const visit = (current, prefix, depth) => {
122
+ if (rows.length >= limit || depth > 3) return
123
+ if (Array.isArray(current)) {
124
+ rows.push(`${prefix || "$"}: array(${current.length})`)
125
+ current.slice(0, 8).forEach((item, index) => visit(item, `${prefix || "$"}[${index}]`, depth + 1))
126
+ return
127
+ }
128
+ if (current && typeof current === "object") {
129
+ const keys = Object.keys(current)
130
+ rows.push(`${prefix || "$"}: object(${keys.length}) keys=[${keys.slice(0, 16).join(", ")}]`)
131
+ for (const key of keys.slice(0, 12)) {
132
+ visit(current[key], prefix ? `${prefix}.${key}` : key, depth + 1)
133
+ if (rows.length >= limit) break
134
+ }
135
+ return
136
+ }
137
+ const rendered = JSON.stringify(current)
138
+ rows.push(`${prefix || "$"}: ${String(rendered).slice(0, 160)}`)
139
+ }
140
+ visit(value, "", 0)
141
+ return rows
142
+ }
143
+
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 }
157
+ }
158
+ if (/(git|diff|status)/.test(hint)) {
159
+ const rows = gitSummaryLines(source)
160
+ if (rows.length) return { kind: "git", rows }
161
+ }
162
+ if (/(json|--json)/.test(hint) || /^[\s]*[\[{]/.test(String(source || ""))) {
163
+ const rows = jsonSummaryLines(source)
164
+ if (rows.length) return { kind: "json", rows }
165
+ }
166
+ const rows = uniqueSignalLines(source)
167
+ return { kind: rows.length ? "signal" : "generic", rows }
168
+ }
169
+
170
+ function boundedPreview(text, maxChars, options = {}) {
171
+ 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)
175
+ const head = source.slice(0, headBudget)
176
+ 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 }
181
+ }
182
+
183
+ export async function compactReversibleOutput(root, text, options = {}) {
184
+ const source = String(text ?? "")
185
+ const maxChars = clampInt(options.maxChars, DEFAULT_LIMIT, 8 * 1024, 256 * 1024)
186
+ if (source.length <= maxChars) {
187
+ return {
188
+ schemaVersion: 1,
189
+ compacted: false,
190
+ strategy: "raw",
191
+ originalChars: source.length,
192
+ returnedChars: source.length,
193
+ evidenceRef: null,
194
+ text: source,
195
+ }
196
+ }
197
+
198
+ const evidence = await putEvidence(root, source, {
199
+ 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)`,
202
+ })
203
+ const { head, tail, signalText, signalKind } = boundedPreview(source, maxChars, options)
204
+ const recovery = `Raw captured output: ${evidence.ref}. Recover exact captured bytes in slices with: ues store get ${evidence.ref} --start N --max 24000`
205
+ let preview = [
206
+ `[UES reversible output compaction: ${source.length} -> <=${maxChars} chars; reducer=${signalKind}]`,
207
+ recovery,
208
+ "",
209
+ "--- head ---",
210
+ head,
211
+ signalText ? "\n--- high-signal lines ---\n" + signalText : "",
212
+ "\n--- tail ---",
213
+ tail,
214
+ ].filter(Boolean).join("\n")
215
+ if (preview.length > maxChars) {
216
+ const reserve = Math.min(1400, recovery.length + 160)
217
+ preview = preview.slice(0, Math.max(0, maxChars - reserve)) + `\n...[model-visible preview truncated]\n${recovery}`
218
+ }
219
+
220
+ return {
221
+ schemaVersion: 1,
222
+ compacted: true,
223
+ strategy: `reversible-head-${signalKind}-tail`,
224
+ originalChars: source.length,
225
+ returnedChars: preview.length,
226
+ evidenceRef: evidence.ref,
227
+ text: preview,
228
+ }
229
+ }