@shanepadgett/tau-agent 0.44.0 → 0.45.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 (47) hide show
  1. package/docs/extending-tau-agent.md +3 -3
  2. package/extensions/appshot/index.ts +3 -0
  3. package/extensions/aside/index.ts +3 -11
  4. package/extensions/attention/README.md +2 -4
  5. package/extensions/attention/index.ts +2 -40
  6. package/extensions/auto-name/index.ts +5 -8
  7. package/extensions/cache-diagnostics/index.ts +1 -1
  8. package/extensions/codex-priority/README.md +7 -0
  9. package/extensions/codex-priority/index.ts +95 -0
  10. package/extensions/compaction/README.md +5 -0
  11. package/extensions/compaction/index.ts +47 -0
  12. package/extensions/cost-report/README.md +1 -1
  13. package/extensions/cost-report/analyze.ts +11 -60
  14. package/extensions/cost-report/html.ts +1 -37
  15. package/extensions/cost-report/types.ts +0 -9
  16. package/extensions/handoff/index.ts +1 -1
  17. package/extensions/image-gen/index.ts +1 -0
  18. package/extensions/review/README.md +1 -1
  19. package/extensions/run-summary/README.md +1 -1
  20. package/extensions/run-summary/index.ts +6 -24
  21. package/extensions/runtime-context/README.md +1 -1
  22. package/extensions/script-runner/README.md +3 -1
  23. package/extensions/script-runner/index.ts +41 -96
  24. package/extensions/silent-command-runner/index.ts +38 -69
  25. package/extensions/soul/README.md +3 -5
  26. package/extensions/soul/index.ts +59 -135
  27. package/extensions/soul/prompt.ts +2 -0
  28. package/extensions/tau-help/help.md +10 -2
  29. package/extensions/tool-approval/README.md +3 -3
  30. package/extensions/tool-approval/index.ts +86 -47
  31. package/extensions/tool-approval/panel.ts +16 -2
  32. package/extensions/tool-loader/README.md +5 -5
  33. package/extensions/tool-loader/index.ts +10 -222
  34. package/extensions/web/codesearch.ts +1 -0
  35. package/extensions/web/webfetch.ts +1 -0
  36. package/extensions/web/websearch.ts +1 -0
  37. package/package.json +2 -2
  38. package/shared/events.ts +7 -20
  39. package/shared/model-effort.ts +12 -8
  40. package/shared/model-fallback/index.ts +12 -29
  41. package/shared/model-fallback/types.ts +1 -5
  42. package/shared/prompt-contributions.ts +0 -2
  43. package/shared/script-source.ts +94 -0
  44. package/src/tool-loading/index.ts +5 -42
  45. package/extensions/soul/context.ts +0 -115
  46. package/extensions/soul/state.ts +0 -114
  47. package/extensions/soul/tools.ts +0 -31
@@ -14,6 +14,6 @@ Choose one focused review type:
14
14
  - `Architecture` reconsiders ownership, boundaries, reuse, and overall structure.
15
15
  - `Correctness` checks concrete runtime bugs and failure paths after accepting the architecture.
16
16
 
17
- Then choose which logged-in provider runs the review. OpenAI Codex uses `gpt-5.6-sol` and Anthropic uses `claude-opus-5`, both at high thinking. Only providers you are logged in to appear. With no logged-in provider, the review uses the current model.
17
+ Then choose which logged-in provider runs the review. Models come from Tau's shared `deep` effort tier: OpenAI Codex uses `gpt-6-astra` and Anthropic uses `claude-opus-5-5`, both at medium thinking. Only providers you are logged in to appear. With no logged-in provider, the review uses the current model.
18
18
 
19
19
  Tau writes each result as Markdown under `.pi/tau/reviews/`. Review results do not enter the parent agent context. Reference the Markdown file later when you want an agent to use it.
@@ -1,5 +1,5 @@
1
1
  # Run Summary
2
2
 
3
- Run Summary adds a compact marker after the agent settles with no automatic continuation pending. It shows wall time and model cost across the full continuation chain. Older markers also display the delegated cost recorded for those runs.
3
+ Run Summary adds a compact marker after the agent settles with no automatic continuation pending. It shows wall time and model cost across the full continuation chain.
4
4
 
5
5
  The marker is stored as a display-only session entry. It does not enter model context or trigger another agent turn.
@@ -1,4 +1,3 @@
1
- import type { AssistantMessage } from "@earendil-works/pi-ai";
2
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
2
  import { Marker } from "@shanepadgett/tau-tui";
4
3
 
@@ -9,11 +8,6 @@ interface RunSummary {
9
8
  runCost: number;
10
9
  }
11
10
 
12
- interface PreviousRunSummary extends RunSummary {
13
- subagentCost: number;
14
- totalCost: number;
15
- }
16
-
17
11
  export default function runSummaryExtension(pi: ExtensionAPI): void {
18
12
  let startedAt: number | undefined;
19
13
  let runCost = 0;
@@ -25,13 +19,7 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
25
19
  theme,
26
20
  state: "muted",
27
21
  label: "Run complete:",
28
- parts: [
29
- `Wall ${formatDuration(summary.wallMs)}`,
30
- `Run ${formatCost(summary.runCost)}`,
31
- ...("subagentCost" in summary
32
- ? [`Subagents ${formatCost(summary.subagentCost)}`, `Total ${formatCost(summary.totalCost)}`]
33
- : []),
34
- ],
22
+ parts: [`Wall ${formatDuration(summary.wallMs)}`, `Run ${formatCost(summary.runCost)}`],
35
23
  });
36
24
  });
37
25
 
@@ -46,7 +34,10 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
46
34
 
47
35
  pi.on("agent_end", (event) => {
48
36
  for (const message of event.messages) {
49
- if (message.role === "assistant") runCost += finiteNonNegative((message as AssistantMessage).usage.cost.total);
37
+ // Tool results carry the usage of model calls a tool made.
38
+ if (message.role === "assistant") runCost += finiteNonNegative(message.usage.cost.total);
39
+ else if (message.role === "toolResult" && message.usage)
40
+ runCost += finiteNonNegative(message.usage.cost.total);
50
41
  }
51
42
  });
52
43
 
@@ -62,19 +53,10 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
62
53
  });
63
54
  }
64
55
 
65
- function readRunSummary(value: unknown): RunSummary | PreviousRunSummary | undefined {
56
+ function readRunSummary(value: unknown): RunSummary | undefined {
66
57
  if (!value || typeof value !== "object") return undefined;
67
58
  const record = value as Record<string, unknown>;
68
59
  if (![record.wallMs, record.runCost].every(isFiniteNonNegative)) return undefined;
69
- if ("subagentCost" in record || "totalCost" in record) {
70
- if (![record.subagentCost, record.totalCost].every(isFiniteNonNegative)) return undefined;
71
- return {
72
- wallMs: record.wallMs as number,
73
- runCost: record.runCost as number,
74
- subagentCost: record.subagentCost as number,
75
- totalCost: record.totalCost as number,
76
- };
77
- }
78
60
  return {
79
61
  wallMs: record.wallMs as number,
80
62
  runCost: record.runCost as number,
@@ -1,5 +1,5 @@
1
1
  # Runtime Context
2
2
 
3
- Supplies Soul with the local date and root directory snapshot. Both are captured together when a prompt baseline is created and refreshed after successful compaction. Reload, resume, and midnight do not change an existing baseline.
3
+ Supplies Soul with the local date and root directory snapshot. Both are captured on the first prompt of a session and refreshed after successful compaction. Reload, resume, and midnight do not change them.
4
4
 
5
5
  After changing this extension, run `/reload` before testing the new behavior.
@@ -2,6 +2,8 @@
2
2
 
3
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
- 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.
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 script source file path is exposed.
6
+
7
+ Long output shows the tail and a path to the complete output in a temporary file for the active session.
6
8
 
7
9
  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.
@@ -4,17 +4,13 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { StringEnum } from "@earendil-works/pi-ai";
7
- import {
8
- DEFAULT_MAX_BYTES,
9
- DEFAULT_MAX_LINES,
10
- defineTool,
11
- type ExecResult,
12
- type ExtensionAPI,
13
- type Theme,
14
- truncateTail,
15
- } from "@earendil-works/pi-coding-agent";
7
+ import { defineTool, type ExecResult, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent";
16
8
  import { Text } from "@earendil-works/pi-tui";
17
9
  import { Type } from "typebox";
10
+ import { BoundedTextResultBuilder } from "../../shared/bounded-text-result.ts";
11
+ import { onTauEventImmediately } from "../../shared/events.ts";
12
+ import { createScriptSourceStore } from "../../shared/script-source.ts";
13
+ import { createTemporaryOutputStore } from "../../shared/temporary-output-store.ts";
18
14
  import { renderToolOutputPreview } from "../../shared/text.ts";
19
15
 
20
16
  type Language = "python3" | "node" | "deno";
@@ -25,13 +21,7 @@ interface Runtimes {
25
21
  deno: string | undefined;
26
22
  }
27
23
 
28
- interface StoredScript {
29
- language: Language;
30
- source: string;
31
- }
32
-
33
24
  const TIMEOUT_MS = 120_000;
34
- const MAX_STORED = 8;
35
25
 
36
26
  function detectRuntimes(): Runtimes {
37
27
  let python3: string | undefined;
@@ -81,18 +71,6 @@ function scrubPath(text: string, file: string, dir: string): string {
81
71
  return text.replaceAll(file, "<script>").replaceAll(dir, "<tmpdir>");
82
72
  }
83
73
 
84
- function applyEdits(source: string, edits: ReadonlyArray<{ oldText: string; newText: string }>): string {
85
- let next = source;
86
- for (const edit of edits) {
87
- const idx = next.indexOf(edit.oldText);
88
- if (idx === -1) {
89
- throw new Error("edits oldText not found. Copy exact text from the script you wrote.");
90
- }
91
- next = next.slice(0, idx) + edit.newText + next.slice(idx + edit.oldText.length);
92
- }
93
- return next;
94
- }
95
-
96
74
  function renderEditsPreview(edits: ReadonlyArray<{ oldText: string; newText: string }>, theme: Theme): string {
97
75
  return edits
98
76
  .map((edit) => {
@@ -109,56 +87,6 @@ function renderEditsPreview(edits: ReadonlyArray<{ oldText: string; newText: str
109
87
  .join("\n");
110
88
  }
111
89
 
112
- function resolveScriptSource(
113
- scripts: Map<string, StoredScript>,
114
- language: Language,
115
- params: {
116
- script?: string;
117
- scriptId?: string;
118
- edits?: ReadonlyArray<{ oldText: string; newText: string }>;
119
- },
120
- ): { scriptId: string; source: string } {
121
- const edits = params.edits;
122
- if (edits && edits.length > 0) {
123
- const scriptId = params.scriptId;
124
- if (!scriptId) throw new Error("edits require scriptId from the failed run.");
125
- const stored = scripts.get(scriptId);
126
- if (!stored) throw new Error(`No stored script for scriptId ${scriptId}. Evicted; resend full script.`);
127
- if (stored.language !== language) {
128
- throw new Error(`Language mismatch: scriptId ${scriptId} is ${stored.language}, not ${language}.`);
129
- }
130
- return { scriptId, source: applyEdits(stored.source, edits) };
131
- }
132
- if (typeof params.script !== "string" || params.script.length === 0) {
133
- throw new Error("Provide script, or edits + scriptId.");
134
- }
135
- return { scriptId: params.scriptId ?? newScriptId(), source: params.script };
136
- }
137
-
138
- function formatTruncatedTail(text: string): { body: string; note: string } {
139
- const trunc = truncateTail(text, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
140
- const note = trunc.truncated
141
- ? `\n\n[output truncated: kept tail ${trunc.outputLines} / ${trunc.totalLines} lines]`
142
- : "";
143
- return { body: trunc.content, note };
144
- }
145
-
146
- function successScriptResult(stdout: string): { content: [{ type: "text"; text: string }]; details: undefined } {
147
- const { body, note } = formatTruncatedTail(stdout.trim());
148
- const out = body.trim();
149
- return {
150
- content: [{ type: "text", text: out ? `${out}${note}` : `(no output)${note}` }],
151
- details: undefined,
152
- };
153
- }
154
-
155
- function throwScriptFailure(scriptId: string, result: { stdout: string; stderr: string }): never {
156
- const diag = result.stderr.trim() || result.stdout.trim();
157
- const { body, note } = formatTruncatedTail(diag);
158
- const detail = body ? `${body}${note}\n\n` : "";
159
- throw new Error(`${detail}scriptId: ${scriptId}`);
160
- }
161
-
162
90
  export default function scriptRunnerExtension(pi: ExtensionAPI): void {
163
91
  const runtimes = detectRuntimes();
164
92
  const detected = (["python3", "node", "deno"] as const).filter(
@@ -168,16 +96,15 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
168
96
 
169
97
  const langPhrase = formatLangList(detected);
170
98
 
171
- const scripts = new Map<string, StoredScript>();
172
-
173
- function remember(scriptId: string, script: StoredScript): void {
174
- scripts.set(scriptId, script);
175
- while (scripts.size > MAX_STORED) {
176
- const oldest = scripts.keys().next().value;
177
- if (oldest === undefined) break;
178
- scripts.delete(oldest);
179
- }
180
- }
99
+ const scriptStore = createScriptSourceStore();
100
+ const temporaryOutput = createTemporaryOutputStore();
101
+ onTauEventImmediately(pi, "script-runner.source-store", "tau:script-runner.source-store", ({ accept }) =>
102
+ accept(scriptStore),
103
+ );
104
+ pi.on("session_start", async () => {
105
+ await temporaryOutput.shutdown();
106
+ await temporaryOutput.start();
107
+ });
181
108
 
182
109
  function resolveCommand(language: Language): string {
183
110
  const cmd = runtimes[language];
@@ -265,24 +192,41 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
265
192
  "script_runner never exposes the script path; you already have the source. Never try to read it back.",
266
193
  ],
267
194
  parameters: paramsSchema,
268
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
195
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
269
196
  const language = params.language;
270
197
  if (signal?.aborted) {
271
198
  return { content: [{ type: "text", text: "Cancelled." }], details: undefined };
272
199
  }
200
+ scriptStore.verifyAndConsume(toolCallId, params);
273
201
  const command = resolveCommand(language);
274
- const { scriptId, source } = resolveScriptSource(scripts, language, params);
275
- remember(scriptId, { language, source });
202
+ const resolved = scriptStore.resolve(params);
203
+ const scriptId = resolved.scriptId ?? newScriptId();
204
+ const source = resolved.source;
205
+ scriptStore.remember(scriptId, language, source);
276
206
  await onUpdate?.({
277
207
  content: [{ type: "text", text: `Running ${languageLabel(language)}...` }],
278
208
  details: undefined,
279
209
  });
280
210
  const result = await runScript(language, command, source, ctx.cwd, signal);
281
- if (result.code === 0 && !result.killed) {
282
- scripts.delete(scriptId);
283
- return successScriptResult(result.stdout);
211
+ const succeeded = result.code === 0 && !result.killed;
212
+ const output = new BoundedTextResultBuilder(temporaryOutput, "tail");
213
+ let content: string;
214
+ try {
215
+ await output.append(succeeded ? result.stdout.trim() : result.stderr.trim() || result.stdout.trim());
216
+ if (signal?.aborted) {
217
+ await output.abort();
218
+ return { content: [{ type: "text", text: "Cancelled." }], details: undefined };
219
+ }
220
+ content = (await output.finish()).content;
221
+ } catch (error) {
222
+ await output.abort();
223
+ throw error;
224
+ }
225
+ if (succeeded) {
226
+ scriptStore.forget(scriptId);
227
+ return { content: [{ type: "text", text: content.trim() || "(no output)" }], details: undefined };
284
228
  }
285
- throwScriptFailure(scriptId, result);
229
+ throw new Error(`${content ? `${content}\n\n` : ""}scriptId: ${scriptId}`);
286
230
  },
287
231
  renderCall(args, theme, context) {
288
232
  const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
@@ -312,7 +256,8 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
312
256
 
313
257
  pi.registerTool(tool);
314
258
 
315
- pi.on("session_shutdown", () => {
316
- scripts.clear();
259
+ pi.on("session_shutdown", async () => {
260
+ scriptStore.clear();
261
+ await temporaryOutput.shutdown();
317
262
  });
318
263
  }
@@ -2,7 +2,6 @@ import { readdir, stat } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import { type ExecResult, type ExtensionAPI, keyText, type Theme } from "@earendil-works/pi-coding-agent";
4
4
  import { Box, Text } from "@earendil-works/pi-tui";
5
- import { emitTauEvent } from "../../shared/events.ts";
6
5
  import { registerPromptSource } from "../../shared/prompt-contributions.ts";
7
6
  import { matchGlob, posixPath } from "../../shared/glob.ts";
8
7
  import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
@@ -82,16 +81,6 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
82
81
  let turnPaths = new Set<string>();
83
82
  let abortController: AbortController | undefined;
84
83
  let sessionActive = false;
85
- let chainActive = false;
86
- let attentionHoldSequence = 0;
87
- let attentionHoldId: string | undefined;
88
-
89
- function finalizeChain(): void {
90
- chainActive = false;
91
- const holdId = attentionHoldId;
92
- attentionHoldId = undefined;
93
- if (holdId) emitTauEvent(pi, "tau:attention.hold.release", { id: holdId, disposition: "notify" });
94
- }
95
84
 
96
85
  pi.registerMessageRenderer<FailureDetails>(MESSAGE_TYPE, (message, { expanded }, theme) =>
97
86
  renderFailure(asFailureDetails(message.details), expanded, theme),
@@ -102,9 +91,6 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
102
91
  sessionActive = true;
103
92
  turnStart = Date.now();
104
93
  turnPaths = new Set();
105
- chainActive = false;
106
- attentionHoldSequence = 0;
107
- attentionHoldId = undefined;
108
94
  });
109
95
 
110
96
  registerPromptSource(pi, {
@@ -119,41 +105,43 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
119
105
  });
120
106
 
121
107
  pi.on("agent_start", async (_event, ctx) => {
122
- const startingChain = !chainActive;
123
- chainActive = true;
124
- if (!settings.enabled || settings.commands.length === 0) {
125
- if (startingChain) {
126
- turnStart = Date.now();
127
- turnPaths = new Set();
128
- }
129
- return;
130
- }
131
- if (!startingChain) return;
132
- attentionHoldId = `silent-command-runner:${++attentionHoldSequence}`;
133
- emitTauEvent(pi, "tau:attention.hold.acquire", { id: attentionHoldId });
134
108
  turnStart = Date.now();
135
- const projectRoot = await resolveProjectRoot(ctx.cwd);
136
- turnPaths = new Set(await walkFiles(projectRoot));
109
+ turnPaths =
110
+ settings.enabled && settings.commands.length > 0
111
+ ? new Set(await walkFiles(await resolveProjectRoot(ctx.cwd)))
112
+ : new Set();
137
113
  });
138
114
 
139
- pi.on("agent_end", async (event, ctx) => {
140
- if (hasAbortedAssistantMessage(event.messages)) return;
115
+ // The last boundary before the run settles: failures continue the run instead of starting a new one.
116
+ pi.on("agent_before_settle", async (event, ctx) => {
117
+ if (event.outcome !== "completed") return undefined;
141
118
  try {
142
- await runChangedCommands(ctx.cwd, turnStart, ctx.ui.notify);
119
+ const failures = await runChangedCommands(ctx.cwd, ctx.ui.notify);
120
+ if (!failures || failures.length === 0) return undefined;
121
+ return {
122
+ entries: [
123
+ ...event.entries,
124
+ {
125
+ type: "custom_message" as const,
126
+ customType: MESSAGE_TYPE,
127
+ content: formatAgentMessage(failures),
128
+ display: true,
129
+ details: { failed: [...failures] } satisfies FailureDetails,
130
+ },
131
+ ],
132
+ continue: true,
133
+ };
143
134
  } catch (error: unknown) {
144
135
  ctx.ui.notify(`silent-command-runner: ${errorMessage(error)}`, "error");
136
+ return undefined;
145
137
  }
146
138
  });
147
139
 
148
- pi.on("agent_settled", finalizeChain);
149
-
150
140
  pi.on("session_shutdown", () => {
151
141
  sessionActive = false;
152
142
  abortController?.abort();
153
143
  abortController = undefined;
154
144
  turnPaths = new Set();
155
- chainActive = false;
156
- attentionHoldId = undefined;
157
145
  });
158
146
 
159
147
  async function collectCommandFailures(
@@ -183,37 +171,16 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
183
171
  return failures;
184
172
  }
185
173
 
186
- function reportCommandResults(
187
- changed: readonly CommandConfig[],
188
- failures: readonly FailedCommandDetails[],
189
- notify: (message: string, type?: "info" | "warning" | "error") => void,
190
- ): void {
191
- const failedNames = new Set(failures.map((failure) => failure.name));
192
- const passed = changed.filter((command) => !failedNames.has(command.name));
193
- if (passed.length > 0) notify(`silent-command-runner: passed ${formatCommandNames(passed)}`, "info");
194
- if (failures.length === 0) return;
195
- pi.sendMessage<FailureDetails>(
196
- {
197
- customType: MESSAGE_TYPE,
198
- content: formatAgentMessage(failures),
199
- display: true,
200
- details: { failed: [...failures] },
201
- },
202
- { deliverAs: "followUp" },
203
- );
204
- }
205
-
206
174
  async function runChangedCommands(
207
175
  cwd: string,
208
- turnStart: number,
209
176
  notify: (message: string, type?: "info" | "warning" | "error") => void,
210
- ): Promise<void> {
211
- if (!settings.enabled || settings.commands.length === 0) return;
177
+ ): Promise<FailedCommandDetails[] | undefined> {
178
+ if (!settings.enabled || settings.commands.length === 0) return undefined;
212
179
 
213
180
  const projectRoot = await resolveProjectRoot(cwd);
214
181
  const paths = await walkFiles(projectRoot);
215
182
  const changed = await scanChangedCommands(projectRoot, settings.commands, paths, turnPaths, turnStart);
216
- if (!sessionActive || changed.length === 0) return;
183
+ if (!sessionActive || changed.length === 0) return undefined;
217
184
 
218
185
  notify(
219
186
  changed.length === 1
@@ -223,8 +190,16 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
223
190
  );
224
191
 
225
192
  const failures = await collectCommandFailures(projectRoot, changed, notify);
226
- if (failures === undefined || !sessionActive) return;
227
- reportCommandResults(changed, failures, notify);
193
+ if (failures === undefined || !sessionActive) return undefined;
194
+ const failedNames = new Set(failures.map((failure) => failure.name));
195
+ const passed = changed.filter((command) => !failedNames.has(command.name));
196
+ if (passed.length > 0) notify(`silent-command-runner: passed ${formatCommandNames(passed)}`, "info");
197
+ if (failures.length > 0) {
198
+ // Only files the agent changes after this report can trigger another check and continuation.
199
+ turnStart = Date.now();
200
+ turnPaths = new Set(paths);
201
+ }
202
+ return failures;
228
203
  }
229
204
  }
230
205
 
@@ -255,7 +230,7 @@ function normalizeSettings(value: typeof silentCommandRunnerSettings.defaults):
255
230
 
256
231
  function formatSilentCheckPrompt(commands: readonly CommandConfig[]): string {
257
232
  return [
258
- "Do not manually run the commands listed below. They run automatically after matching changes. After fixing a reported failure, end the turn so they can run again. Targeted checks outside this list remain allowed when needed.",
233
+ "Do not manually run the commands listed below. They run automatically after matching changes. Complete the work and finish normally; treat it as correct unless a failure is reported. Do not announce pending validation, hedge completion because of these commands, or tell the user that checks will run or rerun. If a failure is reported, fix it and describe the correction, then finish normally. Do not claim commands passed without evidence. Targeted checks outside this list remain allowed when needed.",
259
234
  "Automatic commands:",
260
235
  ...commands.map(formatSilentCheckCommand),
261
236
  ].join("\n");
@@ -271,12 +246,6 @@ function formatSilentCheckCommand(command: CommandConfig): string {
271
246
  ].join("\n");
272
247
  }
273
248
 
274
- function hasAbortedAssistantMessage(messages: readonly unknown[]): boolean {
275
- return messages.some(
276
- (message) => isRecord(message) && message.role === "assistant" && message.stopReason === "aborted",
277
- );
278
- }
279
-
280
249
  async function scanChangedCommands(
281
250
  projectRoot: string,
282
251
  commands: readonly CommandConfig[],
@@ -403,7 +372,7 @@ function formatAgentMessage(failures: readonly FailedCommandDetails[]): string {
403
372
  return [
404
373
  `silent-command-runner failed: ${failures.length} command${failures.length === 1 ? "" : "s"}`,
405
374
  ...failures.map(formatAgentFailure),
406
- "**Do not** rerun these checks. They will run automatically after your fix.",
375
+ "Fix the reported failures without manually rerunning these commands. Describe the correction and finish normally; do not announce validation or reruns.",
407
376
  ].join("\n\n");
408
377
  }
409
378
 
@@ -2,10 +2,8 @@
2
2
 
3
3
  Soul supplies Tau's system prompt: communication, discussion, planning, execution, and coding guidance. It is always on.
4
4
 
5
- Soul captures tools, skills, project instructions, documentation, and environment context in a saved baseline. Reload and resume reuse it. Successful compaction captures a fresh baseline, including a new date and directory listing.
5
+ Soul adds Pi documentation pointers, tool guidance, and the context that other Tau extensions supply, such as the local date, directory snapshot, and automatic-check instructions. The date and directory snapshot are captured on the first prompt and again after successful compaction, so they stay fixed across turns, reload, resume, and tree navigation.
6
6
 
7
- Changes to available agents, automatic checks, approval guidance, and loaded tools are supplied as saved context updates. Earlier instructions and updates keep their positions instead of being rewritten each turn.
7
+ Everything else is read each turn. When an instruction changes, such as an edited `AGENTS.md` after `/reload`, a changed automatic-check configuration, or a different set of active tools, Pi appends the updated section without rewriting earlier instructions. Models that cannot take later system messages receive the change in the leading instructions, which costs one cache miss on the next request.
8
8
 
9
- Tools that cannot be loaded without changing the cached prefix wait for compaction. Tool execution permissions still take effect immediately. Soul reports incompatible prompt replacements or changes to previously sent history rather than silently replacing its baseline.
10
-
11
- Run `/reload` after changing this extension. The first request after installation captures the new Soul baseline; later instruction edits take effect after compaction.
9
+ Run `/reload` after changing this extension.