@shanepadgett/tau-agent 0.33.1 → 0.35.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 (203) 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/bash-approval/README.md +20 -0
  8. package/extensions/bash-approval/index.ts +235 -0
  9. package/extensions/bash-approval/settings.ts +24 -0
  10. package/extensions/branch/index.ts +38 -21
  11. package/extensions/cache-diagnostics/index.ts +70 -58
  12. package/extensions/checkpoint/README.md +9 -0
  13. package/extensions/checkpoint/checkpoint-budget.ts +66 -0
  14. package/extensions/checkpoint/checkpoint.ts +279 -0
  15. package/extensions/checkpoint/index.ts +102 -0
  16. package/extensions/checkpoint/messages.ts +170 -0
  17. package/extensions/checkpoint/prompt.ts +24 -0
  18. package/extensions/checkpoint/settings.ts +27 -0
  19. package/extensions/commit/commit-effort.ts +3 -3
  20. package/extensions/commit/commit-plan.ts +2 -2
  21. package/extensions/context/README.md +12 -8
  22. package/extensions/context/definitions.ts +178 -55
  23. package/extensions/context/index.ts +192 -160
  24. package/extensions/context/panel.ts +57 -37
  25. package/extensions/context/sync.ts +139 -89
  26. package/extensions/context/validation.ts +37 -25
  27. package/extensions/effort/README.md +1 -1
  28. package/extensions/effort/index.ts +3 -3
  29. package/extensions/effort/state.ts +3 -18
  30. package/extensions/explore/guidance.ts +6 -47
  31. package/extensions/explore/index.ts +38 -53
  32. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  33. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  34. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  35. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  36. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  37. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  38. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  39. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  40. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  41. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  42. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  43. package/extensions/explore/tools/target-params.ts +9 -0
  44. package/extensions/footer/index.ts +75 -56
  45. package/extensions/handoff/index.ts +8 -30
  46. package/extensions/image-gen/index.ts +46 -40
  47. package/extensions/patch/executor.ts +190 -135
  48. package/extensions/patch/matcher.ts +82 -48
  49. package/extensions/patch/parser.ts +149 -105
  50. package/extensions/patch/render.ts +32 -39
  51. package/extensions/patch/summary.ts +30 -26
  52. package/extensions/ready/README.md +7 -0
  53. package/extensions/ready/index.ts +58 -0
  54. package/extensions/ready/model.ts +128 -0
  55. package/extensions/ready/packs.ts +272 -0
  56. package/extensions/ready/render.ts +445 -0
  57. package/extensions/ready/scan.ts +760 -0
  58. package/extensions/reference/index.ts +2 -0
  59. package/extensions/reference/panel.ts +94 -31
  60. package/extensions/review/README.md +14 -6
  61. package/extensions/review/index.ts +58 -99
  62. package/extensions/review/model.ts +13 -33
  63. package/extensions/review/session.ts +9 -23
  64. package/extensions/runtime-context/context.ts +30 -26
  65. package/extensions/script-runner/README.md +2 -2
  66. package/extensions/script-runner/index.ts +122 -82
  67. package/extensions/silent-command-runner/index.ts +38 -23
  68. package/extensions/soul/README.md +2 -2
  69. package/extensions/soul/index.ts +4 -4
  70. package/extensions/soul/prompt.ts +6 -10
  71. package/extensions/soul/settings.ts +6 -3
  72. package/extensions/subagent/agents/context-sync.md +185 -33
  73. package/extensions/subagent/agents/scout.md +2 -2
  74. package/extensions/subagent/agents/web-research.md +2 -2
  75. package/extensions/subagent/agents.ts +137 -84
  76. package/extensions/subagent/cmux-dashboard.ts +39 -30
  77. package/extensions/subagent/index.ts +79 -59
  78. package/extensions/subagent/resume.ts +1 -1
  79. package/extensions/subagent/run.ts +308 -193
  80. package/extensions/subagent/runtime.ts +781 -343
  81. package/extensions/tau/index.ts +47 -38
  82. package/extensions/tau-help/help.md +20 -8
  83. package/extensions/tool-loader/README.md +4 -2
  84. package/extensions/tool-loader/index.ts +77 -52
  85. package/extensions/web/index.ts +6 -3
  86. package/extensions/web/webfetch.ts +114 -86
  87. package/package.json +3 -3
  88. package/schemas/tau.schema.json +38 -35
  89. package/shared/bounded-text-result.ts +1 -0
  90. package/shared/checkpoint-visibility.ts +9 -0
  91. package/shared/events.ts +28 -10
  92. package/shared/isolated-session.ts +72 -30
  93. package/shared/model-effort.ts +23 -22
  94. package/shared/model-fallback/index.ts +66 -41
  95. package/shared/settings/load.ts +45 -38
  96. package/shared/temporary-output-store.ts +16 -10
  97. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  98. package/{extensions/explore → src}/ast/engine.ts +54 -38
  99. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  100. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  101. package/src/ast/format/deps.ts +94 -0
  102. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  103. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  104. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  105. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  106. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  107. package/src/ast/format/targeted-result.ts +20 -0
  108. package/src/ast/grammars/c_sharp.wasm +0 -0
  109. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  110. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  111. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  112. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  113. package/{extensions/explore → src}/ast/ir.ts +6 -0
  114. package/src/ast/languages/csharp-file-deps.ts +90 -0
  115. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  116. package/src/ast/languages/dotted-index.ts +131 -0
  117. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  118. package/src/ast/languages/fixtures/sample.cs +53 -0
  119. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  120. package/src/ast/languages/fixtures/sample.go +37 -0
  121. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  122. package/src/ast/languages/fixtures/sample.java +40 -0
  123. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  124. package/src/ast/languages/fixtures/sample.kt +31 -0
  125. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  126. package/src/ast/languages/fixtures/sample.md +23 -0
  127. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  128. package/src/ast/languages/fixtures/sample.rs +54 -0
  129. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  130. package/src/ast/languages/fixtures/sample.swift +45 -0
  131. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  132. package/src/ast/languages/fixtures/sample.ts +45 -0
  133. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  134. package/src/ast/languages/fixtures/sample.tsx +36 -0
  135. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  136. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  137. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  138. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  139. package/src/ast/languages/jvm-file-deps.ts +135 -0
  140. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  141. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  142. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  143. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  144. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  145. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  146. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  147. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  148. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  149. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  150. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  151. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  152. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  153. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  154. package/src/ast/queries/impact.ts +291 -0
  155. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  156. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  157. package/{extensions/explore → src}/ast/query.ts +1 -1
  158. package/{extensions/explore → src}/ast/registry.ts +1 -2
  159. package/{extensions/explore → src}/ast/scan.ts +1 -1
  160. package/src/ast/session.ts +50 -0
  161. package/{extensions/explore → src}/ast/slice.ts +8 -0
  162. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  163. package/src/file-injection/index.ts +547 -0
  164. package/src/image-generation/client.ts +50 -31
  165. package/src/image-generation/index.ts +177 -134
  166. package/src/index.ts +2 -0
  167. package/src/tool-loading/index.ts +58 -0
  168. package/extensions/context/evidence.ts +0 -517
  169. package/extensions/context/projection.ts +0 -141
  170. package/extensions/context/state.ts +0 -30
  171. package/extensions/context/write-scope.ts +0 -109
  172. package/extensions/explore/ast/format/deps.ts +0 -86
  173. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  174. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  175. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  176. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  177. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  178. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  179. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  180. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  181. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  182. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  183. package/extensions/explore/ast/languages/odin.ts +0 -407
  184. package/extensions/explore/ast/queries/impact.ts +0 -250
  185. package/extensions/explore/outline-injection.ts +0 -151
  186. package/extensions/explore/read/autoread.ts +0 -118
  187. package/extensions/review/panel.ts +0 -128
  188. package/extensions/working-memory/README.md +0 -17
  189. package/extensions/working-memory/checkpoint.ts +0 -242
  190. package/extensions/working-memory/index.ts +0 -341
  191. package/extensions/working-memory/memory.ts +0 -235
  192. package/extensions/working-memory/render.ts +0 -185
  193. package/extensions/working-memory/settings.ts +0 -38
  194. package/extensions/working-memory/state.ts +0 -158
  195. package/shared/autoread.ts +0 -234
  196. package/shared/context-messages.ts +0 -19
  197. package/shared/outline-injection.ts +0 -56
  198. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  199. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  200. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  201. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  202. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  203. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -1,5 +1,4 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
2
  import { type Dirent, readdirSync } from "node:fs";
4
3
  import { join, resolve } from "node:path";
5
4
 
@@ -19,13 +18,6 @@ export function freezeRuntimeContext(cwd: string): RuntimeContext {
19
18
  return { cwd: cwd.replace(/\\/g, "/"), rootSnapshot: listRootSnapshot(cwd) };
20
19
  }
21
20
 
22
- export function formatLocalDateKey(date: Date): string {
23
- const year = date.getFullYear();
24
- const month = String(date.getMonth() + 1).padStart(2, "0");
25
- const day = String(date.getDate()).padStart(2, "0");
26
- return `${year}-${month}-${day}`;
27
- }
28
-
29
21
  export function formatLocalDisplayDate(date: Date): string {
30
22
  const months = [
31
23
  "January",
@@ -44,12 +36,6 @@ export function formatLocalDisplayDate(date: Date): string {
44
36
  return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`;
45
37
  }
46
38
 
47
- export function fingerprintRuntimeSnapshot(context: RuntimeContext): string {
48
- return createHash("sha256")
49
- .update(JSON.stringify({ version: 1, cwd: context.cwd, paths: context.rootSnapshot }))
50
- .digest("hex");
51
- }
52
-
53
39
  export function formatRuntimeContextMessage(displayDate: string, rootSnapshot: readonly string[] | undefined): string {
54
40
  const blocks = [`Current local date: ${displayDate}`];
55
41
  if (rootSnapshot?.length) {
@@ -58,42 +44,60 @@ export function formatRuntimeContextMessage(displayDate: string, rootSnapshot: r
58
44
  return blocks.join("\n");
59
45
  }
60
46
 
61
- function listRootSnapshot(cwd: string): string[] {
62
- const root = resolve(cwd);
47
+ function visibleRootEntries(root: string): SnapshotEntry[] {
63
48
  const rootEntries = listSnapshotEntries(root, ".");
64
49
  const ignoredRootPaths = gitIgnoredPaths(
65
50
  root,
66
51
  rootEntries.map((entry) => entry.path),
67
52
  );
68
- const visibleRootEntries = rootEntries.filter(
53
+ return rootEntries.filter(
69
54
  (entry) => !isAlwaysHiddenFromSnapshot(entry.dirent.name) && !ignoredRootPaths.has(entry.path),
70
55
  );
71
- const childEntriesByParent = new Map<string, SnapshotEntry[]>();
72
- const childPaths: string[] = [];
56
+ }
73
57
 
74
- for (const entry of visibleRootEntries) {
58
+ function childEntriesByParent(
59
+ root: string,
60
+ roots: readonly SnapshotEntry[],
61
+ ): {
62
+ byParent: Map<string, SnapshotEntry[]>;
63
+ ignoredChildPaths: Set<string>;
64
+ } {
65
+ const byParent = new Map<string, SnapshotEntry[]>();
66
+ const childPaths: string[] = [];
67
+ for (const entry of roots) {
75
68
  if (!entry.dirent.isDirectory()) continue;
76
69
  const childEntries = listSnapshotEntries(root, entry.path).filter(
77
70
  (childEntry) => !isAlwaysHiddenFromSnapshot(childEntry.dirent.name),
78
71
  );
79
- childEntriesByParent.set(entry.path, childEntries);
72
+ byParent.set(entry.path, childEntries);
80
73
  childPaths.push(...childEntries.map((childEntry) => childEntry.path));
81
74
  }
75
+ return { byParent, ignoredChildPaths: gitIgnoredPaths(root, childPaths) };
76
+ }
82
77
 
83
- const ignoredChildPaths = gitIgnoredPaths(root, childPaths);
78
+ function collectSnapshotPaths(
79
+ roots: readonly SnapshotEntry[],
80
+ byParent: Map<string, SnapshotEntry[]>,
81
+ ignoredChildPaths: ReadonlySet<string>,
82
+ ): string[] {
84
83
  const paths: string[] = [];
85
-
86
- for (const entry of visibleRootEntries) {
84
+ for (const entry of roots) {
87
85
  pushSnapshotPath(paths, entry);
88
86
  if (paths.length >= ROOT_SNAPSHOT_MAX_PATHS) break;
89
-
90
- for (const childEntry of childEntriesByParent.get(entry.path) ?? []) {
87
+ for (const childEntry of byParent.get(entry.path) ?? []) {
91
88
  if (ignoredChildPaths.has(childEntry.path)) continue;
92
89
  pushSnapshotPath(paths, childEntry);
93
90
  if (paths.length >= ROOT_SNAPSHOT_MAX_PATHS) break;
94
91
  }
95
92
  }
93
+ return paths;
94
+ }
96
95
 
96
+ function listRootSnapshot(cwd: string): string[] {
97
+ const root = resolve(cwd);
98
+ const roots = visibleRootEntries(root);
99
+ const children = childEntriesByParent(root, roots);
100
+ const paths = collectSnapshotPaths(roots, children.byParent, children.ignoredChildPaths);
97
101
  return paths.length === ROOT_SNAPSHOT_MAX_PATHS ? [...paths, "..."] : paths;
98
102
  }
99
103
 
@@ -1,7 +1,7 @@
1
1
  # Script Runner
2
2
 
3
- Gives the agent a first-class `script_runner` tool for running Python 3 and TypeScript instead of falling back to bash. The agent picks whichever language is more efficient for the task.
3
+ Gives the agent a first-class `script_runner` tool for running Python 3, Node.js, and Deno scripts instead of falling back to bash. The agent picks whichever runtime is more efficient for the task.
4
4
 
5
5
  When a run fails, the tool keeps the script and returns a `scriptId`. The agent retries with targeted `{oldText, newText}` edits against what it just wrote instead of resending the whole script, saving output tokens and keeping duplicate scripts out of context. Only the source the agent already sent is referenced; no file path is exposed.
6
6
 
7
- Languages are detected from the environment: Python 3 via `python3`, TypeScript via Node with `--experimental-strip-types` (Node 22.6 or newer). The tool registers only the languages actually available and is hidden from the prompt entirely when neither is present.
7
+ Runtimes are detected from the environment: Python 3 via `python3`, Node via the current process when Node is 22.6 or newer (`node --experimental-strip-types`), Deno via `deno` (`deno run -A`). The `node` language is the local Node.js runtime with full Node APIs; scripts may be TypeScript with erasable syntax or plain JavaScript. The `deno` language is the local Deno runtime with full permissions and native TypeScript/JavaScript. The tool registers only the runtimes actually available and is hidden from the prompt entirely when none are present.
@@ -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 {
@@ -3,7 +3,7 @@
3
3
  Soul shapes how Tau works and talks by adding two independent sections to Pi's native assistant prompt:
4
4
 
5
5
  - **ponytail** — a lazy-senior-dev build ethos: do the smallest correct thing, reuse before writing, YAGNI, fix bugs at the root, never cut validation, security, or accessibility.
6
- - **caveman** — a terse communication style: drop filler, keep technical substance exact, quote the shortest decisive line, spell out anything where brevity would risk safety.
6
+ - **simplified** — Simplified Technical English (ASD-STE100): use short sentences and paragraphs, explain jargon, and shape plans and conversations in small chunks.
7
7
 
8
8
  Pi continues to own tool guidance, project instructions, skills, documentation paths, custom prompts, and working-directory context.
9
9
 
@@ -12,7 +12,7 @@ Toggle each section in Tau settings (both on by default):
12
12
  ```json
13
13
  {
14
14
  "extensions": {
15
- "soul": { "ponytail": true, "caveman": false }
15
+ "soul": { "ponytail": true, "simplified": false }
16
16
  }
17
17
  }
18
18
  ```
@@ -1,22 +1,22 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
3
- import { CAVEMAN_STYLE, PONYTAIL_ETHOS } from "./prompt.ts";
3
+ import { PONYTAIL_ETHOS, SIMPLIFIED_TECHNICAL_ENGLISH } from "./prompt.ts";
4
4
  import soulSettings from "./settings.ts";
5
5
 
6
6
  export default function soulExtension(pi: ExtensionAPI): void {
7
7
  let ponytail = true;
8
- let caveman = true;
8
+ let simplified = true;
9
9
 
10
10
  pi.on("session_start", async (_event, ctx) => {
11
11
  const settings = await loadTauExtensionSettings(ctx, soulSettings);
12
12
  ponytail = settings.ponytail;
13
- caveman = settings.caveman;
13
+ simplified = settings.simplified;
14
14
  });
15
15
 
16
16
  pi.on("before_agent_start", (event) => {
17
17
  const sections: string[] = [];
18
18
  if (ponytail) sections.push(PONYTAIL_ETHOS);
19
- if (caveman) sections.push(CAVEMAN_STYLE);
19
+ if (simplified) sections.push(SIMPLIFIED_TECHNICAL_ENGLISH);
20
20
  if (sections.length === 0) return undefined;
21
21
  return { systemPrompt: [event.systemPrompt, ...sections].join("\n\n") };
22
22
  });
@@ -26,18 +26,14 @@ Non-trivial logic leaves one runnable check behind: the smallest thing that fail
26
26
 
27
27
  Mark a deliberate simplification that cuts a real corner with a named ceiling and its upgrade path.`;
28
28
 
29
- export const CAVEMAN_STYLE = `## Communication style
29
+ export const SIMPLIFIED_TECHNICAL_ENGLISH = `## Communication style
30
30
 
31
- Primary directive: simple, terse, human-like conversation. Talk to the human the way a person does, not the way a document reads. A wall of text does not get read. Outside a plan or a file you were asked to write, keep replies short. All technical substance stays. Only fluff dies.
31
+ Use Simplified Technical English (ASD-STE100) when you communicate with the user. Assume the user is tired and has limited capacity for jargon.
32
32
 
33
- Drop articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), and hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). No emoji, no decorative tables, no tool-call narration. Do not dump long raw logs. Quote the shortest decisive line.
33
+ Use short sentences and short paragraphs. Explain one idea at a time. Prefer common words, active voice, and concrete explanations. Avoid idioms, vague language, filler, and unexplained abbreviations. Explain unavoidable jargon in plain words when you first use it.
34
34
 
35
- Standard well-known acronyms OK (DB, API, HTTP). Never invent abbreviations (cfg, impl, req, fn). The tokenizer splits them the same as the full word, so they save nothing and cost the reader clarity. No causal arrows. Technical terms, code blocks, and error strings stay exact and verbatim.
35
+ Keep technical content exact. Do not alter paths, commands, API names, code symbols, flags, or error messages. Explain what they mean around the exact text.
36
36
 
37
- Preserve the user's language. Compress the style, not the language.
37
+ Work in small chunks. Answer the immediate question first. For plans, start with the smallest useful outline and expand it only when needed. Expect plans to change after each decision. Do not write a novel before the plan has been checked.
38
38
 
39
- No self-reference. Never name or announce the style.
40
-
41
- No filler emphasis or manufactured insight. Skip "here's the thing", "that's the part that really matters", "and that's the key". Skip "not just X, it's Y" and "it's not X, it's Y" framing. Skip grand closers that restate the point as a lesson. State the fact, the mechanism, or the next step, then stop.
42
-
43
- Full clear sentences, terse style dropped, for security warnings, irreversible-action confirmations, multi-step sequences where order matters, and anywhere compression would create ambiguity. Resume terse after.`;
39
+ Keep responses short enough to scan, while giving enough explanation for the user to understand the reason and next step. Do not replace explanations with fragments just to be brief. Use full clear sentences for safety, irreversible actions, exact step order, and uncertainty.`;
@@ -3,14 +3,17 @@ import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
3
3
 
4
4
  export default defineTauExtensionSettings({
5
5
  key: "soul",
6
- defaults: { ponytail: true as boolean, caveman: true as boolean },
6
+ defaults: { ponytail: true as boolean, simplified: true as boolean },
7
7
  schema: Type.Object(
8
8
  {
9
9
  ponytail: Type.Optional(
10
10
  Type.Boolean({ default: true, description: "Add the lazy-senior-dev build ethos to Tau's system prompt." }),
11
11
  ),
12
- caveman: Type.Optional(
13
- Type.Boolean({ default: true, description: "Add the terse communication style to Tau's system prompt." }),
12
+ simplified: Type.Optional(
13
+ Type.Boolean({
14
+ default: true,
15
+ description: "Add Simplified Technical English and small-chunk explanations to Tau's system prompt.",
16
+ }),
14
17
  ),
15
18
  },
16
19
  { additionalProperties: false },