opencode-agent-skill 13.0.0-beta.1 → 14.2.0-beta.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 (95) hide show
  1. package/README.md +1557 -606
  2. package/bin/ocskill.mjs +172 -24
  3. package/docs/OPENCODE-COMPAT.md +34 -95
  4. package/docs/PI-COMPAT.md +188 -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/commands/run.md +27 -20
  12. package/global-config/plugins/ues-router/command-runtime.js +54 -0
  13. package/global-config/plugins/ues-router/index.js +30 -24
  14. package/global-config/plugins/ues-router/policy-runtime.js +7 -0
  15. package/global-config/plugins/ues-router/router.js +12 -2
  16. package/global-config/skills/ecommerce-engineering/SKILL.md +1 -1
  17. package/global-config/skills/file-upload-engineering/SKILL.md +1 -1
  18. package/global-config/skills/git-safety/SKILL.md +1 -1
  19. package/global-config/skills/nestjs-engineering/SKILL.md +1 -1
  20. package/global-config/skills/performance-engineering/SKILL.md +1 -1
  21. package/global-config/skills/react-native-engineering/SKILL.md +1 -1
  22. package/global-config/skills/rest-api-design/SKILL.md +1 -1
  23. package/global-config/skills/ui-ux-engineering/SKILL.md +1 -1
  24. package/lib/adaptive-context-budget.mjs +97 -0
  25. package/lib/affected-tests.mjs +260 -0
  26. package/lib/benchmark-confidence.mjs +41 -2
  27. package/lib/browser-mcp-routing.mjs +166 -0
  28. package/lib/capability-fabric.mjs +336 -0
  29. package/lib/capability-registry.mjs +9 -0
  30. package/lib/context-engine-v11.mjs +65 -1
  31. package/lib/context-graph-rank.mjs +118 -0
  32. package/lib/context-manifest.mjs +97 -18
  33. package/lib/control-center.mjs +19 -1
  34. package/lib/dynamic-workflow.mjs +3 -1
  35. package/lib/evidence-store.mjs +82 -1
  36. package/lib/hierarchical-context.mjs +215 -0
  37. package/lib/memory-engine.mjs +465 -0
  38. package/lib/model-performance.mjs +33 -8
  39. package/lib/model-policy.mjs +3 -3
  40. package/lib/orchestrator-policy.mjs +5 -209
  41. package/lib/performance-fabric.mjs +229 -0
  42. package/lib/pi-rpc-pool.mjs +433 -0
  43. package/lib/process-hang-detector.mjs +83 -0
  44. package/lib/process-supervisor.mjs +193 -0
  45. package/lib/prompt-cache.mjs +2 -0
  46. package/lib/repo-graph.mjs +53 -2
  47. package/lib/runtime-config.mjs +31 -0
  48. package/lib/safety.mjs +132 -0
  49. package/lib/semantic-index.mjs +52 -3
  50. package/lib/skill-compiler.mjs +128 -0
  51. package/lib/skill-quality.mjs +48 -2
  52. package/lib/task-engine.mjs +66 -5
  53. package/lib/task-policy.mjs +235 -0
  54. package/lib/verification-broker.mjs +284 -0
  55. package/lib/verification-command.mjs +111 -0
  56. package/lib/windows-shim.mjs +35 -0
  57. package/lib/workspace-fingerprint.mjs +198 -0
  58. package/package.json +52 -42
  59. package/pi/extensions/ues-child-runtime.ts +238 -0
  60. package/pi/extensions/ues.ts +3200 -0
  61. package/pi/prompts/ues-audit.md +9 -0
  62. package/pi/prompts/ues-critique.md +9 -0
  63. package/pi/prompts/ues-debug.md +9 -0
  64. package/pi/prompts/ues-feature.md +9 -0
  65. package/pi/prompts/ues-fix.md +9 -0
  66. package/pi/prompts/ues-plan.md +9 -0
  67. package/pi/prompts/ues-research.md +9 -0
  68. package/pi/prompts/ues-resume.md +9 -0
  69. package/pi/prompts/ues-review.md +7 -0
  70. package/pi/prompts/ues-run.md +17 -0
  71. package/pi/prompts/ues-verify.md +9 -0
  72. package/scripts/check-release-consistency.mjs +119 -185
  73. package/scripts/check-runtime-exports.mjs +66 -0
  74. package/scripts/check-source-integrity.mjs +184 -0
  75. package/scripts/eval-pi.mjs +492 -0
  76. package/scripts/install.mjs +16 -0
  77. package/scripts/smoke-package-closure.mjs +110 -0
  78. package/scripts/smoke-packed-install.mjs +24 -11
  79. package/scripts/smoke-pi-extension.mjs +144 -0
  80. package/scripts/uninstall.mjs +44 -0
  81. package/CHANGELOG.md +0 -405
  82. package/docs/DETERMINISTIC-TOOLS.md +0 -105
  83. package/docs/ENGINEERING-DESIGN.md +0 -194
  84. package/docs/EVALS.md +0 -158
  85. package/docs/GITHUB-RULESET.md +0 -50
  86. package/docs/NPM-PUBLISH.md +0 -116
  87. package/docs/RESEARCH-SOURCES.md +0 -37
  88. package/docs/TRACE-SCHEMA.md +0 -122
  89. package/docs/V11-PERCEPTION-ADAPTIVE-EXECUTION.md +0 -75
  90. package/docs/V11-PERCEPTION-ADAPTIVE.md +0 -220
  91. package/docs/V12-WEAK-MODEL-INTELLIGENCE.md +0 -27
  92. package/docs/V13-PARALLEL-WEAK-MODEL-RUNTIME.md +0 -75
  93. package/docs/V7-INTELLIGENCE-RUNTIME.md +0 -166
  94. package/docs/V8-INTELLIGENCE-RELIABILITY.md +0 -206
  95. package/docs/V9-SPEED-INTELLIGENCE.md +0 -102
@@ -0,0 +1,198 @@
1
+ import { createHash } from "node:crypto"
2
+ import { lstatSync, readFileSync, readlinkSync } from "node:fs"
3
+ import { spawnSync } from "node:child_process"
4
+ import path from "node:path"
5
+
6
+ const RUNTIME_DIRS = [
7
+ ".ues-cache",
8
+ ".ues-traces",
9
+ ".ues-work",
10
+ ".ues-learning",
11
+ ".ues-dashboard",
12
+ ".ues-sandboxes",
13
+ ".ues-memory",
14
+ ".ues-evals",
15
+ ]
16
+ const RUNTIME_PATHSPECS = [
17
+ ".",
18
+ ...RUNTIME_DIRS.map((dir) => `:(exclude)${dir}/**`),
19
+ ]
20
+
21
+ let nonGitNonce = 0
22
+
23
+ function git(root, args) {
24
+ return spawnSync("git", args, {
25
+ cwd: root,
26
+ encoding: "utf8",
27
+ maxBuffer: 16 * 1024 * 1024,
28
+ })
29
+ }
30
+
31
+ function normalizePath(value) {
32
+ return String(value || "").replaceAll("\\", "/").replace(/^\.\//, "")
33
+ }
34
+
35
+ function porcelainEntries(statusOutput) {
36
+ const records = String(statusOutput || "").split("\0").filter(Boolean)
37
+ const entries = []
38
+ let renameCode = null
39
+
40
+ for (const record of records) {
41
+ if (record.length >= 3 && record[2] === " ") {
42
+ const code = record.slice(0, 2)
43
+ const file = normalizePath(record.slice(3))
44
+ if (file) entries.push({ code, file })
45
+ renameCode = /[RC]/.test(code) ? code : null
46
+ continue
47
+ }
48
+ if (renameCode) {
49
+ const file = normalizePath(record)
50
+ if (file) entries.push({ code: renameCode, file, renameSource: true })
51
+ renameCode = null
52
+ }
53
+ }
54
+ return entries
55
+ }
56
+
57
+ function porcelainChangedFiles(statusOutput) {
58
+ return [...new Set(porcelainEntries(statusOutput).map((entry) => entry.file))].sort()
59
+ }
60
+
61
+ function untrackedContentDigest(root, statusOutput, options = {}) {
62
+ const limit = Math.max(1024 * 1024, Number(options.maxUntrackedBytes || 32 * 1024 * 1024))
63
+ const totalLimit = Math.max(limit, Number(options.maxUntrackedTotalBytes || 64 * 1024 * 1024))
64
+ const files = porcelainEntries(statusOutput)
65
+ .filter((entry) => entry.code === "??")
66
+ .map((entry) => entry.file)
67
+ .sort()
68
+ const hash = createHash("sha256")
69
+ let totalBytes = 0
70
+
71
+ for (const relative of files) {
72
+ const full = path.resolve(root, relative)
73
+ if (full !== root && !full.startsWith(root + path.sep)) {
74
+ return { cacheable: false, reason: "untracked-path-escape", files }
75
+ }
76
+ try {
77
+ const info = lstatSync(full)
78
+ hash.update(relative)
79
+ hash.update("\0")
80
+ if (info.isSymbolicLink()) {
81
+ hash.update("symlink:")
82
+ hash.update(readlinkSync(full))
83
+ hash.update("\0")
84
+ continue
85
+ }
86
+ if (!info.isFile()) {
87
+ return { cacheable: false, reason: "untracked-non-file", files }
88
+ }
89
+ if (info.size > limit) {
90
+ return {
91
+ cacheable: false,
92
+ reason: "untracked-file-too-large",
93
+ files,
94
+ file: relative,
95
+ bytes: info.size,
96
+ }
97
+ }
98
+ totalBytes += info.size
99
+ if (totalBytes > totalLimit) {
100
+ return {
101
+ cacheable: false,
102
+ reason: "untracked-total-too-large",
103
+ files,
104
+ bytes: totalBytes,
105
+ }
106
+ }
107
+ hash.update(readFileSync(full))
108
+ hash.update("\0")
109
+ } catch {
110
+ return { cacheable: false, reason: "untracked-read-failed", files, file: relative }
111
+ }
112
+ }
113
+ return { cacheable: true, digest: hash.digest("hex"), files }
114
+ }
115
+
116
+ function nonGitSnapshot(root) {
117
+ nonGitNonce += 1
118
+ return {
119
+ schemaVersion: 1,
120
+ root,
121
+ git: false,
122
+ cacheable: false,
123
+ fingerprint: createHash("sha256")
124
+ .update(
125
+ "non-git-no-reuse:" +
126
+ root +
127
+ ":" +
128
+ process.pid +
129
+ ":" +
130
+ nonGitNonce,
131
+ )
132
+ .digest("hex"),
133
+ changedFiles: [],
134
+ head: null,
135
+ }
136
+ }
137
+
138
+ export function runtimeWorkspaceSnapshot(root = process.cwd()) {
139
+ root = path.resolve(root)
140
+ const inside = git(root, ["rev-parse", "--is-inside-work-tree"])
141
+ if (inside.status !== 0 || String(inside.stdout || "").trim() !== "true") {
142
+ return nonGitSnapshot(root)
143
+ }
144
+
145
+ const head = git(root, ["rev-parse", "HEAD"])
146
+ const status = git(root, [
147
+ "status",
148
+ "--porcelain=v1",
149
+ "-z",
150
+ "--untracked-files=all",
151
+ "--",
152
+ ...RUNTIME_PATHSPECS,
153
+ ])
154
+ const diff = git(root, ["diff", "--binary", "--no-ext-diff", "--", ...RUNTIME_PATHSPECS])
155
+ const cached = git(root, ["diff", "--cached", "--binary", "--no-ext-diff", "--", ...RUNTIME_PATHSPECS])
156
+
157
+ const commands = [head, status, diff, cached]
158
+ const commandComplete = commands.every((result) => result.status === 0)
159
+ const untracked = status.status === 0
160
+ ? untrackedContentDigest(root, status.stdout)
161
+ : { cacheable: false, reason: "status-unavailable", files: [] }
162
+ const cacheable = commandComplete && untracked.cacheable === true
163
+ if (!cacheable) {
164
+ // A partial Git snapshot or unhashable untracked file must never be reused
165
+ // as if it proved repository identity. Return a nonce fingerprint so
166
+ // callers fail closed while still exposing changed-file hints.
167
+ return {
168
+ ...nonGitSnapshot(root),
169
+ git: true,
170
+ head: head.status === 0 ? String(head.stdout || "").trim() || null : null,
171
+ changedFiles: status.status === 0 ? porcelainChangedFiles(status.stdout) : [],
172
+ reason: commandComplete ? untracked.reason : "git-snapshot-incomplete",
173
+ }
174
+ }
175
+
176
+ const parts = [
177
+ String(head.stdout || ""),
178
+ String(status.stdout || ""),
179
+ String(diff.stdout || ""),
180
+ String(cached.stdout || ""),
181
+ "untracked:" + String(untracked.digest || ""),
182
+ ]
183
+ return {
184
+ schemaVersion: 1,
185
+ root,
186
+ git: true,
187
+ cacheable: true,
188
+ fingerprint: createHash("sha256")
189
+ .update(parts.join("\n---UES-RUNTIME-FP---\n"))
190
+ .digest("hex"),
191
+ changedFiles: porcelainChangedFiles(status.stdout),
192
+ head: String(head.stdout || "").trim() || null,
193
+ }
194
+ }
195
+
196
+ export function runtimeWorkspaceFingerprint(root = process.cwd()) {
197
+ return runtimeWorkspaceSnapshot(root).fingerprint
198
+ }
package/package.json CHANGED
@@ -1,61 +1,50 @@
1
1
  {
2
2
  "name": "opencode-agent-skill",
3
- "version": "13.0.0-beta.1",
4
- "description": "Parallel weak-model engineering runtime with isolated same-model workers, event-driven DAG execution, transactional integration, adaptive context and evidence-gated verification for OpenCode",
3
+ "version": "14.2.0-beta.1",
4
+ "description": "Pi Agent engineering runtime with hierarchical context, verified persistent memory, learned capability failover, durable evidence, and evidence-gated verification",
5
5
  "type": "module",
6
6
  "bin": {
7
+ "ues": "bin/ocskill.mjs",
7
8
  "ocskill": "bin/ocskill.mjs"
8
9
  },
9
10
  "files": [
10
11
  "bin/ocskill.mjs",
11
12
  "lib/",
13
+ "global-config/AGENTS.md",
14
+ "global-config/skills/",
15
+ "global-config/agents/",
16
+ "global-config/commands/",
17
+ "global-config/plugins/",
18
+ "pi/",
12
19
  "scripts/",
13
- "global-config/",
14
20
  "evals/",
15
- "docs/",
21
+ "docs/V14-CONTEXT-MEMORY-FABRIC.md",
22
+ "docs/V14.1-QUALITY-PERFORMANCE-FABRIC.md",
23
+ "docs/V14.2-TURBO-WEAK-MODEL-RUNTIME.md",
24
+ "docs/PI-COMPAT.md",
25
+ "docs/OPENCODE-COMPAT.md",
16
26
  "README.md",
17
- "CHANGELOG.md",
18
27
  "LICENSE"
19
28
  ],
20
29
  "scripts": {
21
30
  "syntax": "node scripts/syntax-check.mjs",
22
- "validate": "node scripts/validate.mjs",
23
- "evals": "node scripts/eval-skills.mjs",
24
- "evals:live:validate": "node scripts/validate-live-suite.mjs",
25
- "test": "node --test test/*.test.mjs",
26
- "ci": "npm run syntax && npm run validate && npm run docs:check && npm run evals && npm run evals:router && npm run evals:v11:validate && npm run evals:v12:validate && npm run evals:v13 && npm run evals:repo-scale:validate && npm run evals:live:validate && npm run evals:long:validate && npm run evals:polyglot:validate && npm test && npm pack --dry-run && npm run smoke:pack && npm run smoke:plain-install",
27
- "postinstall": "node scripts/install.mjs",
28
- "preuninstall": "node scripts/uninstall.mjs",
29
- "prepublishOnly": "npm run ci",
30
- "evals:live": "node scripts/eval-live.mjs",
31
- "smoke:pack": "node scripts/smoke-packed-install.mjs",
32
- "inspect:stack": "node scripts/detect-stack.mjs",
33
- "inspect:tests": "node scripts/detect-test-commands.mjs",
34
- "inspect:repo": "node scripts/repo-map.mjs",
35
- "inspect:impact": "node scripts/impact-map.mjs",
36
- "inspect:evidence": "node scripts/collect-evidence.mjs",
37
- "inspect:git": "node scripts/check-working-tree.mjs",
38
- "evals:report": "node scripts/eval-report.mjs",
39
- "evals:router": "node scripts/eval-router.mjs",
40
- "evals:long:validate": "node scripts/validate-live-suite.mjs --suite long",
41
- "evals:long": "node scripts/eval-live.mjs --suite long",
42
- "dashboard": "node scripts/control-center.mjs",
43
- "evals:matrix": "node scripts/eval-matrix.mjs",
44
- "smoke:plain-install": "node scripts/smoke-plain-install.mjs",
31
+ "test": "node --test",
32
+ "test:pi": "node --test test/pi-package.test.mjs",
45
33
  "docs:check": "node scripts/check-release-consistency.mjs",
46
34
  "release:check-tag": "node scripts/check-release-tag.mjs",
47
- "evals:polyglot:validate": "node scripts/validate-live-suite.mjs --suite polyglot",
48
- "evals:polyglot": "node scripts/eval-live.mjs --suite polyglot",
49
- "evals:matrix:gate": "node scripts/eval-matrix.mjs --require-confidence",
50
- "evals:ablation": "node scripts/eval-ablation.mjs",
51
- "evals:v11": "node --test test/*-v11.test.mjs",
52
- "evals:v11:validate": "node scripts/validate-v11-suite.mjs",
53
- "evals:repo-scale:validate": "node scripts/validate-repo-scale-suite.mjs",
54
- "evals:v12:validate": "node scripts/validate-v12-foundation.mjs",
55
- "evals:v13": "node --test test/*-v13.test.mjs"
35
+ "smoke:pi": "node scripts/smoke-pi-extension.mjs",
36
+ "smoke:package": "node scripts/smoke-package-closure.mjs",
37
+ "eval:pi": "node scripts/eval-pi.mjs",
38
+ "ci": "npm run integrity && npm run runtime:exports && npm run syntax && npm test && npm run docs:check && npm pack --dry-run && npm run smoke:pi && npm run smoke:package && npm run smoke:packed",
39
+ "prepublishOnly": "npm run ci",
40
+ "smoke:packed": "node scripts/smoke-packed-install.mjs",
41
+ "eval:v14": "node --test test/capability-fabric-v14.test.mjs test/hierarchical-context-v14.test.mjs test/memory-engine-v14.test.mjs test/context-engine-v14.test.mjs test/task-engine-v14-context.test.mjs test/cli-v14.test.mjs test/v14-contract.test.mjs test/performance-fabric-v14.test.mjs test/browser-mcp-routing-v14.test.mjs test/process-hang-detector-v14.test.mjs test/v14.2-runtime.test.mjs test/benchmark-confidence.test.mjs",
42
+ "integrity": "node scripts/check-source-integrity.mjs",
43
+ "runtime:exports": "node scripts/check-runtime-exports.mjs"
56
44
  },
57
45
  "keywords": [
58
- "opencode",
46
+ "pi-package",
47
+ "pi-agent",
59
48
  "agent-skills",
60
49
  "coding-agent",
61
50
  "developer-tools",
@@ -63,12 +52,10 @@
63
52
  "code-review",
64
53
  "debugging",
65
54
  "verification",
66
- "visual-testing",
67
- "browser-automation",
68
55
  "multimodal-agents"
69
56
  ],
70
57
  "engines": {
71
- "node": ">=20"
58
+ "node": ">=22.19"
72
59
  },
73
60
  "publishConfig": {
74
61
  "access": "public"
@@ -81,5 +68,28 @@
81
68
  "bugs": {
82
69
  "url": "https://github.com/laivannha0202/opencode-agent-skill-/issues"
83
70
  },
84
- "license": "MIT"
71
+ "license": "MIT",
72
+ "pi": {
73
+ "extensions": [
74
+ "./pi/extensions/ues.ts"
75
+ ],
76
+ "skills": [
77
+ "./global-config/skills"
78
+ ],
79
+ "prompts": [
80
+ "./pi/prompts/*.md"
81
+ ]
82
+ },
83
+ "peerDependencies": {
84
+ "@earendil-works/pi-coding-agent": "*",
85
+ "typebox": "*"
86
+ },
87
+ "peerDependenciesMeta": {
88
+ "@earendil-works/pi-coding-agent": {
89
+ "optional": true
90
+ },
91
+ "typebox": {
92
+ "optional": true
93
+ }
94
+ }
85
95
  }
@@ -0,0 +1,238 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { compactReversibleOutput } from "../../lib/performance-fabric.mjs";
5
+ import { getEvidenceSelected } from "../../lib/evidence-store.mjs";
6
+ import { recordVerification } from "../../lib/verification-broker.mjs";
7
+ import { runtimeWorkspaceFingerprint } from "../../lib/workspace-fingerprint.mjs";
8
+ import { destructiveShellRisk } from "../../lib/safety.mjs";
9
+ import {
10
+ canonicalVerificationCommand,
11
+ looksLikeVerificationCommand,
12
+ } from "../../lib/verification-command.mjs";
13
+
14
+ const toolExecutionState = new Map<string, {
15
+ startedAt: number;
16
+ workspaceBefore?: string;
17
+ reusableCandidate: boolean;
18
+ canonicalVerification?: { command: string; args: string[]; raw: string } | null;
19
+ }>();
20
+
21
+ function configuredLimit() {
22
+ const raw = Number(process.env.UES_CHILD_TOOL_OUTPUT_LIMIT || 24 * 1024);
23
+ if (!Number.isFinite(raw)) return 24 * 1024;
24
+ return Math.max(4 * 1024, Math.min(128 * 1024, Math.trunc(raw)));
25
+ }
26
+
27
+ function configuredVerificationTimeout() {
28
+ const raw = Number(process.env.UES_CHILD_VERIFICATION_TIMEOUT_SEC || 300);
29
+ if (!Number.isFinite(raw) || raw <= 0) return 300;
30
+ return Math.max(30, Math.min(1800, Math.trunc(raw)));
31
+ }
32
+
33
+ function configuredRawCaptureLimit() {
34
+ const raw = Number(process.env.UES_CHILD_RAW_CAPTURE_LIMIT || 32 * 1024 * 1024);
35
+ if (!Number.isFinite(raw) || raw <= 0) return 32 * 1024 * 1024;
36
+ return Math.max(1024 * 1024, Math.min(128 * 1024 * 1024, Math.trunc(raw)));
37
+ }
38
+
39
+ function shellExitCode(event: any, rawText: string) {
40
+ if (!event.isError) return 0;
41
+ const match = String(rawText || "").match(/Command exited with code\s+(\d+)/i);
42
+ if (match) return Number(match[1]);
43
+ if (/timed out|timeout/i.test(rawText)) return 124;
44
+ if (/aborted/i.test(rawText)) return 130;
45
+ return 1;
46
+ }
47
+
48
+ function visibleText(event: any) {
49
+ return (event.content || [])
50
+ .filter((part: any) => part?.type === "text" && typeof part.text === "string")
51
+ .map((part: any) => part.text)
52
+ .join("\n");
53
+ }
54
+
55
+ async function capturedText(event: any, fallback: string) {
56
+ const fullOutputPath = String((event.details as any)?.fullOutputPath || "").trim();
57
+ if (!fullOutputPath) return { text: fallback, full: false, sourcePath: null };
58
+
59
+ try {
60
+ const info = await stat(fullOutputPath);
61
+ if (!info.isFile() || info.size <= 0 || info.size > configuredRawCaptureLimit()) {
62
+ return { text: fallback, full: false, sourcePath: fullOutputPath };
63
+ }
64
+ return {
65
+ text: await readFile(fullOutputPath, "utf8"),
66
+ full: true,
67
+ sourcePath: fullOutputPath,
68
+ };
69
+ } catch {
70
+ return { text: fallback, full: false, sourcePath: fullOutputPath };
71
+ }
72
+ }
73
+
74
+ export default function (pi: ExtensionAPI) {
75
+ const clearExecutionState = () => toolExecutionState.clear();
76
+ pi.on("session_start", clearExecutionState);
77
+ pi.on("session_shutdown", clearExecutionState);
78
+
79
+ pi.on("tool_call", async (event, ctx) => {
80
+ const toolName = String(event.toolName || "");
81
+ if (!["bash", "powershell"].includes(toolName)) return undefined;
82
+
83
+ const command = String((event.input as any)?.command || "");
84
+ const risk = destructiveShellRisk(command);
85
+ if (risk.risky) {
86
+ toolExecutionState.delete(String(event.toolCallId || ""));
87
+ return {
88
+ block: true,
89
+ reason:
90
+ `UES child safety blocked ${risk.id || "destructive"} shell operation` +
91
+ (risk.segment ? `: ${risk.segment}` : ""),
92
+ };
93
+ }
94
+
95
+ const canonicalVerification = canonicalVerificationCommand(command);
96
+ const reusableCandidate = Boolean(canonicalVerification);
97
+ const workspaceBefore = reusableCandidate
98
+ ? (() => {
99
+ try { return runtimeWorkspaceFingerprint(ctx.cwd); } catch { return undefined; }
100
+ })()
101
+ : undefined;
102
+ toolExecutionState.set(String(event.toolCallId || ""), {
103
+ startedAt: Date.now(),
104
+ workspaceBefore,
105
+ reusableCandidate,
106
+ canonicalVerification,
107
+ });
108
+ if (looksLikeVerificationCommand(command) && (event.input as any)?.timeout == null) {
109
+ (event.input as any).timeout = configuredVerificationTimeout();
110
+ }
111
+ return undefined;
112
+ });
113
+
114
+ pi.on("tool_result", async (event, ctx) => {
115
+ const toolName = String(event.toolName || "");
116
+ if (!["bash", "powershell", "grep", "find", "ls"].includes(toolName)) {
117
+ return undefined;
118
+ }
119
+
120
+ const shownText = visibleText(event);
121
+ const capture = await capturedText(event, shownText);
122
+ const rawText = capture.text;
123
+ const images = (event.content || []).filter((part: any) => part?.type !== "text");
124
+ const commandHint = String(
125
+ (event.input as any)?.command ||
126
+ (event.input as any)?.pattern ||
127
+ toolName ||
128
+ "tool",
129
+ );
130
+
131
+ const executionState = toolExecutionState.get(String(event.toolCallId || ""));
132
+ if (
133
+ ["bash", "powershell"].includes(toolName) &&
134
+ executionState?.reusableCandidate === true &&
135
+ executionState.workspaceBefore &&
136
+ !event.isError
137
+ ) {
138
+ const finishedAtMs = Date.now();
139
+ let workspaceAfter: string | undefined;
140
+ try { workspaceAfter = runtimeWorkspaceFingerprint(ctx.cwd); } catch {}
141
+ if (workspaceAfter && workspaceAfter === executionState.workspaceBefore) {
142
+ const canonical = executionState.canonicalVerification;
143
+ if (canonical) {
144
+ await recordVerification(ctx.cwd, {
145
+ command: canonical.command,
146
+ args: canonical.args,
147
+ exitCode: shellExitCode(event, shownText || rawText),
148
+ stdout: rawText,
149
+ stderr: "",
150
+ startedAt: new Date(executionState.startedAt).toISOString(),
151
+ finishedAt: new Date(finishedAtMs).toISOString(),
152
+ durationMs: Math.max(0, finishedAtMs - executionState.startedAt),
153
+ workspaceBefore: executionState.workspaceBefore,
154
+ workspaceAfter,
155
+ }).catch(() => null);
156
+ }
157
+ }
158
+ }
159
+ toolExecutionState.delete(String(event.toolCallId || ""));
160
+
161
+ if (String(process.env.UES_CHILD_TOOL_COMPACTION || "") !== "1") return undefined;
162
+ const maxChars = configuredLimit();
163
+ if (!rawText || rawText.length <= maxChars) return undefined;
164
+
165
+ const compacted = await compactReversibleOutput(ctx.cwd, rawText, {
166
+ maxChars,
167
+ kind: `child-${toolName}-output`,
168
+ source: commandHint,
169
+ summary: capture.full
170
+ ? `Full Pi shell output captured from ${capture.sourcePath} before model-visible compaction`
171
+ : "Captured Pi tool output preserved before model-visible compaction",
172
+ }).catch(() => null);
173
+ if (!compacted?.compacted) return undefined;
174
+
175
+ return {
176
+ content: [{ type: "text", text: compacted.text }, ...images],
177
+ details: {
178
+ ...(event.details && typeof event.details === "object" ? event.details : {}),
179
+ uesCompaction: {
180
+ strategy: compacted.strategy,
181
+ originalChars: compacted.originalChars,
182
+ returnedChars: compacted.returnedChars,
183
+ evidenceRef: compacted.evidenceRef,
184
+ rawCapture: capture.full ? "full-output-path" : "tool-result",
185
+ recoveryTool: "ues_evidence_get",
186
+ },
187
+ },
188
+ isError: event.isError,
189
+ usage: event.usage,
190
+ };
191
+ });
192
+
193
+ pi.registerTool({
194
+ name: "ues_evidence_get",
195
+ label: "UES Evidence Get",
196
+ description:
197
+ "Read an exact bounded slice or JSON selector from a UES Evidence Store reference when a compacted tool result says omitted raw evidence is available.",
198
+ parameters: Type.Object({
199
+ ref: Type.String({ minLength: 1 }),
200
+ start: Type.Optional(Type.Number({ minimum: 0 })),
201
+ maxBytes: Type.Optional(Type.Number({ minimum: 1, maximum: 64000 })),
202
+ }),
203
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
204
+ try {
205
+ const result = await getEvidenceSelected(ctx.cwd, params.ref, {
206
+ start: params.start || 0,
207
+ maxBytes: params.maxBytes || 16000,
208
+ });
209
+ return {
210
+ content: [{
211
+ type: "text",
212
+ text: [
213
+ `ref: ${result.ref}`,
214
+ `bytes: ${result.bytes}; start: ${result.start}; returned: ${result.returnedBytes}; truncated: ${result.truncated}`,
215
+ "",
216
+ result.content,
217
+ ].join("\n"),
218
+ }],
219
+ details: {
220
+ ref: result.ref,
221
+ start: result.start,
222
+ returnedBytes: result.returnedBytes,
223
+ truncated: result.truncated,
224
+ },
225
+ };
226
+ } catch (error) {
227
+ return {
228
+ content: [{
229
+ type: "text",
230
+ text: error instanceof Error ? error.message : String(error),
231
+ }],
232
+ details: { ref: params.ref },
233
+ isError: true,
234
+ };
235
+ }
236
+ },
237
+ });
238
+ }