@shanepadgett/tau-agent 0.16.0 → 0.18.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 (55) hide show
  1. package/docs/subagents.md +25 -6
  2. package/extensions/attention/README.md +1 -0
  3. package/extensions/attention/index.ts +38 -0
  4. package/extensions/context/README.md +13 -3
  5. package/extensions/context/definitions.ts +3 -15
  6. package/extensions/context/evidence.ts +516 -0
  7. package/extensions/context/index.ts +114 -116
  8. package/extensions/context/panel.ts +60 -0
  9. package/extensions/context/settings.ts +30 -1
  10. package/extensions/context/sync.ts +171 -686
  11. package/extensions/context/validation.ts +4 -1
  12. package/extensions/context/write-scope.ts +109 -0
  13. package/extensions/context-pruning/README.md +22 -0
  14. package/extensions/context-pruning/file-evidence.ts +265 -0
  15. package/extensions/context-pruning/index.ts +379 -0
  16. package/extensions/context-pruning/projection.ts +108 -0
  17. package/extensions/context-pruning/prune.ts +346 -0
  18. package/extensions/context-pruning/render.ts +179 -0
  19. package/extensions/context-pruning/settings.ts +41 -0
  20. package/extensions/explore/README.md +3 -1
  21. package/extensions/explore/autoread.ts +85 -22
  22. package/extensions/explore/full-file-knowledge.ts +234 -0
  23. package/extensions/explore/index.ts +2 -3
  24. package/extensions/explore/read-cache.ts +150 -86
  25. package/extensions/explore/read-snapshots.ts +17 -4
  26. package/extensions/explore/read.ts +57 -38
  27. package/extensions/footer/index.ts +62 -60
  28. package/extensions/run-summary/index.ts +5 -5
  29. package/extensions/silent-command-runner/README.md +1 -1
  30. package/extensions/silent-command-runner/index.ts +45 -28
  31. package/extensions/soul/prompt.ts +3 -1
  32. package/extensions/subagent/README.md +21 -5
  33. package/extensions/subagent/agents/context-sync.md +97 -0
  34. package/extensions/subagent/agents/{generalist.md → dormant/generalist.md} +6 -0
  35. package/extensions/subagent/agents/{scout.md → dormant/scout.md} +6 -0
  36. package/extensions/subagent/agents/review.md +48 -0
  37. package/extensions/subagent/agents/web-research.md +6 -0
  38. package/extensions/subagent/agents.ts +15 -2
  39. package/extensions/subagent/cmux-dashboard.ts +454 -0
  40. package/extensions/subagent/index.ts +181 -234
  41. package/extensions/subagent/render.ts +1 -1
  42. package/extensions/subagent/resume.ts +78 -0
  43. package/extensions/subagent/run.ts +213 -118
  44. package/extensions/subagent/runtime.ts +856 -0
  45. package/extensions/subagent/session-resource.ts +169 -0
  46. package/extensions/tau-help/help.md +6 -2
  47. package/extensions/turn-budget/index.ts +8 -36
  48. package/package.json +2 -2
  49. package/schemas/tau.schema.json +48 -1
  50. package/shared/context-pruning-state.ts +364 -0
  51. package/shared/events.ts +18 -0
  52. package/shared/model-fallback/index.ts +21 -10
  53. package/shared/model-fallback/types.ts +5 -3
  54. package/shared/settings/load.ts +78 -1
  55. package/shared/tool-row-state.ts +21 -1
@@ -1,34 +1,82 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { join } from "node:path";
3
- import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
- import { Text } from "@earendil-works/pi-tui";
5
- import { Type } from "typebox";
3
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { getKeybindings } from "@earendil-works/pi-tui";
6
5
  import { emitTauEvent } from "../../shared/events.ts";
7
6
  import { createGitRunner, loadRepoStatus } from "../../shared/git.ts";
8
7
  import { createInjectedContext } from "../../shared/injected-context.ts";
9
8
  import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
10
- import { ContextPanel } from "./panel.ts";
9
+ import { ContextPanel, ContextSyncStatusPanel } from "./panel.ts";
11
10
  import { findProjectRoot, loadContextEntries, type ContextEntry } from "./definitions.ts";
11
+ import { hideContextSyncEvidenceTool, registerContextSyncEvidenceTool } from "./evidence.ts";
12
12
  import contextSettings from "./settings.ts";
13
- import { runContextSync, type ContextSyncDetails } from "./sync.ts";
13
+ import { runContextSync } from "./sync.ts";
14
14
  import { formatContextValidationFailure, validateContextCatalog } from "./validation.ts";
15
15
 
16
- const contextSyncParams = Type.Object({}, { additionalProperties: false });
16
+ export default function contextExtension(pi: ExtensionAPI): void {
17
+ let settings = contextSettings.defaults;
18
+ let lastValidationFailure: string | undefined;
19
+ let syncCommandRegistered = false;
20
+
21
+ registerContextSyncEvidenceTool(pi);
17
22
 
18
- function compactResult(details: ContextSyncDetails) {
19
- const value = {
20
- outcome: details.outcome,
21
- summary: details.summary,
22
- reason: details.reason,
23
- changedContextFiles: details.changedContextFiles,
23
+ const refreshSettings = async (ctx: { cwd: string; isProjectTrusted(): boolean }) => {
24
+ settings = await loadTauExtensionSettings(ctx, contextSettings);
25
+ if (settings.sync.enabled) registerContextSyncCommand();
26
+ hideContextSyncEvidenceTool(pi);
24
27
  };
25
- return { content: [{ type: "text" as const, text: JSON.stringify(value) }], details };
26
- }
27
28
 
28
- export default function contextExtension(pi: ExtensionAPI): void {
29
- let validationSettings = contextSettings.defaults.validation;
30
- let lastValidationFailure: string | undefined;
31
- const activeToolExecutions = new Map<string, { toolName: string; done: Promise<void>; complete: () => void }>();
29
+ const registerContextSyncCommand = () => {
30
+ if (syncCommandRegistered) return;
31
+ syncCommandRegistered = true;
32
+ pi.registerCommand("context-sync", {
33
+ description: "Synchronize repository context from current Git changes via context-sync subagent",
34
+ handler: async (args, ctx) => {
35
+ if (!(await loadTauExtensionSettings(ctx, contextSettings)).sync.enabled) {
36
+ ctx.ui.notify("Context sync is disabled in settings", "warning");
37
+ return;
38
+ }
39
+ if (ctx.mode !== "tui" || !ctx.isProjectTrusted()) {
40
+ ctx.ui.notify("/context-sync requires a trusted TUI project", "warning");
41
+ return;
42
+ }
43
+ await ctx.waitForIdle();
44
+ const controller = new AbortController();
45
+ const widget = showContextSyncWidget(ctx);
46
+ const stopListening = ctx.ui.onTerminalInput((data) => {
47
+ if (!getKeybindings().matches(data, "tui.select.cancel")) return;
48
+ if (!controller.signal.aborted) widget.update("Cancelling context sync");
49
+ controller.abort();
50
+ return { consume: true };
51
+ });
52
+ try {
53
+ const result = await runContextSync(pi, ctx, {
54
+ nudge: args.trim() || undefined,
55
+ signal: controller.signal,
56
+ onStatus: (status) => {
57
+ widget.update(status);
58
+ },
59
+ });
60
+ if (controller.signal.aborted) {
61
+ ctx.ui.notify("Context sync cancelled", "info");
62
+ return;
63
+ }
64
+ const level = result.outcome === "failed" ? "error" : "info";
65
+ ctx.ui.notify(result.summary, level);
66
+ } catch (error) {
67
+ ctx.ui.notify(
68
+ controller.signal.aborted
69
+ ? "Context sync cancelled"
70
+ : `Context sync failed: ${error instanceof Error ? error.message : String(error)}`,
71
+ controller.signal.aborted ? "info" : "error",
72
+ );
73
+ } finally {
74
+ stopListening();
75
+ widget.clear();
76
+ }
77
+ },
78
+ });
79
+ };
32
80
 
33
81
  pi.registerCommand("context", {
34
82
  description: "Select repository context entries and inject their files",
@@ -85,98 +133,14 @@ export default function contextExtension(pi: ExtensionAPI): void {
85
133
  },
86
134
  });
87
135
 
88
- pi.registerCommand("context-sync", {
89
- description: "Synchronize repository context from current Git changes",
90
- handler: async (args, ctx) => {
91
- if (args.trim()) {
92
- ctx.ui.notify("Usage: /context-sync", "warning");
93
- return;
94
- }
95
- if (ctx.mode !== "tui" || !ctx.isProjectTrusted()) {
96
- ctx.ui.notify("/context-sync requires a trusted TUI project", "warning");
97
- return;
98
- }
99
- await ctx.waitForIdle();
100
- ctx.ui.setStatus("context-sync", "synchronizing context");
101
- try {
102
- const result = await runContextSync(pi, ctx);
103
- ctx.ui.notify(result.summary, "info");
104
- } catch (error) {
105
- ctx.ui.notify(`Context sync failed: ${error instanceof Error ? error.message : String(error)}`, "error");
106
- } finally {
107
- ctx.ui.setStatus("context-sync", undefined);
108
- }
109
- },
110
- });
111
-
112
- pi.registerTool(
113
- defineTool<typeof contextSyncParams, ContextSyncDetails | undefined>({
114
- name: "context_sync",
115
- label: "context_sync",
116
- description:
117
- "Synchronize repository context membership after context validation reports changed, stale, or unassigned files.",
118
- parameters: contextSyncParams,
119
- async execute(id, _params, _signal, onUpdate, ctx) {
120
- await Promise.all(
121
- [...activeToolExecutions]
122
- .filter(([toolCallId, execution]) => toolCallId !== id && execution.toolName !== "context_sync")
123
- .map(([, execution]) => execution.done),
124
- );
125
- return compactResult(
126
- await runContextSync(pi, ctx, (status) =>
127
- onUpdate?.({ content: [{ type: "text", text: status }], details: undefined }),
128
- ),
129
- );
130
- },
131
- renderCall(_args, theme, context) {
132
- const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
133
- text.setText(theme.fg("toolTitle", "context_sync"));
134
- return text;
135
- },
136
- renderResult(result, options, theme, context) {
137
- const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
138
- const details = result.details;
139
- const output = result.content.map((part) => (part.type === "text" ? part.text : "")).join("");
140
- text.setText(
141
- options.isPartial
142
- ? theme.fg("dim", output)
143
- : context.expanded && details
144
- ? [
145
- details.summary,
146
- details.reason,
147
- ...details.changes.map((change) =>
148
- change.action === "set-entry"
149
- ? `${change.action} ${change.tab}/${change.concept}/${change.entry}: ${change.files.join(", ")}`
150
- : `${change.action} ${change.tab}/${change.concept}/${change.entry}`,
151
- ),
152
- ...details.changedContextFiles,
153
- ].join("\n")
154
- : (details?.summary ?? theme.fg("error", output)),
155
- );
156
- return text;
157
- },
158
- }),
159
- );
160
-
161
- pi.on("tool_execution_start", (event) => {
162
- let complete = () => {};
163
- const done = new Promise<void>((resolve) => {
164
- complete = resolve;
165
- });
166
- activeToolExecutions.set(event.toolCallId, { toolName: event.toolName, done, complete });
167
- });
168
-
169
- pi.on("tool_execution_end", (event) => {
170
- const execution = activeToolExecutions.get(event.toolCallId);
171
- activeToolExecutions.delete(event.toolCallId);
172
- execution?.complete();
173
- });
136
+ // Default on: register immediately so /context-sync exists before session_start in tests and early UI.
137
+ if (settings.sync.enabled) registerContextSyncCommand();
174
138
 
175
139
  pi.on("session_start", async (_event, ctx) => {
176
- validationSettings = (await loadTauExtensionSettings(ctx, contextSettings)).validation;
140
+ await refreshSettings(ctx);
177
141
  });
178
142
  pi.on("agent_start", async (_event, ctx) => {
179
- validationSettings = (await loadTauExtensionSettings(ctx, contextSettings)).validation;
143
+ await refreshSettings(ctx);
180
144
  });
181
145
  pi.on("agent_end", async (event, ctx) => {
182
146
  if (
@@ -191,7 +155,8 @@ export default function contextExtension(pi: ExtensionAPI): void {
191
155
  )
192
156
  )
193
157
  return;
194
- if (!validationSettings.enabled || !ctx.isProjectTrusted()) {
158
+ settings = await loadTauExtensionSettings(ctx, contextSettings);
159
+ if (!settings.sync.enabled || !settings.validation.enabled || !ctx.isProjectTrusted()) {
195
160
  lastValidationFailure = undefined;
196
161
  return;
197
162
  }
@@ -200,25 +165,58 @@ export default function contextExtension(pi: ExtensionAPI): void {
200
165
  const git = createGitRunner(pi, ctx);
201
166
  if (!(await loadRepoStatus(git))) return;
202
167
  const failure = formatContextValidationFailure(
203
- await validateContextCatalog(git, root, validationSettings.ignoreGlobs),
168
+ await validateContextCatalog(git, root, settings.validation.ignoreGlobs),
204
169
  );
205
170
  if (!failure) {
206
171
  lastValidationFailure = undefined;
207
172
  return;
208
173
  }
174
+ // Same unresolved failure fingerprint: do not re-spawn until the dirty/catalog signal changes.
209
175
  if (failure === lastValidationFailure) return;
210
176
  lastValidationFailure = failure;
211
- ctx.ui.notify("Context catalog validation failed", "error");
212
- pi.sendMessage(
213
- { customType: "tau.context-validation", content: failure, display: false },
214
- { triggerTurn: true },
215
- );
177
+ ctx.ui.notify("Context catalog validation failed; running context-sync", "error");
178
+ const widget = showContextSyncWidget(ctx);
179
+ try {
180
+ const result = await runContextSync(pi, ctx, {
181
+ onStatus: (status) => {
182
+ widget.update(status);
183
+ },
184
+ });
185
+ const afterFailure = formatContextValidationFailure(
186
+ await validateContextCatalog(git, root, settings.validation.ignoreGlobs),
187
+ );
188
+ if (result.outcome === "failed" || afterFailure) {
189
+ lastValidationFailure = afterFailure ?? `${failure}\n${result.reason}`;
190
+ ctx.ui.notify(
191
+ result.outcome === "failed" ? result.summary : "Context catalog still invalid after context-sync",
192
+ "error",
193
+ );
194
+ return;
195
+ }
196
+ lastValidationFailure = undefined;
197
+ ctx.ui.notify(result.summary, "info");
198
+ } finally {
199
+ widget.clear();
200
+ }
216
201
  } catch (error) {
217
202
  ctx.ui.notify(`Context validation failed: ${error instanceof Error ? error.message : String(error)}`, "error");
218
203
  }
219
204
  });
220
- pi.on("session_shutdown", () => {
221
- for (const execution of activeToolExecutions.values()) execution.complete();
222
- activeToolExecutions.clear();
205
+ }
206
+
207
+ function showContextSyncWidget(ctx: ExtensionContext): { update(status: string): void; clear(): void } {
208
+ let widget: ContextSyncStatusPanel | undefined;
209
+ ctx.ui.setWidget("context-sync", (tui, theme) => {
210
+ widget = new ContextSyncStatusPanel(tui, theme, "Synchronizing repository context");
211
+ return widget;
223
212
  });
213
+ return {
214
+ update(status) {
215
+ widget?.update(status);
216
+ },
217
+ clear() {
218
+ ctx.ui.setWidget("context-sync", undefined);
219
+ widget = undefined;
220
+ },
221
+ };
224
222
  }
@@ -187,3 +187,63 @@ export class ContextPanel implements Component {
187
187
  this.tui.requestRender();
188
188
  }
189
189
  }
190
+
191
+ export class ContextSyncStatusPanel implements Component {
192
+ private readonly tui: TUI;
193
+ private readonly panel: ToolPanel;
194
+ private readonly config: ToolPanelConfig;
195
+ private readonly startedAt = Date.now();
196
+ private readonly timer: ReturnType<typeof setInterval>;
197
+ private lines: string[] = [];
198
+ private lineCount = 0;
199
+
200
+ constructor(tui: TUI, theme: Theme, status: string) {
201
+ this.tui = tui;
202
+ this.config = {
203
+ title: "Context sync",
204
+ body: {
205
+ render: (width) => {
206
+ const visible =
207
+ this.lineCount > 5
208
+ ? [`${this.lineCount - 4} earlier lines omitted`, ...this.lines.slice(-4)]
209
+ : this.lines;
210
+ return visible.map((line, index) =>
211
+ truncateToWidth(theme.fg(index === 0 && this.lineCount > 5 ? "dim" : "muted", line), width, "…"),
212
+ );
213
+ },
214
+ invalidate: () => {},
215
+ },
216
+ footer: { kind: "hints", hints: [] },
217
+ border: "horizontal",
218
+ };
219
+ this.panel = new ToolPanel(theme, this.config);
220
+ this.update(status);
221
+ this.timer = setInterval(() => this.tui.requestRender(), 1_000);
222
+ this.timer.unref();
223
+ }
224
+
225
+ update(status: string): void {
226
+ const added = status
227
+ .split(/\r?\n/)
228
+ .map((line) => line.trim())
229
+ .filter(Boolean);
230
+ this.lineCount += added.length;
231
+ this.lines = [...this.lines, ...added].slice(-5);
232
+ this.tui.requestRender();
233
+ }
234
+
235
+ render(width: number): string[] {
236
+ const elapsed = Math.floor((Date.now() - this.startedAt) / 1_000);
237
+ this.config.title =
238
+ elapsed < 60 ? `Context sync · ${elapsed}s` : `Context sync · ${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
239
+ return this.panel.render(width);
240
+ }
241
+
242
+ invalidate(): void {
243
+ this.panel.invalidate();
244
+ }
245
+
246
+ dispose(): void {
247
+ clearInterval(this.timer);
248
+ }
249
+ }
@@ -4,6 +4,10 @@ import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
4
4
  export default defineTauExtensionSettings({
5
5
  key: "context",
6
6
  defaults: {
7
+ sync: {
8
+ enabled: true as boolean,
9
+ automation: true as boolean,
10
+ },
7
11
  validation: {
8
12
  enabled: false as boolean,
9
13
  ignoreGlobs: [] as string[],
@@ -11,11 +15,36 @@ export default defineTauExtensionSettings({
11
15
  },
12
16
  schema: Type.Object(
13
17
  {
18
+ sync: Type.Optional(
19
+ Type.Object(
20
+ {
21
+ enabled: Type.Optional(
22
+ Type.Boolean({
23
+ default: true,
24
+ description:
25
+ "Master switch for context-sync. When false: no /context-sync, parent cannot call the agent, validation does not auto-run sync.",
26
+ }),
27
+ ),
28
+ automation: Type.Optional(
29
+ Type.Boolean({
30
+ default: true,
31
+ description:
32
+ "When true with sync.enabled, the coding agent may call context-sync. When false, only manual /context-sync (validation auto-run still allowed if validation.enabled).",
33
+ }),
34
+ ),
35
+ },
36
+ { additionalProperties: false },
37
+ ),
38
+ ),
14
39
  validation: Type.Optional(
15
40
  Type.Object(
16
41
  {
17
42
  enabled: Type.Optional(
18
- Type.Boolean({ default: false, description: "Validate context membership after agent turns." }),
43
+ Type.Boolean({
44
+ default: false,
45
+ description:
46
+ "Validate context membership after agent turns and auto-run context-sync on failure (requires sync.enabled).",
47
+ }),
19
48
  ),
20
49
  ignoreGlobs: Type.Optional(
21
50
  Type.Array(Type.String(), {