@shanepadgett/tau-agent 0.33.0 → 0.34.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 (190) hide show
  1. package/docs/context.md +29 -20
  2. package/docs/extending-tau-agent.md +63 -24
  3. package/extensions/appshot/index.ts +112 -111
  4. package/extensions/aside/README.md +7 -0
  5. package/extensions/aside/index.ts +168 -0
  6. package/extensions/aside/panel.ts +83 -0
  7. package/extensions/branch/index.ts +38 -21
  8. package/extensions/cache-diagnostics/index.ts +70 -58
  9. package/extensions/checkpoint/README.md +9 -0
  10. package/extensions/checkpoint/checkpoint-budget.ts +79 -0
  11. package/extensions/checkpoint/checkpoint.ts +277 -0
  12. package/extensions/checkpoint/index.ts +102 -0
  13. package/extensions/checkpoint/messages.ts +169 -0
  14. package/extensions/checkpoint/prompt.ts +24 -0
  15. package/extensions/checkpoint/settings.ts +27 -0
  16. package/extensions/commit/commit-effort.ts +3 -3
  17. package/extensions/context/README.md +12 -8
  18. package/extensions/context/definitions.ts +178 -55
  19. package/extensions/context/index.ts +192 -160
  20. package/extensions/context/panel.ts +57 -37
  21. package/extensions/context/sync.ts +139 -89
  22. package/extensions/context/validation.ts +37 -25
  23. package/extensions/effort/README.md +1 -1
  24. package/extensions/effort/index.ts +3 -3
  25. package/extensions/effort/state.ts +3 -18
  26. package/extensions/explore/guidance.ts +6 -47
  27. package/extensions/explore/index.ts +38 -53
  28. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  29. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  30. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  31. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  32. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  33. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  34. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  35. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  36. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  37. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  38. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  39. package/extensions/explore/tools/target-params.ts +9 -0
  40. package/extensions/footer/index.ts +75 -56
  41. package/extensions/handoff/index.ts +8 -30
  42. package/extensions/image-gen/index.ts +46 -40
  43. package/extensions/patch/executor.ts +190 -135
  44. package/extensions/patch/matcher.ts +82 -48
  45. package/extensions/patch/parser.ts +149 -105
  46. package/extensions/patch/render.ts +32 -39
  47. package/extensions/patch/summary.ts +30 -26
  48. package/extensions/ready/README.md +7 -0
  49. package/extensions/ready/index.ts +58 -0
  50. package/extensions/ready/model.ts +128 -0
  51. package/extensions/ready/packs.ts +272 -0
  52. package/extensions/ready/render.ts +445 -0
  53. package/extensions/ready/scan.ts +760 -0
  54. package/extensions/reference/panel.ts +28 -16
  55. package/extensions/review/index.ts +63 -51
  56. package/extensions/review/panel.ts +5 -34
  57. package/extensions/runtime-context/context.ts +30 -26
  58. package/extensions/script-runner/README.md +2 -2
  59. package/extensions/script-runner/index.ts +130 -93
  60. package/extensions/silent-command-runner/index.ts +38 -23
  61. package/extensions/subagent/agents/context-sync.md +185 -33
  62. package/extensions/subagent/agents/scout.md +2 -2
  63. package/extensions/subagent/agents.ts +137 -84
  64. package/extensions/subagent/cmux-dashboard.ts +39 -30
  65. package/extensions/subagent/index.ts +79 -59
  66. package/extensions/subagent/resume.ts +1 -1
  67. package/extensions/subagent/run.ts +304 -193
  68. package/extensions/subagent/runtime.ts +781 -343
  69. package/extensions/tau/index.ts +47 -38
  70. package/extensions/tau-help/help.md +14 -6
  71. package/extensions/tool-loader/README.md +4 -2
  72. package/extensions/tool-loader/index.ts +77 -52
  73. package/extensions/web/index.ts +6 -3
  74. package/extensions/web/webfetch.ts +114 -86
  75. package/package.json +3 -3
  76. package/schemas/tau.schema.json +20 -33
  77. package/shared/bounded-text-result.ts +1 -0
  78. package/shared/checkpoint-visibility.ts +9 -0
  79. package/shared/events.ts +28 -10
  80. package/shared/isolated-session.ts +71 -29
  81. package/shared/model-effort.ts +9 -19
  82. package/shared/model-fallback/index.ts +66 -41
  83. package/shared/settings/load.ts +45 -38
  84. package/shared/temporary-output-store.ts +16 -10
  85. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  86. package/{extensions/explore → src}/ast/engine.ts +54 -38
  87. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  88. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  89. package/src/ast/format/deps.ts +94 -0
  90. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  91. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  92. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  93. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  94. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  95. package/src/ast/format/targeted-result.ts +20 -0
  96. package/src/ast/grammars/c_sharp.wasm +0 -0
  97. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  98. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  99. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  100. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  101. package/{extensions/explore → src}/ast/ir.ts +6 -0
  102. package/src/ast/languages/csharp-file-deps.ts +90 -0
  103. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  104. package/src/ast/languages/dotted-index.ts +131 -0
  105. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  106. package/src/ast/languages/fixtures/sample.cs +53 -0
  107. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  108. package/src/ast/languages/fixtures/sample.go +37 -0
  109. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  110. package/src/ast/languages/fixtures/sample.java +40 -0
  111. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  112. package/src/ast/languages/fixtures/sample.kt +31 -0
  113. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  114. package/src/ast/languages/fixtures/sample.md +23 -0
  115. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  116. package/src/ast/languages/fixtures/sample.rs +54 -0
  117. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  118. package/src/ast/languages/fixtures/sample.swift +45 -0
  119. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  120. package/src/ast/languages/fixtures/sample.ts +45 -0
  121. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  122. package/src/ast/languages/fixtures/sample.tsx +36 -0
  123. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  124. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  125. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  126. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  127. package/src/ast/languages/jvm-file-deps.ts +135 -0
  128. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  129. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  130. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  131. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  132. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  133. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  134. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  135. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  136. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  137. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  138. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  139. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  140. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  141. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  142. package/src/ast/queries/impact.ts +291 -0
  143. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  144. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  145. package/{extensions/explore → src}/ast/query.ts +1 -1
  146. package/{extensions/explore → src}/ast/registry.ts +1 -2
  147. package/{extensions/explore → src}/ast/scan.ts +1 -1
  148. package/src/ast/session.ts +50 -0
  149. package/{extensions/explore → src}/ast/slice.ts +8 -0
  150. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  151. package/src/file-injection/index.ts +547 -0
  152. package/src/image-generation/client.ts +50 -31
  153. package/src/image-generation/index.ts +177 -134
  154. package/src/index.ts +2 -0
  155. package/src/tool-loading/index.ts +58 -0
  156. package/extensions/context/evidence.ts +0 -517
  157. package/extensions/context/projection.ts +0 -141
  158. package/extensions/context/state.ts +0 -30
  159. package/extensions/context/write-scope.ts +0 -109
  160. package/extensions/explore/ast/format/deps.ts +0 -86
  161. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  162. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  163. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  164. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  165. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  166. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  167. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  168. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  169. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  170. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  171. package/extensions/explore/ast/languages/odin.ts +0 -407
  172. package/extensions/explore/ast/queries/impact.ts +0 -250
  173. package/extensions/explore/outline-injection.ts +0 -151
  174. package/extensions/explore/read/autoread.ts +0 -118
  175. package/extensions/working-memory/README.md +0 -17
  176. package/extensions/working-memory/checkpoint.ts +0 -242
  177. package/extensions/working-memory/index.ts +0 -341
  178. package/extensions/working-memory/memory.ts +0 -235
  179. package/extensions/working-memory/render.ts +0 -185
  180. package/extensions/working-memory/settings.ts +0 -38
  181. package/extensions/working-memory/state.ts +0 -158
  182. package/shared/autoread.ts +0 -234
  183. package/shared/context-messages.ts +0 -19
  184. package/shared/outline-injection.ts +0 -56
  185. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  186. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  187. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  188. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  189. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  190. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -16,11 +16,12 @@ import {
16
16
  import { Text } from "@earendil-works/pi-tui";
17
17
  import { Type } from "typebox";
18
18
 
19
- type Language = "python" | "typescript";
19
+ type Language = "python3" | "node" | "deno";
20
20
 
21
21
  interface Runtimes {
22
- python: string | undefined;
23
- typescript: string | undefined;
22
+ python3: string | undefined;
23
+ node: string | undefined;
24
+ deno: string | undefined;
24
25
  }
25
26
 
26
27
  interface StoredScript {
@@ -32,26 +33,42 @@ const TIMEOUT_MS = 120_000;
32
33
  const MAX_STORED = 8;
33
34
 
34
35
  function detectRuntimes(): Runtimes {
35
- let python: string | undefined;
36
- for (const cmd of ["python3", "python"] as const) {
37
- try {
38
- execFileSync(cmd, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
39
- python = cmd;
40
- break;
41
- } catch {
42
- // runtime not installed
43
- }
36
+ let python3: string | undefined;
37
+ try {
38
+ execFileSync("python3", ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
39
+ python3 = "python3";
40
+ } catch {
41
+ // runtime not installed
44
42
  }
45
43
  const [major, minor] = process.versions.node.split(".").map(Number);
46
- let typescript: string | undefined;
44
+ let node: string | undefined;
47
45
  if (major > 22 || (major === 22 && minor >= 6)) {
48
- typescript = process.execPath;
46
+ node = process.execPath;
47
+ }
48
+ let deno: string | undefined;
49
+ try {
50
+ execFileSync("deno", ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
51
+ deno = "deno";
52
+ } catch {
53
+ // runtime not installed
49
54
  }
50
- return { python, typescript };
55
+ return { python3, node, deno };
51
56
  }
52
57
 
53
58
  function capitalize(lang: Language): string {
54
- return lang === "python" ? "Python" : "TypeScript";
59
+ if (lang === "python3") return "Python 3";
60
+ if (lang === "node") return "Node";
61
+ return "Deno";
62
+ }
63
+
64
+ function formatLangList(langs: readonly Language[]): string {
65
+ const names = langs.map(capitalize);
66
+ if (names.length <= 2) return names.join(" or ");
67
+ return `${names.slice(0, -1).join(", ")}, or ${names[names.length - 1]}`;
68
+ }
69
+
70
+ function languageLabel(lang: string): string {
71
+ return lang === "node" ? "node (Node.js)" : lang;
55
72
  }
56
73
 
57
74
  function newScriptId(): string {
@@ -68,9 +85,7 @@ function applyEdits(source: string, edits: ReadonlyArray<{ oldText: string; newT
68
85
  for (const edit of edits) {
69
86
  const idx = next.indexOf(edit.oldText);
70
87
  if (idx === -1) {
71
- throw new Error(
72
- "An edits oldText was not found in the script. Copy the exact text from the script you wrote.",
73
- );
88
+ throw new Error("edits oldText not found. Copy exact text from the script you wrote.");
74
89
  }
75
90
  next = next.slice(0, idx) + edit.newText + next.slice(idx + edit.oldText.length);
76
91
  }
@@ -93,14 +108,64 @@ function renderEditsPreview(edits: ReadonlyArray<{ oldText: string; newText: str
93
108
  .join("\n");
94
109
  }
95
110
 
111
+ function resolveScriptSource(
112
+ scripts: Map<string, StoredScript>,
113
+ language: Language,
114
+ params: {
115
+ script?: string;
116
+ scriptId?: string;
117
+ edits?: ReadonlyArray<{ oldText: string; newText: string }>;
118
+ },
119
+ ): { scriptId: string; source: string } {
120
+ const edits = params.edits;
121
+ if (edits && edits.length > 0) {
122
+ const scriptId = params.scriptId;
123
+ if (!scriptId) throw new Error("edits require scriptId from the failed run.");
124
+ const stored = scripts.get(scriptId);
125
+ if (!stored) throw new Error(`No stored script for scriptId ${scriptId}. Evicted; resend full script.`);
126
+ if (stored.language !== language) {
127
+ throw new Error(`Language mismatch: scriptId ${scriptId} is ${stored.language}, not ${language}.`);
128
+ }
129
+ return { scriptId, source: applyEdits(stored.source, edits) };
130
+ }
131
+ if (typeof params.script !== "string" || params.script.length === 0) {
132
+ throw new Error("Provide script, or edits + scriptId.");
133
+ }
134
+ return { scriptId: params.scriptId ?? newScriptId(), source: params.script };
135
+ }
136
+
137
+ function formatTruncatedTail(text: string): { body: string; note: string } {
138
+ const trunc = truncateTail(text, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
139
+ const note = trunc.truncated
140
+ ? `\n\n[output truncated: kept tail ${trunc.outputLines} / ${trunc.totalLines} lines]`
141
+ : "";
142
+ return { body: trunc.content, note };
143
+ }
144
+
145
+ function successScriptResult(stdout: string): { content: [{ type: "text"; text: string }]; details: undefined } {
146
+ const { body, note } = formatTruncatedTail(stdout.trim());
147
+ const out = body.trim();
148
+ return {
149
+ content: [{ type: "text", text: out ? `${out}${note}` : `(no output)${note}` }],
150
+ details: undefined,
151
+ };
152
+ }
153
+
154
+ function throwScriptFailure(scriptId: string, result: { stdout: string; stderr: string }): never {
155
+ const diag = result.stderr.trim() || result.stdout.trim();
156
+ const { body, note } = formatTruncatedTail(diag);
157
+ const detail = body ? `${body}${note}\n\n` : "";
158
+ throw new Error(`${detail}scriptId: ${scriptId}`);
159
+ }
160
+
96
161
  export default function scriptRunnerExtension(pi: ExtensionAPI): void {
97
162
  const runtimes = detectRuntimes();
98
- const detected = (["python", "typescript"] as const).filter(
99
- (lang): lang is Language => (lang === "python" ? runtimes.python : runtimes.typescript) !== undefined,
163
+ const detected = (["python3", "node", "deno"] as const).filter(
164
+ (lang): lang is Language => runtimes[lang] !== undefined,
100
165
  );
101
166
  if (detected.length === 0) return;
102
167
 
103
- const langPhrase = detected.map(capitalize).join(" or ");
168
+ const langPhrase = formatLangList(detected);
104
169
 
105
170
  const scripts = new Map<string, StoredScript>();
106
171
  let tempDir: string | undefined;
@@ -115,14 +180,13 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
115
180
  }
116
181
 
117
182
  function resolveCommand(language: Language): string {
118
- if (language === "python") {
119
- const cmd = runtimes.python;
120
- if (!cmd) throw new Error("Python is not available on this machine.");
121
- return cmd;
183
+ const cmd = runtimes[language];
184
+ if (cmd) return cmd;
185
+ if (language === "python3") throw new Error("Python 3 is not available on this machine.");
186
+ if (language === "node") {
187
+ throw new Error("Node unavailable (needs Node >= 22.6 with --experimental-strip-types).");
122
188
  }
123
- const cmd = runtimes.typescript;
124
- if (!cmd) throw new Error("TypeScript is not available (requires Node >= 22.6).");
125
- return cmd;
189
+ throw new Error("Deno is not available on this machine.");
126
190
  }
127
191
 
128
192
  async function ensureTempDir(): Promise<string> {
@@ -139,9 +203,14 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
139
203
  signal: AbortSignal | undefined,
140
204
  ): Promise<ExecResult> {
141
205
  const dir = await ensureTempDir();
142
- const file = join(dir, language === "python" ? "_run.py" : "_run.ts");
206
+ const file = join(dir, language === "python3" ? "_run.py" : "_run.ts");
143
207
  await writeFile(file, source, "utf8");
144
- const args = language === "python" ? [file] : ["--experimental-strip-types", file];
208
+ const args =
209
+ language === "python3"
210
+ ? [file]
211
+ : language === "node"
212
+ ? ["--experimental-strip-types", file]
213
+ : ["run", "-A", file];
145
214
  const result = await pi.exec(command, args, { cwd, signal, timeout: TIMEOUT_MS });
146
215
  return {
147
216
  ...result,
@@ -152,13 +221,13 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
152
221
 
153
222
  const paramsSchema = Type.Object(
154
223
  {
155
- language: StringEnum(detected, { description: "Execution language available on this machine." }),
224
+ language: StringEnum(detected, {
225
+ description: "Execution runtime.",
226
+ }),
156
227
  script: Type.Optional(
157
- Type.String({ description: "Full script source for a new run. Omit when retrying with edits." }),
158
- ),
159
- scriptId: Type.Optional(
160
- Type.String({ description: "scriptId returned by a failed run. Required when retrying with edits." }),
228
+ Type.String({ description: "Full source for a new run. Omit when retrying with edits." }),
161
229
  ),
230
+ scriptId: Type.Optional(Type.String({ description: "From a failed run. Required with edits." })),
162
231
  edits: Type.Optional(
163
232
  Type.Array(
164
233
  Type.Object({
@@ -166,8 +235,7 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
166
235
  newText: Type.String({ description: "Replacement text." }),
167
236
  }),
168
237
  {
169
- description:
170
- "Targeted oldText/newText patches applied to the stored script before rerun. Prefer this over resending the whole script.",
238
+ description: "Patches applied to the stored script before rerun.",
171
239
  },
172
240
  ),
173
241
  ),
@@ -175,15 +243,26 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
175
243
  { additionalProperties: false },
176
244
  );
177
245
 
246
+ const runtimeNotes = [
247
+ detected.includes("node")
248
+ ? "language=node: local Node.js via node --experimental-strip-types (not tsc/ts-node)."
249
+ : undefined,
250
+ detected.includes("deno") ? "language=deno: local Deno via deno run -A." : undefined,
251
+ ].filter((note): note is string => note !== undefined);
252
+
178
253
  const tool = defineTool<typeof paramsSchema, undefined>({
179
254
  name: "script_runner",
180
255
  label: "Script Runner",
181
- description: `Run a ${langPhrase} script in the project working directory and return its output. On a non-zero exit, returns a scriptId; retry by sending targeted {oldText,newText} edits against the script you already wrote (same language + scriptId) instead of resending the whole script. Available on this machine: ${langPhrase}.`,
182
- promptSnippet: `Run ${langPhrase} scripts; fix failures with targeted edits instead of rewriting the whole script.`,
256
+ description: [
257
+ `Run a ${langPhrase} script in the project cwd and return stdout.`,
258
+ ...runtimeNotes,
259
+ "On non-zero exit: result includes scriptId. Retry with same language + scriptId + edits [{oldText,newText}]; do not resend the full script unless the approach is wrong.",
260
+ `Available: ${langPhrase}.`,
261
+ ].join("\n"),
262
+ promptSnippet: `Run ${langPhrase} scripts; on failure retry with edits + scriptId.`,
183
263
  promptGuidelines: [
184
- `Prefer script_runner over bash for ${langPhrase} when computation, data handling, or bulk file work is cleaner than chaining built-in tools.`,
185
- `When script_runner fails, do not resend the full script. Call script_runner again with the same language, the returned scriptId, and an edits array of {oldText,newText} patches against the script you just wrote. Only resend a full script if the approach itself was wrong.`,
186
- `script_runner never exposes the script file path, and you already know the script you sent. Never try to read it back.`,
264
+ `Prefer script_runner over bash for ${langPhrase} when computation, data handling, or bulk file work is cleaner than chaining tools.`,
265
+ "script_runner never exposes the script path; you already have the source. Never try to read it back.",
187
266
  ],
188
267
  parameters: paramsSchema,
189
268
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
@@ -191,65 +270,23 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
191
270
  if (signal?.aborted) {
192
271
  return { content: [{ type: "text", text: "Cancelled." }], details: undefined };
193
272
  }
194
-
195
273
  const command = resolveCommand(language);
196
- const edits = params.edits;
197
- let scriptId: string | undefined = params.scriptId;
198
- let source: string;
199
-
200
- if (edits && edits.length > 0) {
201
- if (!scriptId) throw new Error("edits require the scriptId returned by the failed run.");
202
- const stored = scripts.get(scriptId);
203
- if (!stored) {
204
- throw new Error(
205
- `No stored script for scriptId ${scriptId}. It may have been evicted; resend the full script.`,
206
- );
207
- }
208
- if (stored.language !== language) {
209
- throw new Error(`Language mismatch: scriptId ${scriptId} is ${stored.language}, not ${language}.`);
210
- }
211
- source = applyEdits(stored.source, edits);
212
- } else {
213
- if (typeof params.script !== "string" || params.script.length === 0) {
214
- throw new Error("Provide a script for a new run, or edits + scriptId to retry.");
215
- }
216
- source = params.script;
217
- if (!scriptId) scriptId = newScriptId();
218
- }
219
-
274
+ const { scriptId, source } = resolveScriptSource(scripts, language, params);
220
275
  remember(scriptId, { language, source });
221
- await onUpdate?.({ content: [{ type: "text", text: `Running ${language}...` }], details: undefined });
222
-
276
+ await onUpdate?.({
277
+ content: [{ type: "text", text: `Running ${languageLabel(language)}...` }],
278
+ details: undefined,
279
+ });
223
280
  const result = await runScript(language, command, source, ctx.cwd, signal);
224
281
  if (result.code === 0 && !result.killed) {
225
282
  scripts.delete(scriptId);
226
- const trunc = truncateTail(result.stdout.trim(), {
227
- maxLines: DEFAULT_MAX_LINES,
228
- maxBytes: DEFAULT_MAX_BYTES,
229
- });
230
- const out = trunc.content.trim();
231
- const note = trunc.truncated
232
- ? `\n\n[output truncated: kept tail ${trunc.outputLines} / ${trunc.totalLines} lines]`
233
- : "";
234
- return {
235
- content: [{ type: "text", text: out ? `${out}${note}` : `(no output)${note}` }],
236
- details: undefined,
237
- };
283
+ return successScriptResult(result.stdout);
238
284
  }
239
-
240
- const diag = result.stderr.trim() || result.stdout.trim();
241
- const trunc = truncateTail(diag, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
242
- const note = trunc.truncated
243
- ? `\n\n[output truncated: kept tail ${trunc.outputLines} / ${trunc.totalLines} lines]`
244
- : "";
245
- const detail = trunc.content ? `${trunc.content}${note}\n\n` : "";
246
- throw new Error(
247
- `${detail}scriptId: ${scriptId}\nRetry with edits: [{oldText,newText}] using the same language and scriptId; do not resend the full script.`,
248
- );
285
+ throwScriptFailure(scriptId, result);
249
286
  },
250
287
  renderCall(args, theme, context) {
251
288
  const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
252
- const header = `${theme.fg("toolTitle", theme.bold("script_runner"))} ${theme.fg("muted", args.language)}`;
289
+ const header = `${theme.fg("toolTitle", theme.bold("script_runner"))} ${theme.fg("muted", languageLabel(args.language))}`;
253
290
  const edits = args.edits;
254
291
  const body =
255
292
  edits && edits.length > 0
@@ -146,25 +146,11 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
146
146
  attentionHoldId = undefined;
147
147
  });
148
148
 
149
- async function runChangedCommands(
150
- cwd: string,
151
- turnStart: number,
149
+ async function collectCommandFailures(
150
+ projectRoot: string,
151
+ changed: readonly CommandConfig[],
152
152
  notify: (message: string, type?: "info" | "warning" | "error") => void,
153
- ): Promise<void> {
154
- if (!settings.enabled || settings.commands.length === 0) return;
155
-
156
- const projectRoot = await resolveProjectRoot(cwd);
157
- const paths = await walkFiles(projectRoot);
158
- const changed = await scanChangedCommands(projectRoot, settings.commands, paths, turnPaths, turnStart);
159
- if (!sessionActive || changed.length === 0) return;
160
-
161
- notify(
162
- changed.length === 1
163
- ? `silent-command-runner: running ${changed[0]?.name ?? "command"}`
164
- : `silent-command-runner: running ${changed.length} commands`,
165
- "info",
166
- );
167
-
153
+ ): Promise<FailedCommandDetails[] | undefined> {
168
154
  const failures: FailedCommandDetails[] = [];
169
155
  const controller = new AbortController();
170
156
  abortController = controller;
@@ -175,32 +161,61 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
175
161
  try {
176
162
  result = await runCommand(pi, projectRoot, command, controller.signal, settings.maxOutputBytes);
177
163
  } catch (error: unknown) {
178
- if (controller.signal.aborted) return;
164
+ if (controller.signal.aborted) return undefined;
179
165
  throw error;
180
166
  }
181
- if (controller.signal.aborted) return;
167
+ if (controller.signal.aborted) return undefined;
182
168
  if (result.code !== 0 || result.killed) failures.push(result);
183
169
  }
184
170
  } finally {
185
171
  if (abortController === controller) abortController = undefined;
186
172
  }
187
- if (!sessionActive) return;
173
+ return failures;
174
+ }
188
175
 
176
+ function reportCommandResults(
177
+ changed: readonly CommandConfig[],
178
+ failures: readonly FailedCommandDetails[],
179
+ notify: (message: string, type?: "info" | "warning" | "error") => void,
180
+ ): void {
189
181
  const failedNames = new Set(failures.map((failure) => failure.name));
190
182
  const passed = changed.filter((command) => !failedNames.has(command.name));
191
183
  if (passed.length > 0) notify(`silent-command-runner: passed ${formatCommandNames(passed)}`, "info");
192
184
  if (failures.length === 0) return;
193
-
194
185
  pi.sendMessage<FailureDetails>(
195
186
  {
196
187
  customType: MESSAGE_TYPE,
197
188
  content: formatAgentMessage(failures),
198
189
  display: true,
199
- details: { failed: failures },
190
+ details: { failed: [...failures] },
200
191
  },
201
192
  { deliverAs: "followUp" },
202
193
  );
203
194
  }
195
+
196
+ async function runChangedCommands(
197
+ cwd: string,
198
+ turnStart: number,
199
+ notify: (message: string, type?: "info" | "warning" | "error") => void,
200
+ ): Promise<void> {
201
+ if (!settings.enabled || settings.commands.length === 0) return;
202
+
203
+ const projectRoot = await resolveProjectRoot(cwd);
204
+ const paths = await walkFiles(projectRoot);
205
+ const changed = await scanChangedCommands(projectRoot, settings.commands, paths, turnPaths, turnStart);
206
+ if (!sessionActive || changed.length === 0) return;
207
+
208
+ notify(
209
+ changed.length === 1
210
+ ? `silent-command-runner: running ${changed[0]?.name ?? "command"}`
211
+ : `silent-command-runner: running ${changed.length} commands`,
212
+ "info",
213
+ );
214
+
215
+ const failures = await collectCommandFailures(projectRoot, changed, notify);
216
+ if (failures === undefined || !sessionActive) return;
217
+ reportCommandResults(changed, failures, notify);
218
+ }
204
219
  }
205
220
 
206
221
  function normalizeSettings(value: typeof silentCommandRunnerSettings.defaults): Settings {