@shanepadgett/tau-agent 0.33.1 → 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 +122 -82
  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 = "python3" | "typescript";
19
+ type Language = "python3" | "node" | "deno";
20
20
 
21
21
  interface Runtimes {
22
22
  python3: string | undefined;
23
- typescript: string | undefined;
23
+ node: string | undefined;
24
+ deno: string | undefined;
24
25
  }
25
26
 
26
27
  interface StoredScript {
@@ -40,15 +41,34 @@ function detectRuntimes(): Runtimes {
40
41
  // runtime not installed
41
42
  }
42
43
  const [major, minor] = process.versions.node.split(".").map(Number);
43
- let typescript: string | undefined;
44
+ let node: string | undefined;
44
45
  if (major > 22 || (major === 22 && minor >= 6)) {
45
- typescript = process.execPath;
46
+ node = process.execPath;
46
47
  }
47
- return { python3, typescript };
48
+ let deno: string | undefined;
49
+ try {
50
+ execFileSync("deno", ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
51
+ deno = "deno";
52
+ } catch {
53
+ // runtime not installed
54
+ }
55
+ return { python3, node, deno };
48
56
  }
49
57
 
50
58
  function capitalize(lang: Language): string {
51
- return lang === "python3" ? "Python 3" : "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;
52
72
  }
53
73
 
54
74
  function newScriptId(): string {
@@ -65,9 +85,7 @@ function applyEdits(source: string, edits: ReadonlyArray<{ oldText: string; newT
65
85
  for (const edit of edits) {
66
86
  const idx = next.indexOf(edit.oldText);
67
87
  if (idx === -1) {
68
- throw new Error(
69
- "An edits oldText was not found in the script. Copy the exact text from the script you wrote.",
70
- );
88
+ throw new Error("edits oldText not found. Copy exact text from the script you wrote.");
71
89
  }
72
90
  next = next.slice(0, idx) + edit.newText + next.slice(idx + edit.oldText.length);
73
91
  }
@@ -90,14 +108,64 @@ function renderEditsPreview(edits: ReadonlyArray<{ oldText: string; newText: str
90
108
  .join("\n");
91
109
  }
92
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
+
93
161
  export default function scriptRunnerExtension(pi: ExtensionAPI): void {
94
162
  const runtimes = detectRuntimes();
95
- const detected = (["python3", "typescript"] as const).filter(
96
- (lang): lang is Language => (lang === "python3" ? runtimes.python3 : runtimes.typescript) !== undefined,
163
+ const detected = (["python3", "node", "deno"] as const).filter(
164
+ (lang): lang is Language => runtimes[lang] !== undefined,
97
165
  );
98
166
  if (detected.length === 0) return;
99
167
 
100
- const langPhrase = detected.map(capitalize).join(" or ");
168
+ const langPhrase = formatLangList(detected);
101
169
 
102
170
  const scripts = new Map<string, StoredScript>();
103
171
  let tempDir: string | undefined;
@@ -112,14 +180,13 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
112
180
  }
113
181
 
114
182
  function resolveCommand(language: Language): string {
115
- if (language === "python3") {
116
- const cmd = runtimes.python3;
117
- if (!cmd) throw new Error("Python 3 is not available on this machine.");
118
- 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).");
119
188
  }
120
- const cmd = runtimes.typescript;
121
- if (!cmd) throw new Error("TypeScript is not available (requires Node >= 22.6).");
122
- return cmd;
189
+ throw new Error("Deno is not available on this machine.");
123
190
  }
124
191
 
125
192
  async function ensureTempDir(): Promise<string> {
@@ -138,7 +205,12 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
138
205
  const dir = await ensureTempDir();
139
206
  const file = join(dir, language === "python3" ? "_run.py" : "_run.ts");
140
207
  await writeFile(file, source, "utf8");
141
- const args = language === "python3" ? [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];
142
214
  const result = await pi.exec(command, args, { cwd, signal, timeout: TIMEOUT_MS });
143
215
  return {
144
216
  ...result,
@@ -149,13 +221,13 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
149
221
 
150
222
  const paramsSchema = Type.Object(
151
223
  {
152
- language: StringEnum(detected, { description: "Execution language available on this machine." }),
224
+ language: StringEnum(detected, {
225
+ description: "Execution runtime.",
226
+ }),
153
227
  script: Type.Optional(
154
- Type.String({ description: "Full script source for a new run. Omit when retrying with edits." }),
155
- ),
156
- scriptId: Type.Optional(
157
- 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." }),
158
229
  ),
230
+ scriptId: Type.Optional(Type.String({ description: "From a failed run. Required with edits." })),
159
231
  edits: Type.Optional(
160
232
  Type.Array(
161
233
  Type.Object({
@@ -163,8 +235,7 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
163
235
  newText: Type.String({ description: "Replacement text." }),
164
236
  }),
165
237
  {
166
- description:
167
- "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.",
168
239
  },
169
240
  ),
170
241
  ),
@@ -172,15 +243,26 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
172
243
  { additionalProperties: false },
173
244
  );
174
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
+
175
253
  const tool = defineTool<typeof paramsSchema, undefined>({
176
254
  name: "script_runner",
177
255
  label: "Script Runner",
178
- 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}.`,
179
- 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.`,
180
263
  promptGuidelines: [
181
- `Prefer script_runner over bash for ${langPhrase} when computation, data handling, or bulk file work is cleaner than chaining built-in tools.`,
182
- `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.`,
183
- `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.",
184
266
  ],
185
267
  parameters: paramsSchema,
186
268
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
@@ -188,65 +270,23 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
188
270
  if (signal?.aborted) {
189
271
  return { content: [{ type: "text", text: "Cancelled." }], details: undefined };
190
272
  }
191
-
192
273
  const command = resolveCommand(language);
193
- const edits = params.edits;
194
- let scriptId: string | undefined = params.scriptId;
195
- let source: string;
196
-
197
- if (edits && edits.length > 0) {
198
- if (!scriptId) throw new Error("edits require the scriptId returned by the failed run.");
199
- const stored = scripts.get(scriptId);
200
- if (!stored) {
201
- throw new Error(
202
- `No stored script for scriptId ${scriptId}. It may have been evicted; resend the full script.`,
203
- );
204
- }
205
- if (stored.language !== language) {
206
- throw new Error(`Language mismatch: scriptId ${scriptId} is ${stored.language}, not ${language}.`);
207
- }
208
- source = applyEdits(stored.source, edits);
209
- } else {
210
- if (typeof params.script !== "string" || params.script.length === 0) {
211
- throw new Error("Provide a script for a new run, or edits + scriptId to retry.");
212
- }
213
- source = params.script;
214
- if (!scriptId) scriptId = newScriptId();
215
- }
216
-
274
+ const { scriptId, source } = resolveScriptSource(scripts, language, params);
217
275
  remember(scriptId, { language, source });
218
- await onUpdate?.({ content: [{ type: "text", text: `Running ${language}...` }], details: undefined });
219
-
276
+ await onUpdate?.({
277
+ content: [{ type: "text", text: `Running ${languageLabel(language)}...` }],
278
+ details: undefined,
279
+ });
220
280
  const result = await runScript(language, command, source, ctx.cwd, signal);
221
281
  if (result.code === 0 && !result.killed) {
222
282
  scripts.delete(scriptId);
223
- const trunc = truncateTail(result.stdout.trim(), {
224
- maxLines: DEFAULT_MAX_LINES,
225
- maxBytes: DEFAULT_MAX_BYTES,
226
- });
227
- const out = trunc.content.trim();
228
- const note = trunc.truncated
229
- ? `\n\n[output truncated: kept tail ${trunc.outputLines} / ${trunc.totalLines} lines]`
230
- : "";
231
- return {
232
- content: [{ type: "text", text: out ? `${out}${note}` : `(no output)${note}` }],
233
- details: undefined,
234
- };
283
+ return successScriptResult(result.stdout);
235
284
  }
236
-
237
- const diag = result.stderr.trim() || result.stdout.trim();
238
- const trunc = truncateTail(diag, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
239
- const note = trunc.truncated
240
- ? `\n\n[output truncated: kept tail ${trunc.outputLines} / ${trunc.totalLines} lines]`
241
- : "";
242
- const detail = trunc.content ? `${trunc.content}${note}\n\n` : "";
243
- throw new Error(
244
- `${detail}scriptId: ${scriptId}\nRetry with edits: [{oldText,newText}] using the same language and scriptId; do not resend the full script.`,
245
- );
285
+ throwScriptFailure(scriptId, result);
246
286
  },
247
287
  renderCall(args, theme, context) {
248
288
  const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
249
- 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))}`;
250
290
  const edits = args.edits;
251
291
  const body =
252
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 {