tau-coding-agent 0.1.6 → 0.2.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 (80) hide show
  1. package/README.md +15 -12
  2. package/extensions/answer.ts +129 -79
  3. package/extensions/branch-term/README.md +7 -0
  4. package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
  5. package/extensions/btw.ts +17 -2
  6. package/extensions/caffeinate/README.md +5 -0
  7. package/extensions/caffeinate/index.ts +144 -0
  8. package/extensions/fast.ts +292 -0
  9. package/extensions/ghostty.ts +214 -211
  10. package/extensions/git-diff-stats.ts +124 -84
  11. package/extensions/git-pr-status.ts +274 -208
  12. package/extensions/insights.ts +109 -191
  13. package/extensions/loop.ts +149 -145
  14. package/extensions/memory.ts +138 -86
  15. package/extensions/notify.ts +14 -26
  16. package/extensions/openai-verbosity.ts +108 -42
  17. package/extensions/review/fix.ts +15 -6
  18. package/extensions/review/git.ts +93 -103
  19. package/extensions/review/index.ts +60 -62
  20. package/extensions/review/interrupt.ts +117 -0
  21. package/extensions/review/message-queue.ts +45 -12
  22. package/extensions/review/models.ts +79 -3
  23. package/extensions/review/prompts.ts +41 -38
  24. package/extensions/review/review.ts +161 -166
  25. package/extensions/review/runner.ts +139 -147
  26. package/extensions/review/runtime.ts +173 -120
  27. package/extensions/review/triage.ts +41 -44
  28. package/extensions/sandbox/bash.ts +775 -0
  29. package/extensions/sandbox/command.ts +605 -0
  30. package/extensions/sandbox/config.ts +764 -0
  31. package/extensions/sandbox/index.ts +132 -3338
  32. package/extensions/sandbox/permissions/dialog.ts +118 -0
  33. package/extensions/sandbox/permissions/filesystem.ts +559 -0
  34. package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
  35. package/extensions/sandbox/permissions/network.ts +164 -0
  36. package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
  37. package/extensions/sandbox/runtime.ts +616 -0
  38. package/extensions/stash.ts +28 -15
  39. package/extensions/subagent/README.md +73 -0
  40. package/extensions/subagent/index.ts +822 -0
  41. package/extensions/subagent/interrupt.ts +117 -0
  42. package/extensions/subagent/permissions.ts +101 -0
  43. package/extensions/subagent/rpc.ts +177 -0
  44. package/extensions/tool-display-mode.ts +267 -64
  45. package/extensions/usage/index.ts +249 -356
  46. package/extensions/usage/openrouter.ts +10 -2
  47. package/extensions/websearch/README.md +12 -47
  48. package/extensions/websearch/config.ts +5 -2
  49. package/extensions/websearch/index.ts +94 -109
  50. package/extensions/websearch/output.ts +51 -0
  51. package/extensions/websearch/providers/anthropic.pi.ts +27 -44
  52. package/extensions/websearch/providers/gemini.browser.ts +68 -77
  53. package/extensions/websearch/providers/gemini.pi.ts +17 -17
  54. package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
  55. package/extensions/websearch/providers/pi-model.shared.ts +33 -39
  56. package/extensions/websearch/providers/shared.ts +87 -2
  57. package/extensions/websearch/types.ts +0 -3
  58. package/extensions/worktree.ts +132 -172
  59. package/package.json +10 -5
  60. package/skills/browser-tools/SKILL.md +29 -234
  61. package/skills/browser-tools/references/cookies.md +36 -0
  62. package/skills/browser-tools/references/interaction.md +90 -0
  63. package/skills/browser-tools/references/logging.md +34 -0
  64. package/skills/git-clean-history/SKILL.md +6 -6
  65. package/skills/git-commit/SKILL.md +5 -3
  66. package/skills/github-pull-request/SKILL.md +60 -0
  67. package/skills/github-pull-request/references/create.md +40 -0
  68. package/skills/github-pull-request/references/stewardship.md +60 -0
  69. package/skills/oracle/SKILL.md +3 -3
  70. package/skills/oracle/scripts/oracle +10 -1
  71. package/skills/sentry/SKILL.md +15 -185
  72. package/skills/sentry/references/events.md +79 -0
  73. package/skills/sentry/references/issues.md +62 -0
  74. package/skills/sentry/references/logs.md +46 -0
  75. package/skills/update-changelog/SKILL.md +27 -121
  76. package/skills/web-design/SKILL.md +16 -105
  77. package/themes/tau-dark.json +4 -0
  78. package/extensions/openai-fast.ts +0 -229
  79. package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
  80. package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
@@ -12,6 +12,8 @@ import path from "node:path";
12
12
  const STATUS_KEY = "1-git-diff-stats";
13
13
  const REFRESH_DEBOUNCE_MS = 250;
14
14
  const WRITE_REFRESH_DEBOUNCE_MS = 1_000;
15
+ const GIT_KILL_GRACE_MS = 500;
16
+ const GIT_PIPE_GRACE_MS = 100;
15
17
 
16
18
  type DiffStats = {
17
19
  added: number;
@@ -26,7 +28,9 @@ function isInsideGitRepo(startDir: string): boolean {
26
28
  }
27
29
 
28
30
  function mergeNumstatEntries(output: string, statsByPath: Map<string, DiffStats>): void {
29
- for (const line of output.split("\n")) {
31
+ const records = output.split("\0");
32
+ for (let index = 0; index < records.length; index++) {
33
+ const line = records[index];
30
34
  if (!line) continue;
31
35
 
32
36
  const firstTab = line.indexOf("\t");
@@ -35,8 +39,13 @@ function mergeNumstatEntries(output: string, statsByPath: Map<string, DiffStats>
35
39
  const secondTab = line.indexOf("\t", firstTab + 1);
36
40
  if (secondTab === -1) continue;
37
41
 
38
- const filePath = line.slice(secondTab + 1);
39
- if (statsByPath.has(filePath)) continue;
42
+ let filePath = line.slice(secondTab + 1);
43
+ if (!filePath) {
44
+ // Renames carry separate old and new paths after the numstat record.
45
+ index += 2;
46
+ filePath = records[index];
47
+ }
48
+ if (!filePath || statsByPath.has(filePath)) continue;
40
49
 
41
50
  const addedToken = line.slice(0, firstTab);
42
51
  const removedToken = line.slice(firstTab + 1, secondTab);
@@ -50,22 +59,48 @@ function mergeNumstatEntries(output: string, statsByPath: Map<string, DiffStats>
50
59
  async function gitText(
51
60
  cwd: string,
52
61
  args: string[],
62
+ signal: AbortSignal,
53
63
  options?: {
54
64
  env?: NodeJS.ProcessEnv;
55
65
  stdin?: string;
66
+ allowExitCode?: number;
56
67
  },
57
68
  ): Promise<string> {
69
+ signal.throwIfAborted();
58
70
  return new Promise((resolve, reject) => {
59
71
  const child = spawn("git", args, {
60
72
  cwd,
73
+ detached: true,
61
74
  stdio: ["pipe", "pipe", "pipe"],
62
75
  env: options?.env ? { ...process.env, ...options.env } : process.env,
63
76
  });
64
77
 
65
78
  let stdout = "";
66
79
  let stderr = "";
67
-
68
- child.on("error", reject);
80
+ let processError: Error | undefined;
81
+ let killTimer: NodeJS.Timeout | undefined;
82
+ let pipeTimer: NodeJS.Timeout | undefined;
83
+ let escalated = false;
84
+ const releaseOrphanedPipes = () => {
85
+ if (!escalated || pipeTimer || (child.exitCode === null && child.signalCode === null)) return;
86
+ pipeTimer = setTimeout(() => {
87
+ child.stdin?.destroy();
88
+ child.stdout?.destroy();
89
+ child.stderr?.destroy();
90
+ }, GIT_PIPE_GRACE_MS);
91
+ };
92
+ const onAbort = () => {
93
+ killProcessGroup(child, "SIGTERM");
94
+ killTimer = setTimeout(() => {
95
+ killProcessGroup(child, "SIGKILL");
96
+ escalated = true;
97
+ releaseOrphanedPipes();
98
+ }, GIT_KILL_GRACE_MS);
99
+ };
100
+
101
+ child.on("error", (error) => {
102
+ processError = error;
103
+ });
69
104
  child.stdin?.on("error", () => {});
70
105
  child.stdout?.on("data", (chunk: Buffer) => {
71
106
  stdout += chunk.toString("utf8");
@@ -73,25 +108,56 @@ async function gitText(
73
108
  child.stderr?.on("data", (chunk: Buffer) => {
74
109
  stderr += chunk.toString("utf8");
75
110
  });
111
+ // Drain normally, but don't let a detached daemon retain cancelled work after Git exits.
112
+ child.once("exit", releaseOrphanedPipes);
76
113
  child.on("close", (code) => {
114
+ clearTimeout(killTimer);
115
+ clearTimeout(pipeTimer);
116
+ child.removeListener("exit", releaseOrphanedPipes);
117
+ signal.removeEventListener("abort", onAbort);
118
+ if (signal.aborted) {
119
+ killProcessGroup(child, "SIGKILL");
120
+ reject(signal.reason);
121
+ return;
122
+ }
123
+ if (processError) {
124
+ reject(processError);
125
+ return;
126
+ }
77
127
  const exitCode = code ?? -1;
78
- if (exitCode !== 0) {
128
+ if (exitCode !== 0 && exitCode !== options?.allowExitCode) {
79
129
  const details = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
80
130
  reject(new Error(details || `git ${args.join(" ")} failed`));
81
131
  return;
82
132
  }
83
-
84
- resolve(stdout.trim());
133
+ resolve(stdout);
85
134
  });
86
135
 
136
+ if (signal.aborted) onAbort();
137
+ else signal.addEventListener("abort", onAbort, { once: true });
87
138
  child.stdin?.end(options?.stdin);
88
139
  });
89
140
  }
90
141
 
91
- async function computeLocalStats(cwd: string): Promise<DiffStats | undefined> {
142
+ function killProcessGroup(child: ReturnType<typeof spawn>, signal: NodeJS.Signals): void {
143
+ if (!child.pid) return;
144
+ try {
145
+ process.kill(-child.pid, signal);
146
+ } catch {
147
+ try {
148
+ child.kill(signal);
149
+ } catch {
150
+ // Process likely already exited.
151
+ }
152
+ }
153
+ }
154
+
155
+ async function computeLocalStats(cwd: string, signal: AbortSignal): Promise<DiffStats | undefined> {
92
156
  if (!isInsideGitRepo(cwd)) return undefined;
93
157
 
94
- const gitDir = await gitText(cwd, ["rev-parse", "--path-format=absolute", "--git-dir"]);
158
+ const gitDir = (
159
+ await gitText(cwd, ["rev-parse", "--path-format=absolute", "--git-dir"], signal)
160
+ ).replace(/\n$/, "");
95
161
  const tempDir = await mkdtemp(path.join(gitDir, "pi-git-diff-stats-"));
96
162
  const tempIndex = path.join(tempDir, "index");
97
163
  const realIndex = path.join(gitDir, "index");
@@ -104,48 +170,27 @@ async function computeLocalStats(cwd: string): Promise<DiffStats | undefined> {
104
170
  if (code !== "ENOENT") throw error;
105
171
  }
106
172
 
107
- const addIntent = gitText(cwd, ["add", "-N", "--all"], { env: { GIT_INDEX_FILE: tempIndex } });
108
- const head = spawn("git", ["rev-parse", "--verify", "--quiet", "HEAD"], {
109
- cwd,
110
- stdio: ["ignore", "pipe", "pipe"],
111
- });
112
-
113
- let headStdout = "";
114
- let headStderr = "";
115
- head.stdout?.on("data", (chunk: Buffer) => {
116
- headStdout += chunk.toString("utf8");
117
- });
118
- head.stderr?.on("data", (chunk: Buffer) => {
119
- headStderr += chunk.toString("utf8");
120
- });
121
-
122
- const headOidPromise = new Promise<string | undefined>((resolve, reject) => {
123
- head.on("error", reject);
124
- head.on("close", (code) => {
125
- if (code === 0) {
126
- resolve(headStdout.trim());
127
- return;
128
- }
129
- if (code === 1) {
130
- resolve(undefined);
131
- return;
132
- }
133
-
134
- const details = [headStdout.trim(), headStderr.trim()].filter(Boolean).join("\n");
135
- reject(new Error(details || "git rev-parse --verify --quiet HEAD failed"));
136
- });
137
- });
138
-
139
- const [, headOid] = await Promise.all([addIntent, headOidPromise]);
173
+ const headOid = (
174
+ await gitText(cwd, ["rev-parse", "--verify", "--quiet", "HEAD"], signal, {
175
+ allowExitCode: 1,
176
+ })
177
+ ).trim();
140
178
  const baseOid =
141
- headOid ?? (await gitText(cwd, ["hash-object", "-t", "tree", "--stdin"], { stdin: "" }));
179
+ headOid ||
180
+ (await gitText(cwd, ["hash-object", "-t", "tree", "--stdin"], signal, { stdin: "" })).trim();
142
181
 
143
- const [workingTreeDiff, stagedDiff] = await Promise.all([
144
- gitText(cwd, ["diff", "--numstat", baseOid, "--"], { env: { GIT_INDEX_FILE: tempIndex } }),
145
- gitText(cwd, ["diff", "--cached", "--numstat", baseOid, "--"], {
182
+ const stagedDiff = await gitText(
183
+ cwd,
184
+ ["diff", "--cached", "--numstat", "-z", baseOid, "--"],
185
+ signal,
186
+ {
146
187
  env: { GIT_INDEX_FILE: tempIndex },
147
- }),
148
- ]);
188
+ },
189
+ );
190
+ await gitText(cwd, ["add", "-N", "--all"], signal, { env: { GIT_INDEX_FILE: tempIndex } });
191
+ const workingTreeDiff = await gitText(cwd, ["diff", "--numstat", "-z", baseOid, "--"], signal, {
192
+ env: { GIT_INDEX_FILE: tempIndex },
193
+ });
149
194
 
150
195
  const statsByPath = new Map<string, DiffStats>();
151
196
  mergeNumstatEntries(workingTreeDiff, statsByPath);
@@ -172,15 +217,23 @@ export default function gitDiffStatsExtension(pi: ExtensionAPI) {
172
217
  let refreshTimer: NodeJS.Timeout | undefined;
173
218
  let refreshInFlight: Promise<void> | null = null;
174
219
  let refreshQueued = false;
220
+ let refreshController: AbortController | undefined;
175
221
 
176
- function reset(nextCtx?: ExtensionContext): void {
177
- clearStatus();
178
- ctx = nextCtx;
179
- stats = undefined;
180
- generation += 1;
181
- resetRefreshState();
182
-
183
- if (nextCtx?.hasUI) scheduleRefresh(0);
222
+ async function reset(nextCtx?: ExtensionContext): Promise<void> {
223
+ const resetGeneration = ++generation;
224
+ if (refreshTimer) clearTimeout(refreshTimer);
225
+ refreshTimer = undefined;
226
+ refreshQueued = false;
227
+ const pending = refreshInFlight;
228
+ refreshController?.abort();
229
+ try {
230
+ clearStatus();
231
+ ctx = nextCtx;
232
+ stats = undefined;
233
+ } finally {
234
+ await pending;
235
+ }
236
+ if (generation === resetGeneration && nextCtx?.hasUI) scheduleRefresh(0);
184
237
  }
185
238
 
186
239
  function clearStatus(): void {
@@ -197,13 +250,6 @@ export default function gitDiffStatsExtension(pi: ExtensionAPI) {
197
250
  );
198
251
  }
199
252
 
200
- function resetRefreshState(): void {
201
- if (refreshTimer) clearTimeout(refreshTimer);
202
- refreshTimer = undefined;
203
- refreshInFlight = null;
204
- refreshQueued = false;
205
- }
206
-
207
253
  function scheduleRefresh(delay = REFRESH_DEBOUNCE_MS): void {
208
254
  const activeCtx = ctx;
209
255
  if (!activeCtx?.hasUI) return;
@@ -214,19 +260,21 @@ export default function gitDiffStatsExtension(pi: ExtensionAPI) {
214
260
  const refreshGeneration = generation;
215
261
  refreshTimer = setTimeout(() => {
216
262
  refreshTimer = undefined;
217
- void refresh(cwd, refreshGeneration);
263
+ refresh(cwd, refreshGeneration);
218
264
  }, delay);
219
265
  }
220
266
 
221
- async function refresh(cwd: string, refreshGeneration: number): Promise<void> {
267
+ function refresh(cwd: string, refreshGeneration: number): void {
222
268
  if (refreshInFlight) {
223
269
  refreshQueued = true;
224
270
  return;
225
271
  }
226
272
 
227
- const promise = (async () => {
273
+ const controller = new AbortController();
274
+ refreshController = controller;
275
+ refreshInFlight = (async () => {
228
276
  try {
229
- const nextStats = await computeLocalStats(cwd);
277
+ const nextStats = await computeLocalStats(cwd, controller.signal);
230
278
  if (generation !== refreshGeneration) return;
231
279
 
232
280
  stats = nextStats;
@@ -236,26 +284,18 @@ export default function gitDiffStatsExtension(pi: ExtensionAPI) {
236
284
 
237
285
  stats = undefined;
238
286
  clearStatus();
239
- }
240
- })();
241
-
242
- refreshInFlight = promise;
243
-
244
- try {
245
- await promise;
246
- } finally {
247
- if (refreshInFlight === promise) {
287
+ } finally {
248
288
  refreshInFlight = null;
249
- if (refreshQueued) {
250
- refreshQueued = false;
251
- scheduleRefresh(0);
252
- }
289
+ refreshController = undefined;
290
+ const queued = refreshQueued;
291
+ refreshQueued = false;
292
+ if (queued && generation === refreshGeneration) scheduleRefresh(0);
253
293
  }
254
- }
294
+ })();
255
295
  }
256
296
 
257
297
  pi.on("session_start", async (_event, nextCtx) => {
258
- reset(nextCtx);
298
+ await reset(nextCtx);
259
299
  });
260
300
 
261
301
  pi.on("tool_result", async (event, nextCtx) => {
@@ -272,6 +312,6 @@ export default function gitDiffStatsExtension(pi: ExtensionAPI) {
272
312
  });
273
313
 
274
314
  pi.on("session_shutdown", async () => {
275
- reset(undefined);
315
+ await reset(undefined);
276
316
  });
277
317
  }