@shanepadgett/tau-agent 0.42.2 → 0.43.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 (69) hide show
  1. package/README.md +0 -1
  2. package/docs/context.md +1 -1
  3. package/docs/extending-tau-agent.md +1 -2
  4. package/extensions/attention/README.md +1 -1
  5. package/extensions/cache-diagnostics/index.ts +4 -0
  6. package/extensions/context/README.md +1 -24
  7. package/extensions/context/definitions.ts +1 -52
  8. package/extensions/context/index.ts +1 -150
  9. package/extensions/context/panel.ts +0 -72
  10. package/extensions/explore/guidance.ts +9 -1
  11. package/extensions/footer/README.md +1 -1
  12. package/extensions/footer/index.ts +3 -25
  13. package/extensions/qna/choice-question-body.ts +7 -2
  14. package/extensions/run-summary/README.md +1 -1
  15. package/extensions/run-summary/index.ts +18 -25
  16. package/extensions/runtime-context/README.md +1 -1
  17. package/extensions/runtime-context/context.ts +1 -1
  18. package/extensions/runtime-context/index.ts +10 -16
  19. package/extensions/silent-command-runner/README.md +0 -2
  20. package/extensions/silent-command-runner/index.ts +12 -41
  21. package/extensions/soul/README.md +5 -8
  22. package/extensions/soul/context.ts +115 -0
  23. package/extensions/soul/index.ts +141 -7
  24. package/extensions/soul/prompt.ts +47 -102
  25. package/extensions/soul/state.ts +114 -0
  26. package/extensions/soul/tools.ts +31 -0
  27. package/extensions/tau-help/help.md +6 -18
  28. package/extensions/tau-help/index.ts +10 -4
  29. package/extensions/tool-approval/README.md +2 -0
  30. package/extensions/tool-approval/index.ts +65 -44
  31. package/extensions/tool-approval/panel.ts +153 -0
  32. package/extensions/tool-loader/README.md +1 -1
  33. package/extensions/tool-loader/index.ts +91 -22
  34. package/package.json +2 -2
  35. package/schemas/tau.schema.json +0 -104
  36. package/shared/bounded-text-result.ts +0 -1
  37. package/shared/events.ts +19 -15
  38. package/shared/isolated-session.ts +1 -2
  39. package/shared/model-effort.ts +15 -21
  40. package/shared/prompt-contributions.ts +24 -0
  41. package/src/index.ts +1 -1
  42. package/docs/subagents.md +0 -92
  43. package/extensions/auto-compact/README.md +0 -9
  44. package/extensions/auto-compact/index.ts +0 -122
  45. package/extensions/auto-compact/settings.ts +0 -30
  46. package/extensions/context/settings.ts +0 -62
  47. package/extensions/context/sync.ts +0 -276
  48. package/extensions/context/validation.ts +0 -88
  49. package/extensions/effort/README.md +0 -7
  50. package/extensions/effort/index.ts +0 -134
  51. package/extensions/effort/state.ts +0 -18
  52. package/extensions/qna/inline-editor-row.ts +0 -56
  53. package/extensions/soul/overseer.ts +0 -273
  54. package/extensions/soul/settings.ts +0 -34
  55. package/extensions/subagent/README.md +0 -67
  56. package/extensions/subagent/agents/context-sync.md +0 -244
  57. package/extensions/subagent/agents/dormant/generalist.md +0 -33
  58. package/extensions/subagent/agents/scout.md +0 -104
  59. package/extensions/subagent/agents/web-research.md +0 -101
  60. package/extensions/subagent/agents.ts +0 -261
  61. package/extensions/subagent/cmux-dashboard.ts +0 -495
  62. package/extensions/subagent/index.ts +0 -425
  63. package/extensions/subagent/panel.ts +0 -124
  64. package/extensions/subagent/render.ts +0 -74
  65. package/extensions/subagent/resume.ts +0 -78
  66. package/extensions/subagent/run.ts +0 -641
  67. package/extensions/subagent/runtime.ts +0 -1296
  68. package/extensions/subagent/session-resource.ts +0 -61
  69. package/extensions/subagent/settings.ts +0 -18
package/README.md CHANGED
@@ -45,5 +45,4 @@ pi -e .
45
45
 
46
46
  - [Context management](./docs/context.md) — repository context structure and taxonomy
47
47
  - [Extending Tau Agent](./docs/extending-tau-agent.md) — public events and integration
48
- - [Subagents](./docs/subagents.md) — custom agent definitions
49
48
  - [TUI](./docs/tui.md) — shared UI components
package/docs/context.md CHANGED
@@ -73,4 +73,4 @@ Do not store raw line ranges in the catalog — they drift. `show` stores durabl
73
73
 
74
74
  Inspect the existing catalog before placing new paths. Re-evaluate domain, concept, and entry boundaries after moves, ownership changes, or a coherent batch of new work; avoid stuffing paths into the nearest existing bucket.
75
75
 
76
- Run `/context-sync` to update the catalog from uncommitted repository changes (editor is replaced until it finishes; Escape or Ctrl+C cancels). Tau can also delegate to the `context-sync` subagent when automation is enabled. The agent uses normal repo tools, checks every eligible changed file for membership, removes stale paths, and re-evaluates taxonomy and work-pack quality (including `read` / `show` / `outline` / `references`) before editing `.pi/contexts`. Nudge with `/context-sync <note>` when you want a quality rewrite of weak bags, not only dirty-path coverage.
76
+ Edit the TOML files in `.pi/contexts` when code or work scopes change. `/context` reads the current catalog when opened; selecting an entry injects its current files and declaration slices. The catalog is not generated or checked after agent turns.
@@ -108,7 +108,6 @@ Only events documented in this file are public. Extensions run trusted in-proces
108
108
 
109
109
  Related:
110
110
 
111
- - [Custom subagents](./subagents.md)
112
111
  - [TUI components](./tui.md)
113
112
 
114
113
  ## `tau:footer-item`
@@ -187,4 +186,4 @@ Fields:
187
186
  - `rowId`: tool row id.
188
187
  - `state`: optional visual state. Omit to clear.
189
188
 
190
- Most extenders do not need this; it is for coordinating tool-row rendering with Tau's explore/patch/subagent tooling.
189
+ Most extenders do not need this; it is for coordinating tool-row rendering with Tau's explore/patch tooling.
@@ -1,6 +1,6 @@
1
1
  # Attention
2
2
 
3
- Sends a terminal-driven attention notification when Tau is ready for input, finishes a compaction without an automatic continuation, or summarizes an abandoned branch during tree navigation.
3
+ Sends a terminal-driven attention notification when Tau is ready for input, finishes a compaction, or summarizes an abandoned branch during tree navigation.
4
4
 
5
5
  ## Behavior
6
6
 
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { appendFile, type FileHandle, mkdir, open, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { getAgentDir, type BuildSystemPromptOptions, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import { onTauEventImmediately } from "../../shared/events.ts";
5
6
 
6
7
  const RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
7
8
  const CACHE_NOISE_FLOOR = 1_024;
@@ -329,6 +330,9 @@ export default function cacheDiagnosticsExtension(pi: ExtensionAPI): void {
329
330
  pi.on("before_agent_start", (event) => {
330
331
  latestPromptState = fingerprintPromptState(event.systemPrompt, event.systemPromptOptions);
331
332
  });
333
+ onTauEventImmediately(pi, "cache-diagnostics.prompt", "tau:prompt.snapshot", ({ text }) => {
334
+ if (latestPromptState) latestPromptState = { ...latestPromptState, systemPromptHash: hashJson(text).hash };
335
+ });
332
336
 
333
337
  pi.on("turn_start", (event) => {
334
338
  turnIndex = event.turnIndex;
@@ -6,30 +6,7 @@ See [Context management](../../docs/context.md) for catalog structure and taxono
6
6
 
7
7
  Use `/context` to inject entries into the conversation. Selecting entries reads `read` paths in full, resolves `show` targets to current declaration slices, outlines `outline` paths, and adds one hidden note listing `references` and instructing the agent to treat the injected material as current. Injection happens once, immediately, before your next prompt; nothing is rebuilt on later model calls. Prefer complete reads for bounded work files; use `show` for thin contracts from larger neighbors; reserve `outline` for large or noisy files.
8
8
 
9
- After meaningful uncommitted work (new/moved ownership, not trivial already-covered polish), the coding agent should run the `context-sync` subagent so `.pi/contexts` stays aligned. Context sync catalogs durable code and long-lived documentation as **work packs** (job-shaped entries an agent can start from), not bare path indexes. Scratch pads, working plans, interviews, rough ideas, and other temporary artifacts should stay out; add recurring transient paths to `validation.ignoreGlobs`. Humans can also run `/context-sync` or `/context-sync <nudge>`; the command replaces the editor with a status panel (Escape / Ctrl+C cancel). It uses normal repo tools, walks domain → concept → entry → start-pack modes, and prefers `patch` under `.pi/contexts`. The harness verifies catalog coverage afterward. Optional nudge text soft-steers judgment (including quality rewrites of weak bags) without skipping the ladder or coverage.
10
-
11
- Sync surface is configurable:
12
-
13
- - `sync.enabled` (default true) — master switch. Off: no `/context-sync`, parent cannot call `context-sync`, validation does not auto-run sync.
14
- - `sync.automation` (default true) — when false with sync still enabled: manual `/context-sync` only (coding agent does not see context-sync). Validation auto-run still works if validation is enabled.
15
- - `validation.enabled` (default false) — after agent turns, check membership and auto-run context-sync on failure (requires `sync.enabled`).
16
-
17
- ```json
18
- {
19
- "extensions": {
20
- "context": {
21
- "sync": {
22
- "enabled": true,
23
- "automation": true
24
- },
25
- "validation": {
26
- "enabled": true,
27
- "ignoreGlobs": ["generated/**"]
28
- }
29
- }
30
- }
31
- }
32
- ```
9
+ Edit `.pi/contexts` files to maintain the catalog. Keep entries as work packs for recurring jobs; leave scratch files and plans out of the catalog. `/context` reads current catalog files when opened.
33
10
 
34
11
  ```toml
35
12
  name = "Player"
@@ -2,7 +2,6 @@ import { access, readFile, readdir } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
4
4
  import { parse } from "smol-toml";
5
- import { matchGlob } from "../../shared/glob.ts";
6
5
 
7
6
  export type ContextShowView = "signature" | "signatureWithDocs" | "declaration" | "declarationWithImports";
8
7
 
@@ -27,26 +26,6 @@ export interface ContextEntry {
27
26
  path: string;
28
27
  }
29
28
 
30
- const CONTEXT_IGNORED_FILENAMES = new Set([
31
- "bun.lock",
32
- "bun.lockb",
33
- "Cargo.lock",
34
- "composer.lock",
35
- "flake.lock",
36
- "Gemfile.lock",
37
- "go.sum",
38
- "mix.lock",
39
- "npm-shrinkwrap.json",
40
- "package-lock.json",
41
- "Package.resolved",
42
- "Pipfile.lock",
43
- "pnpm-lock.yaml",
44
- "Podfile.lock",
45
- "poetry.lock",
46
- "pubspec.lock",
47
- "uv.lock",
48
- "yarn.lock",
49
- ]);
50
29
  const CONTEXT_ENTRY_FIELDS = new Set(["description", "read", "show", "outline", "references"]);
51
30
  const CONTEXT_SHOW_VIEWS = new Set<ContextShowView>([
52
31
  "signature",
@@ -56,28 +35,7 @@ const CONTEXT_SHOW_VIEWS = new Set<ContextShowView>([
56
35
  ]);
57
36
  const CONTEXT_SHOW_TARGET_FIELDS = new Set(["path", "name", "view"]);
58
37
 
59
- export function isContextEligiblePath(path: string, ignoreGlobs: readonly string[] = []): boolean {
60
- return (
61
- path !== "LICENSE" &&
62
- !CONTEXT_IGNORED_FILENAMES.has(basename(path)) &&
63
- !ignoreGlobs.some((glob) => matchGlob(glob, path)) &&
64
- path !== ".pi/tau/ideas.jsonl" &&
65
- path !== ".pi/tau/reviews" &&
66
- !path.startsWith(".pi/tau/reviews/") &&
67
- path !== ".working" &&
68
- !path.startsWith(".working/") &&
69
- path !== ".pi/contexts" &&
70
- !path.startsWith(".pi/contexts/")
71
- );
72
- }
73
-
74
- export function isSensitiveContextPath(path: string): boolean {
75
- const name = basename(path);
76
- if (name === ".env.example" || name === ".env.sample") return false;
77
- return name === ".env" || name.startsWith(".env.") || /\.(?:pem|key|crt|p12|pfx)$/i.test(name);
78
- }
79
-
80
- export async function pathExists(path: string): Promise<boolean> {
38
+ async function pathExists(path: string): Promise<boolean> {
81
39
  try {
82
40
  await access(path);
83
41
  return true;
@@ -154,15 +112,6 @@ function sortedUnique(values: readonly string[]): string[] {
154
112
  return [...new Set(values)].sort((a, b) => a.localeCompare(b));
155
113
  }
156
114
 
157
- export function contextEntryPaths(entry: Pick<ContextEntry, "read" | "show" | "outline" | "references">): string[] {
158
- return sortedUnique([
159
- ...entry.read,
160
- ...entry.show.map((target) => target.path),
161
- ...entry.outline,
162
- ...entry.references,
163
- ]);
164
- }
165
-
166
115
  function parseOneShowTarget(root: string, catalogPath: string, entryName: string, item: unknown): ContextShowTarget {
167
116
  const record = item as Record<string, unknown>;
168
117
  const unknownField = Object.keys(record).find((field) => !CONTEXT_SHOW_TARGET_FIELDS.has(field));
@@ -2,13 +2,8 @@ import { randomUUID } from "node:crypto";
2
2
  import { join } from "node:path";
3
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import { prepareFileInjection } from "@shanepadgett/tau-agent";
5
- import { createGitRunner, loadRepoStatus } from "../../shared/git.ts";
6
- import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
7
5
  import { findProjectRoot, loadContextEntries, type ContextEntry, type ContextShowTarget } from "./definitions.ts";
8
- import { ContextPanel, ContextSyncStatusPanel } from "./panel.ts";
9
- import contextSettings from "./settings.ts";
10
- import { type ContextSyncDetails, runContextSync } from "./sync.ts";
11
- import { formatContextValidationFailure, validateContextCatalog } from "./validation.ts";
6
+ import { ContextPanel } from "./panel.ts";
12
7
 
13
8
  const CONTEXT_BRIEF_TYPE = "tau.context.brief";
14
9
 
@@ -103,91 +98,7 @@ async function injectSelectedContext(
103
98
  }
104
99
  }
105
100
 
106
- function hasAbortedAssistant(messages: readonly unknown[]): boolean {
107
- return messages.some(
108
- (message) =>
109
- typeof message === "object" &&
110
- message !== null &&
111
- "role" in message &&
112
- message.role === "assistant" &&
113
- "stopReason" in message &&
114
- message.stopReason === "aborted",
115
- );
116
- }
117
-
118
- async function handleAgentEndValidation(
119
- pi: ExtensionAPI,
120
- ctx: ExtensionContext,
121
- settings: typeof contextSettings.defaults,
122
- lastValidationFailure: string | undefined,
123
- ): Promise<string | undefined> {
124
- if (!settings.sync.enabled || !settings.validation.enabled || !ctx.isProjectTrusted()) return undefined;
125
- const root = await findProjectRoot(ctx.cwd);
126
- const git = createGitRunner(pi, ctx);
127
- if (!(await loadRepoStatus(git))) return lastValidationFailure;
128
- const failure = formatContextValidationFailure(
129
- await validateContextCatalog(git, root, settings.validation.ignoreGlobs),
130
- );
131
- if (!failure) return undefined;
132
- if (failure === lastValidationFailure) return lastValidationFailure;
133
- ctx.ui.notify("Context catalog validation failed; running context-sync", "error");
134
- const result = ctx.mode === "tui" ? await runContextSyncWithEditor(pi, ctx, {}) : await runContextSync(pi, ctx, {});
135
- if (!result || result.outcome === "cancelled") {
136
- ctx.ui.notify("Context sync cancelled", "info");
137
- return failure;
138
- }
139
- const afterFailure = formatContextValidationFailure(
140
- await validateContextCatalog(git, root, settings.validation.ignoreGlobs),
141
- );
142
- if (result.outcome === "failed" || afterFailure) {
143
- ctx.ui.notify(
144
- result.outcome === "failed" ? result.summary : "Context catalog still invalid after context-sync",
145
- "error",
146
- );
147
- return afterFailure ?? `${failure}\n${result.reason}`;
148
- }
149
- ctx.ui.notify(result.summary, "info");
150
- return undefined;
151
- }
152
-
153
101
  export default function contextExtension(pi: ExtensionAPI): void {
154
- let settings = contextSettings.defaults;
155
- let lastValidationFailure: string | undefined;
156
- let syncCommandRegistered = false;
157
-
158
- const refreshSettings = async (ctx: { cwd: string; isProjectTrusted(): boolean }) => {
159
- settings = await loadTauExtensionSettings(ctx, contextSettings);
160
- if (settings.sync.enabled) registerContextSyncCommand();
161
- };
162
-
163
- const registerContextSyncCommand = () => {
164
- if (syncCommandRegistered) return;
165
- syncCommandRegistered = true;
166
- pi.registerCommand("context-sync", {
167
- description: "Synchronize repository context from current Git changes via context-sync subagent",
168
- handler: async (args, ctx) => {
169
- if (!(await loadTauExtensionSettings(ctx, contextSettings)).sync.enabled) {
170
- ctx.ui.notify("Context sync is disabled in settings", "warning");
171
- return;
172
- }
173
- if (ctx.mode !== "tui" || !ctx.isProjectTrusted()) {
174
- ctx.ui.notify("/context-sync requires a trusted TUI project", "warning");
175
- return;
176
- }
177
- await ctx.waitForIdle();
178
- const result = await runContextSyncWithEditor(pi, ctx, {
179
- nudge: args.trim() || undefined,
180
- });
181
- if (!result) return;
182
- if (result.outcome === "cancelled") {
183
- ctx.ui.notify(result.summary, "info");
184
- return;
185
- }
186
- ctx.ui.notify(result.summary, result.outcome === "failed" ? "error" : "info");
187
- },
188
- });
189
- };
190
-
191
102
  pi.registerCommand("context", {
192
103
  description: "Inject repository context entries into the conversation",
193
104
  handler: async (_args, ctx) => {
@@ -213,64 +124,4 @@ export default function contextExtension(pi: ExtensionAPI): void {
213
124
  await injectSelectedContext(pi, ctx, root, selected);
214
125
  },
215
126
  });
216
-
217
- // Default on: register immediately so /context-sync exists before session_start in tests and early UI.
218
- if (settings.sync.enabled) registerContextSyncCommand();
219
-
220
- pi.on("session_start", async (_event, ctx) => {
221
- await refreshSettings(ctx);
222
- });
223
- pi.on("agent_start", async (_event, ctx) => {
224
- await refreshSettings(ctx);
225
- });
226
- pi.on("agent_end", async (event, ctx) => {
227
- if (hasAbortedAssistant(event.messages)) return;
228
- settings = await loadTauExtensionSettings(ctx, contextSettings);
229
- try {
230
- lastValidationFailure = await handleAgentEndValidation(pi, ctx, settings, lastValidationFailure);
231
- } catch (error) {
232
- ctx.ui.notify(`Context validation failed: ${error instanceof Error ? error.message : String(error)}`, "error");
233
- }
234
- });
235
- }
236
-
237
- async function runContextSyncWithEditor(
238
- pi: ExtensionAPI,
239
- ctx: ExtensionContext,
240
- options: { nudge?: string },
241
- ): Promise<ContextSyncDetails | undefined> {
242
- return ctx.ui.custom<ContextSyncDetails | undefined>((tui, theme, _keys, done) => {
243
- const panel = new ContextSyncStatusPanel(tui, theme, "Synchronizing repository context");
244
- void (async () => {
245
- try {
246
- const result = await runContextSync(pi, ctx, {
247
- nudge: options.nudge,
248
- signal: panel.signal,
249
- onStatus: (status) => {
250
- panel.update(status);
251
- },
252
- });
253
- done(result);
254
- } catch (error) {
255
- if (panel.signal.aborted) {
256
- done({
257
- outcome: "cancelled",
258
- summary: "Context sync cancelled",
259
- reason: "Cancelled by user.",
260
- changedContextFiles: [],
261
- });
262
- return;
263
- }
264
- done({
265
- outcome: "failed",
266
- summary: `Context sync failed: ${error instanceof Error ? error.message : String(error)}`,
267
- reason: error instanceof Error ? error.message : String(error),
268
- changedContextFiles: [],
269
- });
270
- } finally {
271
- panel.dispose();
272
- }
273
- })();
274
- return panel;
275
- });
276
127
  }
@@ -196,75 +196,3 @@ export class ContextPanel implements Component {
196
196
  this.tui.requestRender();
197
197
  }
198
198
  }
199
-
200
- export class ContextSyncStatusPanel implements Component {
201
- private readonly tui: TUI;
202
- private readonly panel: ToolPanel;
203
- private readonly config: ToolPanelConfig;
204
- private readonly controller = new AbortController();
205
- private readonly startedAt = Date.now();
206
- private readonly timer: ReturnType<typeof setInterval>;
207
- private lines: string[] = [];
208
- private lineCount = 0;
209
-
210
- constructor(tui: TUI, theme: Theme, status: string) {
211
- this.tui = tui;
212
- this.config = {
213
- title: "Context sync",
214
- body: {
215
- render: (width) => {
216
- const visible =
217
- this.lineCount > 5
218
- ? [`${this.lineCount - 4} earlier lines omitted`, ...this.lines.slice(-4)]
219
- : this.lines;
220
- return visible.map((line, index) =>
221
- truncateToWidth(theme.fg(index === 0 && this.lineCount > 5 ? "dim" : "muted", line), width, "…"),
222
- );
223
- },
224
- invalidate: () => {},
225
- },
226
- footer: { kind: "hints", hints: [bindingHint("tui.select.cancel", "cancel")] },
227
- border: "horizontal",
228
- };
229
- this.panel = new ToolPanel(theme, this.config);
230
- this.update(status);
231
- this.timer = setInterval(() => this.tui.requestRender(), 1_000);
232
- this.timer.unref();
233
- }
234
-
235
- get signal(): AbortSignal {
236
- return this.controller.signal;
237
- }
238
-
239
- handleInput(data: string): void {
240
- if (!getKeybindings().matches(data, "tui.select.cancel")) return;
241
- if (this.controller.signal.aborted) return;
242
- this.update("Cancelling context sync");
243
- this.controller.abort();
244
- }
245
-
246
- update(status: string): void {
247
- const added = status
248
- .split(/\r?\n/)
249
- .map((line) => line.trim())
250
- .filter(Boolean);
251
- this.lineCount += added.length;
252
- this.lines = [...this.lines, ...added].slice(-5);
253
- this.tui.requestRender();
254
- }
255
-
256
- render(width: number): string[] {
257
- const elapsed = Math.floor((Date.now() - this.startedAt) / 1_000);
258
- this.config.title =
259
- elapsed < 60 ? `Context sync · ${elapsed}s` : `Context sync · ${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
260
- return this.panel.render(width);
261
- }
262
-
263
- invalidate(): void {
264
- this.panel.invalidate();
265
- }
266
-
267
- dispose(): void {
268
- clearInterval(this.timer);
269
- }
270
- }
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { registerPromptSource } from "../../shared/prompt-contributions.ts";
2
3
 
3
4
  const GUIDANCE = `## Explore
4
5
  Shape-backed languages: \`markdown\`, \`typescript\`, \`tsx\`, \`go\`, \`rust\`, \`c_sharp\`, \`java\`, \`kotlin\`, \`swift\`.
@@ -8,5 +9,12 @@ Full \`read\` of a large registered source returns outline + follow-up hint, not
8
9
  \`show\` always takes a top-level \`targets\` array, even for one declaration: \`{"targets":[{"path":"...","name":"..."}],"view":"declaration"}\`.`;
9
10
 
10
11
  export function registerExploreGuidance(pi: ExtensionAPI): void {
11
- pi.on("before_agent_start", (event) => ({ systemPrompt: `${event.systemPrompt}\n\n${GUIDANCE}` }));
12
+ registerPromptSource(pi, {
13
+ key: "explore/guidance",
14
+ section: "tool-guidance",
15
+ refresh: "compaction",
16
+ async read() {
17
+ return GUIDANCE;
18
+ },
19
+ });
12
20
  }
@@ -14,7 +14,7 @@ Replaces Pi’s default footer with Tau’s compact two-line footer.
14
14
  ## Layout
15
15
 
16
16
  ```text
17
- git • EFFORT • provider/model (thinking) S $0.18 · D $2.43
17
+ git • provider/model (thinking) S $0.18 · D $2.43
18
18
  cwd • session footer items
19
19
  ```
20
20
 
@@ -6,8 +6,7 @@ import { homedir } from "node:os";
6
6
  import { join, relative } from "node:path";
7
7
  import type { ExtensionAPI, ExtensionContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
8
8
  import { type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
- import { emitTauEvent, onTauEvent } from "../../shared/events.ts";
10
- import { effortForSelection, type ModelEffort } from "../../shared/model-effort.ts";
9
+ import { onTauEvent } from "../../shared/events.ts";
11
10
  import { loadTauExtensionSettings, updateTauExtensionSettings } from "../../shared/settings/load.ts";
12
11
  import footerSettings from "./settings.ts";
13
12
 
@@ -40,8 +39,6 @@ export default function footerExtension(pi: ExtensionAPI): void {
40
39
  let footerInstalled = false;
41
40
  let requestRender: (() => void) | undefined;
42
41
  let unsubscribeFooterItems: (() => void) | undefined;
43
- let unsubscribeModelEffort: (() => void) | undefined;
44
- let activeEffort: ModelEffort | undefined;
45
42
  const gitByCwd = new Map<string, GitSummary | undefined>();
46
43
  let gitRefresh: Promise<void> | undefined;
47
44
  let dailyCost: number | undefined;
@@ -80,8 +77,7 @@ export default function footerExtension(pi: ExtensionAPI): void {
80
77
  const model = currentCtx.model ? `${currentCtx.model.provider}/${currentCtx.model.id}` : "no-model";
81
78
  const thinking = pi.getThinkingLevel();
82
79
  const separator = theme.fg("dim", " • ");
83
- const effort = effortText(theme, activeEffort);
84
- const topLeft = [theme.fg("dim", gitText(git)), effort, theme.fg("dim", `${model} (${thinking})`)]
80
+ const topLeft = [theme.fg("dim", gitText(git)), theme.fg("dim", `${model} (${thinking})`)]
85
81
  .filter(Boolean)
86
82
  .join(separator);
87
83
  const sessionUsage = sessionCost(currentCtx);
@@ -172,11 +168,6 @@ export default function footerExtension(pi: ExtensionAPI): void {
172
168
  items.set(item.id, next);
173
169
  render();
174
170
  });
175
- unsubscribeModelEffort = onTauEvent(pi, "footer.model-effort", "tau:model-effort.changed", (state) => {
176
- activeEffort = state.effort;
177
- render();
178
- });
179
-
180
171
  pi.registerCommand(COMMAND, {
181
172
  description: "Toggle Tau footer",
182
173
  handler: async (args, ctx) => {
@@ -213,20 +204,15 @@ export default function footerExtension(pi: ExtensionAPI): void {
213
204
  pi.on("session_start", async (_event, ctx) => {
214
205
  const settings = await loadTauExtensionSettings(ctx, footerSettings);
215
206
  setEnabled(ctx, settings.enabled);
216
- activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, pi.getThinkingLevel());
217
207
  onStateChange(ctx);
218
- emitTauEvent(pi, "tau:model-effort.snapshot.requested", {});
219
208
  });
220
209
  pi.on("session_tree", (_event, ctx) => {
221
- activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, pi.getThinkingLevel());
222
210
  onStateChange(ctx, false);
223
211
  });
224
212
  pi.on("model_select", (_event, ctx) => {
225
- activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, pi.getThinkingLevel());
226
213
  rerender(ctx);
227
214
  });
228
- pi.on("thinking_level_select", (event, ctx) => {
229
- activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, event.level);
215
+ pi.on("thinking_level_select", (_event, ctx) => {
230
216
  rerender(ctx);
231
217
  });
232
218
  pi.on("agent_start", (_event, ctx) => onStateChange(ctx, false));
@@ -236,9 +222,6 @@ export default function footerExtension(pi: ExtensionAPI): void {
236
222
  pi.on("session_shutdown", (_event, ctx) => {
237
223
  unsubscribeFooterItems?.();
238
224
  unsubscribeFooterItems = undefined;
239
- unsubscribeModelEffort?.();
240
- unsubscribeModelEffort = undefined;
241
- activeEffort = undefined;
242
225
  requestRender = undefined;
243
226
  activeCtx = undefined;
244
227
  footerInstalled = false;
@@ -251,11 +234,6 @@ export default function footerExtension(pi: ExtensionAPI): void {
251
234
  }
252
235
  }
253
236
 
254
- function effortText(theme: Theme, effort: ModelEffort | undefined): string {
255
- if (!effort) return "";
256
- return theme.fg("dim", theme.bold(effort.toUpperCase()));
257
- }
258
-
259
237
  async function saveEnabled(ctx: ExtensionContext, enabled: boolean): Promise<void> {
260
238
  await updateTauExtensionSettings("global", ctx, footerSettings, (current) => ({ ...current, enabled }));
261
239
  }
@@ -1,9 +1,14 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { Editor, type Focusable, Key, matchesKey, type TUI } from "@earendil-works/pi-tui";
3
- import { editorTheme } from "@shanepadgett/tau-tui";
3
+ import {
4
+ editorTheme,
5
+ pushSavedNote,
6
+ renderInlineEditor,
7
+ renderNoteEditor,
8
+ wrapWithPrefix,
9
+ } from "@shanepadgett/tau-tui";
4
10
  import { bindingHint, bindingsHint, rawHint, type ToolKeyHint } from "@shanepadgett/tau-tui";
5
11
  import { renderQuestionPrompt } from "./body-render.ts";
6
- import { pushSavedNote, renderInlineEditor, renderNoteEditor, wrapWithPrefix } from "./inline-editor-row.ts";
7
12
  import { renderRecommendation } from "./input-question-body.ts";
8
13
  import {
9
14
  getAnswer,
@@ -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, direct run cost, delegated subagent cost, and their combined total across the full continuation chain.
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.
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.
@@ -7,6 +7,9 @@ const ENTRY_TYPE = "tau.run-summary";
7
7
  interface RunSummary {
8
8
  wallMs: number;
9
9
  runCost: number;
10
+ }
11
+
12
+ interface PreviousRunSummary extends RunSummary {
10
13
  subagentCost: number;
11
14
  totalCost: number;
12
15
  }
@@ -14,7 +17,6 @@ interface RunSummary {
14
17
  export default function runSummaryExtension(pi: ExtensionAPI): void {
15
18
  let startedAt: number | undefined;
16
19
  let runCost = 0;
17
- let subagentCost = 0;
18
20
 
19
21
  pi.registerEntryRenderer<RunSummary>(ENTRY_TYPE, (entry, _options, theme) => {
20
22
  const summary = readRunSummary(entry.data);
@@ -26,8 +28,9 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
26
28
  parts: [
27
29
  `Wall ${formatDuration(summary.wallMs)}`,
28
30
  `Run ${formatCost(summary.runCost)}`,
29
- `Subagents ${formatCost(summary.subagentCost)}`,
30
- `Total ${formatCost(summary.totalCost)}`,
31
+ ...("subagentCost" in summary
32
+ ? [`Subagents ${formatCost(summary.subagentCost)}`, `Total ${formatCost(summary.totalCost)}`]
33
+ : []),
31
34
  ],
32
35
  });
33
36
  });
@@ -35,7 +38,6 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
35
38
  pi.on("session_start", () => {
36
39
  startedAt = undefined;
37
40
  runCost = 0;
38
- subagentCost = 0;
39
41
  });
40
42
 
41
43
  pi.on("agent_start", () => {
@@ -44,12 +46,7 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
44
46
 
45
47
  pi.on("agent_end", (event) => {
46
48
  for (const message of event.messages) {
47
- if (message.role === "assistant") {
48
- runCost += finiteNonNegative((message as AssistantMessage).usage.cost.total);
49
- continue;
50
- }
51
- if (message.role !== "toolResult" || message.toolName !== "subagent") continue;
52
- subagentCost += readUsageCost(message.usage);
49
+ if (message.role === "assistant") runCost += finiteNonNegative((message as AssistantMessage).usage.cost.total);
53
50
  }
54
51
  });
55
52
 
@@ -60,31 +57,27 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
60
57
  pi.appendEntry<RunSummary>(ENTRY_TYPE, {
61
58
  wallMs,
62
59
  runCost,
63
- subagentCost,
64
- totalCost: runCost + subagentCost,
65
60
  });
66
61
  runCost = 0;
67
- subagentCost = 0;
68
62
  });
69
63
  }
70
64
 
71
- function readUsageCost(value: unknown): number {
72
- if (!value || typeof value !== "object") return 0;
73
- const cost = (value as Record<string, unknown>).cost;
74
- if (!cost || typeof cost !== "object") return 0;
75
- return finiteNonNegative((cost as Record<string, unknown>).total);
76
- }
77
-
78
- function readRunSummary(value: unknown): RunSummary | undefined {
65
+ function readRunSummary(value: unknown): RunSummary | PreviousRunSummary | undefined {
79
66
  if (!value || typeof value !== "object") return undefined;
80
67
  const record = value as Record<string, unknown>;
81
- if (![record.wallMs, record.runCost, record.subagentCost, record.totalCost].every(isFiniteNonNegative))
82
- return undefined;
68
+ 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
+ }
83
78
  return {
84
79
  wallMs: record.wallMs as number,
85
80
  runCost: record.runCost as number,
86
- subagentCost: record.subagentCost as number,
87
- totalCost: record.totalCost as number,
88
81
  };
89
82
  }
90
83
 
@@ -1,5 +1,5 @@
1
1
  # Runtime Context
2
2
 
3
- Supplies Tau with current local date and initial root directory snapshot in each agent run's system prompt. Snapshot remains fixed until extension runtime restarts, while date updates when local day changes.
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.
4
4
 
5
5
  After changing this extension, run `/reload` before testing the new behavior.