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
@@ -0,0 +1,117 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ getKeybindings,
4
+ isKeyRelease,
5
+ type TuiMainScreen,
6
+ type Component,
7
+ type TUI,
8
+ } from "@earendil-works/pi-tui";
9
+
10
+ interface InterruptRequest {
11
+ sessionKey: string;
12
+ cancel?: boolean;
13
+ active: boolean;
14
+ confirming?: boolean;
15
+ }
16
+
17
+ /** Each extension owns its guard; optional peers contribute work through the event bus. */
18
+ export function createInterruptGuard(
19
+ pi: ExtensionAPI,
20
+ ctx: ExtensionContext,
21
+ tui: TUI & Partial<Pick<TuiMainScreen, "getFocusedComponent">>,
22
+ isWorking: () => boolean,
23
+ cancel: () => void,
24
+ isPromptActive: () => boolean,
25
+ ) {
26
+ const sessionKey =
27
+ ctx.sessionManager.getSessionFile() ?? `session:${ctx.sessionManager.getSessionId()}`;
28
+ let closed = false;
29
+ let pending:
30
+ | { controller: AbortController; editor: Component; completion: Promise<void> }
31
+ | undefined;
32
+ const unsubscribeWork = pi.events.on("tau:interrupt", (data) => {
33
+ const request = data as InterruptRequest;
34
+ if (request.sessionKey !== sessionKey || closed) return;
35
+ if (pending) request.confirming = true;
36
+ if (!isWorking()) return;
37
+ request.active = true;
38
+ if (request.cancel) cancel();
39
+ });
40
+ const unsubscribeChanged = pi.events.on("tau:work-changed", (data) => {
41
+ if ((data as { sessionKey: string }).sessionKey === sessionKey && pending && !hasWork())
42
+ pending.controller.abort();
43
+ });
44
+ const unsubscribeInput = ctx.ui.onTerminalInput((data) => {
45
+ if (isKeyRelease(data) || !getKeybindings().matches(data, "app.interrupt")) return;
46
+ const editor = tui.getFocusedComponent?.();
47
+ // A dismissed native dialog restores focus before its promise settles. Swallow a rapid
48
+ // extra Escape in that gap instead of accidentally interrupting the parent.
49
+ if (pending) return editor === pending.editor ? { consume: true } : undefined;
50
+ if (
51
+ closed ||
52
+ isPromptActive() ||
53
+ !editor ||
54
+ !("getText" in editor) ||
55
+ !("setText" in editor) ||
56
+ ("isShowingAutocomplete" in editor &&
57
+ typeof editor.isShowingAutocomplete === "function" &&
58
+ editor.isShowingAutocomplete()) ||
59
+ !hasWork()
60
+ )
61
+ return;
62
+ const current = {
63
+ controller: new AbortController(),
64
+ editor,
65
+ completion: Promise.resolve(),
66
+ };
67
+ pending = current;
68
+ current.completion = (async () => {
69
+ try {
70
+ const request: {
71
+ run: (signal: AbortSignal) => Promise<void>;
72
+ signal: AbortSignal;
73
+ result?: Promise<unknown>;
74
+ } = {
75
+ signal: current.controller.signal,
76
+ async run(signal) {
77
+ const confirmed = await ctx.ui.confirm("Cancel all ongoing work?", "", { signal });
78
+ if (!confirmed || closed || signal.aborted || !hasWork()) return;
79
+ pi.events.emit("tau:interrupt", { sessionKey, active: false, cancel: true });
80
+ // Reuse the editor's native interrupt, including queue restoration, Bash and retries.
81
+ editor.handleInput?.(data);
82
+ },
83
+ };
84
+ // Keep queued approvals blocked until the confirmed cancellation has been applied.
85
+ pi.events.emit("subagent:permission", request);
86
+ await (request.result ?? request.run(request.signal));
87
+ } catch (error) {
88
+ if (!closed && !current.controller.signal.aborted)
89
+ ctx.ui.notify(`Could not confirm cancellation: ${String(error)}`, "warning");
90
+ } finally {
91
+ if (pending === current) pending = undefined;
92
+ }
93
+ })();
94
+ return { consume: true };
95
+ });
96
+
97
+ return {
98
+ refresh() {
99
+ if (!closed) pi.events.emit("tau:work-changed", { sessionKey });
100
+ },
101
+ async dispose() {
102
+ closed = true;
103
+ unsubscribeInput();
104
+ unsubscribeWork();
105
+ unsubscribeChanged();
106
+ pending?.controller.abort();
107
+ pi.events.emit("tau:work-changed", { sessionKey });
108
+ await pending?.completion;
109
+ },
110
+ };
111
+
112
+ function hasWork(): boolean {
113
+ const request: InterruptRequest = { sessionKey, active: false };
114
+ pi.events.emit("tau:interrupt", request);
115
+ return request.active;
116
+ }
117
+ }
@@ -0,0 +1,101 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ /** Optional integration for sharing the parent's approval and confirmation queue. */
4
+ interface PermissionRequest {
5
+ run: (signal: AbortSignal) => Promise<unknown>;
6
+ signal?: AbortSignal;
7
+ result?: Promise<unknown>;
8
+ }
9
+
10
+ export function registerPermissions(pi: ExtensionAPI) {
11
+ let queue = new PermissionQueue();
12
+ pi.events.on("subagent:permission", (data) => {
13
+ const request = data as PermissionRequest;
14
+ request.result = queue.run(request.run, request.signal);
15
+ });
16
+ pi.on("ui_prompt_start", () => queue.setPromptActive(true));
17
+ pi.on("ui_prompt_end", () => queue.setPromptActive(false));
18
+ return {
19
+ get active() {
20
+ return queue.active;
21
+ },
22
+ run<T>(run: (signal: AbortSignal) => Promise<T>, signal: AbortSignal) {
23
+ return queue.run(run, signal);
24
+ },
25
+ async reset() {
26
+ await queue.close();
27
+ queue = new PermissionQueue();
28
+ },
29
+ close: () => queue.close(),
30
+ };
31
+ }
32
+
33
+ /** Cancel queued requests independently; stopping one child must not wait for another child's user. */
34
+ class PermissionQueue {
35
+ private readonly controller = new AbortController();
36
+ private readonly pending: (() => Promise<void>)[] = [];
37
+ private current?: Promise<void>;
38
+ private promptActive = false;
39
+
40
+ get active(): boolean {
41
+ return this.promptActive || this.current !== undefined;
42
+ }
43
+
44
+ run<T>(run: (signal: AbortSignal) => Promise<T>, signal?: AbortSignal): Promise<T> {
45
+ const combined = signal
46
+ ? AbortSignal.any([signal, this.controller.signal])
47
+ : this.controller.signal;
48
+ return new Promise<T>((resolve, reject) => {
49
+ if (combined.aborted) {
50
+ reject(combined.reason);
51
+ return;
52
+ }
53
+ const start = async () => {
54
+ try {
55
+ combined.throwIfAborted();
56
+ resolve(await run(combined));
57
+ } catch (error) {
58
+ reject(error);
59
+ } finally {
60
+ combined.removeEventListener("abort", cancel);
61
+ }
62
+ };
63
+ const cancel = () => {
64
+ const index = this.pending.indexOf(start);
65
+ if (index === -1) return;
66
+ this.pending.splice(index, 1);
67
+ combined.removeEventListener("abort", cancel);
68
+ reject(combined.reason);
69
+ };
70
+ combined.addEventListener("abort", cancel, { once: true });
71
+ this.pending.push(start);
72
+ this.advance();
73
+ });
74
+ }
75
+
76
+ setPromptActive(active: boolean): void {
77
+ this.promptActive = active;
78
+ this.advance();
79
+ }
80
+
81
+ async close(): Promise<void> {
82
+ this.controller.abort();
83
+ await this.current;
84
+ }
85
+
86
+ private advance(): void {
87
+ if (this.current || this.promptActive || this.controller.signal.aborted) return;
88
+ const start = this.pending.shift();
89
+ if (!start) return;
90
+ let begin!: () => void;
91
+ this.current = new Promise<void>((resolve, reject) => {
92
+ begin = () => {
93
+ void start().then(resolve, reject);
94
+ };
95
+ }).finally(() => {
96
+ this.current = undefined;
97
+ this.advance();
98
+ });
99
+ begin();
100
+ }
101
+ }
@@ -0,0 +1,177 @@
1
+ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2
+ import type {
3
+ JsonAgentSessionEvent,
4
+ RpcCommand,
5
+ RpcExtensionUIRequest,
6
+ RpcExtensionUIResponse,
7
+ RpcResponse,
8
+ } from "@earendil-works/pi-coding-agent";
9
+
10
+ export type ChildEvent = JsonAgentSessionEvent | RpcExtensionUIRequest;
11
+
12
+ /** Own the native RPC process, its LF-delimited protocol, and all pending requests through close. */
13
+ export class SubagentProcess {
14
+ private readonly process: ChildProcessWithoutNullStreams;
15
+ private readonly pending = new Map<
16
+ string,
17
+ {
18
+ command: RpcCommand["type"];
19
+ resolve: (response: RpcResponse) => void;
20
+ reject: (error: Error) => void;
21
+ timer: ReturnType<typeof setTimeout>;
22
+ }
23
+ >();
24
+ private sequence = 0;
25
+ private buffer = "";
26
+ private stderr = "";
27
+ private failure?: Error;
28
+ private stopping = false;
29
+ private closed = false;
30
+ private escalation?: ReturnType<typeof setTimeout>;
31
+ private shutdownRequest?: Promise<void>;
32
+ readonly completion: Promise<void>;
33
+
34
+ constructor(
35
+ cwd: string,
36
+ args: string[],
37
+ env: NodeJS.ProcessEnv,
38
+ onEvent: (event: ChildEvent) => void,
39
+ onClose: (error?: Error) => void,
40
+ ) {
41
+ this.process = spawn("pi", ["--mode", "rpc", ...args], {
42
+ cwd,
43
+ env,
44
+ detached: true,
45
+ stdio: ["pipe", "pipe", "pipe"],
46
+ });
47
+ this.process.stdout.setEncoding("utf8");
48
+ this.process.stderr.setEncoding("utf8");
49
+ this.process.stdout.on("data", (chunk: string) => {
50
+ try {
51
+ this.buffer += chunk;
52
+ let newline: number;
53
+ while ((newline = this.buffer.indexOf("\n")) !== -1) {
54
+ const line = this.buffer.slice(0, newline).replace(/\r$/, "");
55
+ this.buffer = this.buffer.slice(newline + 1);
56
+ if (!line) continue;
57
+ if (Buffer.byteLength(line) > 32 * 1024 * 1024)
58
+ throw new Error("Subagent RPC record exceeds 32 MiB");
59
+ const event = JSON.parse(line) as RpcResponse | ChildEvent;
60
+ if (!event || typeof event !== "object" || typeof event.type !== "string") {
61
+ throw new Error("Invalid subagent RPC record");
62
+ }
63
+ if (event.type === "response") {
64
+ const request = event.id ? this.pending.get(event.id) : undefined;
65
+ if (!request) continue;
66
+ if (event.command !== request.command || typeof event.success !== "boolean")
67
+ throw new Error("Invalid subagent RPC acknowledgement");
68
+ clearTimeout(request.timer);
69
+ this.pending.delete(event.id!);
70
+ if (event.success) request.resolve(event);
71
+ else request.reject(new Error(event.error));
72
+ } else if (!this.stopping) {
73
+ onEvent(event);
74
+ }
75
+ }
76
+ if (Buffer.byteLength(this.buffer) > 32 * 1024 * 1024)
77
+ throw new Error("Subagent RPC record exceeds 32 MiB");
78
+ } catch (error) {
79
+ this.fail(error);
80
+ }
81
+ });
82
+ this.process.stderr.on("data", (chunk: string) => {
83
+ this.stderr = (this.stderr + chunk).slice(-8192);
84
+ });
85
+ this.process.on("error", (error) => this.fail(error));
86
+ this.process.stdin.on("error", (error) => this.fail(error));
87
+ this.completion = new Promise<void>((resolve) => {
88
+ this.process.once("close", (code, signal) => {
89
+ this.closed = true;
90
+ clearTimeout(this.escalation);
91
+ const error =
92
+ this.failure ??
93
+ new Error(
94
+ `Subagent exited (${signal ?? code}).${this.stderr.trim() ? ` ${this.stderr.trim()}` : ""}`,
95
+ );
96
+ for (const request of this.pending.values()) {
97
+ clearTimeout(request.timer);
98
+ request.reject(error);
99
+ }
100
+ this.pending.clear();
101
+ try {
102
+ onClose(this.stopping && !this.failure ? undefined : error);
103
+ } finally {
104
+ resolve();
105
+ }
106
+ });
107
+ });
108
+ }
109
+
110
+ async request(command: RpcCommand): Promise<RpcResponse> {
111
+ if (this.closed || this.stopping) throw this.failure ?? new Error("Subagent has stopped");
112
+ return this.send(command);
113
+ }
114
+
115
+ respond(response: RpcExtensionUIResponse): void {
116
+ if (!this.closed && !this.stopping) this.write(response);
117
+ }
118
+
119
+ async stop(): Promise<void> {
120
+ if (!this.closed && !this.stopping) {
121
+ this.stopping = true;
122
+ let terminating = false;
123
+ const terminate = () => {
124
+ if (terminating || this.closed) return;
125
+ terminating = true;
126
+ clearTimeout(this.escalation);
127
+ // Pi's SIGTERM handler also stops separately detached Bash children.
128
+ this.signal("SIGTERM");
129
+ this.escalation = setTimeout(() => this.signal("SIGKILL"), 1000);
130
+ };
131
+ this.escalation = setTimeout(terminate, 250);
132
+ this.shutdownRequest = (
133
+ this.failure
134
+ ? Promise.resolve()
135
+ : this.send({ type: "clear_queue" }).then(() => this.send({ type: "abort" }))
136
+ ).then(terminate, terminate);
137
+ }
138
+ await this.completion;
139
+ await this.shutdownRequest;
140
+ }
141
+
142
+ private send(command: RpcCommand): Promise<RpcResponse> {
143
+ if (this.closed) return Promise.reject(this.failure ?? new Error("Subagent has stopped"));
144
+ const id = String(++this.sequence);
145
+ return new Promise<RpcResponse>((resolve, reject) => {
146
+ const timer = setTimeout(
147
+ () =>
148
+ this.fail(new Error(`Subagent did not acknowledge ${command.type} within 30 seconds`)),
149
+ 30_000,
150
+ );
151
+ this.pending.set(id, { command: command.type, resolve, reject, timer });
152
+ this.write({ ...command, id });
153
+ });
154
+ }
155
+
156
+ private write(message: RpcCommand | RpcExtensionUIResponse): void {
157
+ try {
158
+ this.process.stdin.write(`${JSON.stringify(message)}\n`);
159
+ } catch (error) {
160
+ this.fail(error);
161
+ }
162
+ }
163
+
164
+ private fail(error: unknown): void {
165
+ this.failure ??= error instanceof Error ? error : new Error(String(error));
166
+ void this.stop();
167
+ }
168
+
169
+ private signal(signal: NodeJS.Signals): void {
170
+ if (this.closed) return;
171
+ try {
172
+ if (this.process.pid) process.kill(-this.process.pid, signal);
173
+ } catch (error) {
174
+ if ((error as NodeJS.ErrnoException).code !== "ESRCH") this.process.kill(signal);
175
+ }
176
+ }
177
+ }