tau-coding-agent 0.1.5 → 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.
- package/README.md +15 -12
- package/extensions/answer.ts +129 -73
- package/extensions/branch-term/README.md +7 -0
- package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
- package/extensions/btw.ts +90 -4
- package/extensions/caffeinate/README.md +5 -0
- package/extensions/caffeinate/index.ts +144 -0
- package/extensions/fast.ts +292 -0
- package/extensions/ghostty.ts +214 -211
- package/extensions/git-diff-stats.ts +124 -84
- package/extensions/git-pr-status.ts +274 -208
- package/extensions/insights.ts +138 -183
- package/extensions/loop.ts +150 -135
- package/extensions/memory.ts +172 -97
- package/extensions/notify.ts +14 -26
- package/extensions/openai-verbosity.ts +108 -42
- package/extensions/review/fix.ts +15 -6
- package/extensions/review/git.ts +93 -103
- package/extensions/review/index.ts +60 -62
- package/extensions/review/interrupt.ts +117 -0
- package/extensions/review/message-queue.ts +45 -12
- package/extensions/review/models.ts +79 -3
- package/extensions/review/prompts.ts +43 -40
- package/extensions/review/review.ts +161 -166
- package/extensions/review/runner.ts +139 -147
- package/extensions/review/runtime.ts +173 -120
- package/extensions/review/submit-review-tool.ts +1 -5
- package/extensions/review/submit-triage-tool.ts +1 -0
- package/extensions/review/triage.ts +41 -44
- package/extensions/sandbox/bash.ts +775 -0
- package/extensions/sandbox/command.ts +605 -0
- package/extensions/sandbox/config.ts +764 -0
- package/extensions/sandbox/index.ts +138 -2850
- package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
- package/extensions/sandbox/permissions/dialog.ts +118 -0
- package/extensions/sandbox/permissions/filesystem.ts +559 -0
- package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
- package/extensions/sandbox/permissions/network.ts +164 -0
- package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
- package/extensions/sandbox/runtime.ts +616 -0
- package/extensions/stash.ts +28 -15
- package/extensions/subagent/README.md +73 -0
- package/extensions/subagent/index.ts +822 -0
- package/extensions/subagent/interrupt.ts +117 -0
- package/extensions/subagent/permissions.ts +101 -0
- package/extensions/subagent/rpc.ts +177 -0
- package/extensions/tool-display-mode.ts +267 -64
- package/extensions/usage/anthropic.ts +5 -6
- package/extensions/usage/github-copilot.ts +30 -9
- package/extensions/usage/index.ts +250 -357
- package/extensions/usage/openai-codex.ts +2 -2
- package/extensions/usage/openrouter.ts +10 -2
- package/extensions/websearch/README.md +12 -47
- package/extensions/websearch/config.ts +5 -2
- package/extensions/websearch/index.ts +94 -109
- package/extensions/websearch/output.ts +51 -0
- package/extensions/websearch/providers/anthropic.pi.ts +27 -44
- package/extensions/websearch/providers/gemini.browser.ts +68 -77
- package/extensions/websearch/providers/gemini.pi.ts +17 -17
- package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
- package/extensions/websearch/providers/pi-model.shared.ts +44 -32
- package/extensions/websearch/providers/shared.ts +87 -2
- package/extensions/websearch/types.ts +0 -3
- package/extensions/worktree.ts +132 -172
- package/package.json +10 -5
- package/skills/browser-tools/SKILL.md +29 -234
- package/skills/browser-tools/references/cookies.md +36 -0
- package/skills/browser-tools/references/interaction.md +90 -0
- package/skills/browser-tools/references/logging.md +34 -0
- package/skills/git-clean-history/SKILL.md +6 -6
- package/skills/git-commit/SKILL.md +5 -3
- package/skills/github-pull-request/SKILL.md +60 -0
- package/skills/github-pull-request/references/create.md +40 -0
- package/skills/github-pull-request/references/stewardship.md +60 -0
- package/skills/oracle/SKILL.md +4 -4
- package/skills/oracle/scripts/oracle +64 -51
- package/skills/sentry/SKILL.md +15 -185
- package/skills/sentry/references/events.md +79 -0
- package/skills/sentry/references/issues.md +62 -0
- package/skills/sentry/references/logs.md +46 -0
- package/skills/update-changelog/SKILL.md +27 -121
- package/skills/web-design/SKILL.md +16 -105
- package/themes/tau-dark.json +4 -0
- package/extensions/openai-fast.ts +0 -229
- package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
- package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, open, readFile, realpath, rename, unlink } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import { lock } from "proper-lockfile";
|
|
3
5
|
import {
|
|
4
6
|
getAgentDir,
|
|
5
7
|
type ExtensionAPI,
|
|
@@ -7,7 +9,8 @@ import {
|
|
|
7
9
|
} from "@earendil-works/pi-coding-agent";
|
|
8
10
|
|
|
9
11
|
const STATUS_KEY = "openai-verbosity";
|
|
10
|
-
const
|
|
12
|
+
const CHAT_COMPLETIONS_API = "openai-completions";
|
|
13
|
+
const RESPONSES_APIS = new Set([
|
|
11
14
|
"openai-responses",
|
|
12
15
|
"openai-codex-responses",
|
|
13
16
|
"azure-openai-responses",
|
|
@@ -18,11 +21,11 @@ type VerbositySetting = Verbosity | "auto";
|
|
|
18
21
|
type JsonObject = Record<string, unknown>;
|
|
19
22
|
type ModelInfo = NonNullable<ExtensionContext["model"]>;
|
|
20
23
|
type VerbosityConfig = {
|
|
21
|
-
models: Record<string,
|
|
24
|
+
models: Record<string, VerbositySetting>;
|
|
22
25
|
};
|
|
23
26
|
|
|
24
27
|
function emptyConfig(): VerbosityConfig {
|
|
25
|
-
return { models:
|
|
28
|
+
return { models: Object.create(null) };
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
function getConfigPath(): string {
|
|
@@ -61,12 +64,13 @@ function getVerbosityArgumentCompletions(
|
|
|
61
64
|
function parseConfig(value: unknown): VerbosityConfig {
|
|
62
65
|
if (!isObject(value) || !isObject(value.models)) return emptyConfig();
|
|
63
66
|
|
|
64
|
-
const models: Record<string,
|
|
67
|
+
const models: Record<string, VerbositySetting> = Object.create(null);
|
|
65
68
|
for (const [key, rawVerbosity] of Object.entries(value.models)) {
|
|
66
69
|
const normalizedKey = key.trim();
|
|
67
70
|
if (!normalizedKey || typeof rawVerbosity !== "string") continue;
|
|
71
|
+
if (key !== normalizedKey && Object.hasOwn(value.models, normalizedKey)) continue;
|
|
68
72
|
|
|
69
|
-
const verbosity =
|
|
73
|
+
const verbosity = parseVerbositySetting(rawVerbosity);
|
|
70
74
|
if (!verbosity) continue;
|
|
71
75
|
|
|
72
76
|
models[normalizedKey] = verbosity;
|
|
@@ -90,14 +94,82 @@ async function loadConfig(): Promise<VerbosityConfig> {
|
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
|
|
97
|
+
// Resolve the target before locking and renaming: aliases must share a lock, not lose their symlink.
|
|
98
|
+
async function resolveConfigPath(): Promise<string> {
|
|
94
99
|
const configPath = getConfigPath();
|
|
95
100
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
96
|
-
|
|
101
|
+
try {
|
|
102
|
+
return await realpath(configPath);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
105
|
+
const entry = await lstat(configPath).catch((error: NodeJS.ErrnoException) => {
|
|
106
|
+
if (error.code !== "ENOENT") throw error;
|
|
107
|
+
return undefined;
|
|
108
|
+
});
|
|
109
|
+
if (entry) throw new Error("Cannot save through an unresolved configuration symlink");
|
|
110
|
+
return path.join(await realpath(path.dirname(configPath)), path.basename(configPath));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function saveConfig(
|
|
115
|
+
model: Pick<ModelInfo, "provider" | "id">,
|
|
116
|
+
setting: VerbositySetting,
|
|
117
|
+
onSaved: (config: VerbosityConfig) => void,
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
const configPath = await resolveConfigPath();
|
|
120
|
+
let compromised: Error | undefined;
|
|
121
|
+
const release = await lock(configPath, {
|
|
122
|
+
realpath: false,
|
|
123
|
+
retries: { retries: 10, factor: 1, minTimeout: 100, maxTimeout: 100 },
|
|
124
|
+
// The library's default callback throws asynchronously and can terminate Pi.
|
|
125
|
+
onCompromised: (error) => {
|
|
126
|
+
compromised = error;
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
let temporary: string | undefined;
|
|
130
|
+
try {
|
|
131
|
+
const raw = await readFile(configPath, "utf8").catch((error: NodeJS.ErrnoException) => {
|
|
132
|
+
if (error.code !== "ENOENT") throw error;
|
|
133
|
+
return '{"models":{}}';
|
|
134
|
+
});
|
|
135
|
+
const current: unknown = JSON.parse(raw);
|
|
136
|
+
if (!isObject(current) || !isObject(current.models)) {
|
|
137
|
+
throw new Error("Configuration must be an object with a models object");
|
|
138
|
+
}
|
|
139
|
+
const next = { ...current, models: { ...current.models, [getExactModelKey(model)]: setting } };
|
|
140
|
+
const nextConfig = parseConfig(next);
|
|
141
|
+
if (compromised) throw compromised;
|
|
142
|
+
const candidate = `${configPath}.${randomUUID()}.tmp`;
|
|
143
|
+
const file = await open(candidate, "wx", 0o600);
|
|
144
|
+
temporary = candidate;
|
|
145
|
+
try {
|
|
146
|
+
await file.writeFile(`${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
147
|
+
} finally {
|
|
148
|
+
await file.close();
|
|
149
|
+
}
|
|
150
|
+
// This is a cooperative mtime lease, not fencing against arbitrary OS pauses or lock removal.
|
|
151
|
+
if (compromised) throw compromised;
|
|
152
|
+
await rename(temporary, configPath);
|
|
153
|
+
temporary = undefined;
|
|
154
|
+
// Publication succeeded even if releasing the lock later fails.
|
|
155
|
+
onSaved(nextConfig);
|
|
156
|
+
} finally {
|
|
157
|
+
try {
|
|
158
|
+
if (temporary) await unlink(temporary);
|
|
159
|
+
} finally {
|
|
160
|
+
await release().catch((error: unknown) => {
|
|
161
|
+
throw compromised ?? error;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
97
165
|
}
|
|
98
166
|
|
|
99
167
|
function isSupportedModel(model: ExtensionContext["model"]): model is ModelInfo {
|
|
100
|
-
return
|
|
168
|
+
return (
|
|
169
|
+
!!model &&
|
|
170
|
+
/^gpt-(?:5|6)(?:[.-]|$)/i.test(model.id) &&
|
|
171
|
+
(model.api === CHAT_COMPLETIONS_API || RESPONSES_APIS.has(model.api))
|
|
172
|
+
);
|
|
101
173
|
}
|
|
102
174
|
|
|
103
175
|
function getExactModelKey(model: Pick<ModelInfo, "provider" | "id">): string {
|
|
@@ -107,28 +179,9 @@ function getExactModelKey(model: Pick<ModelInfo, "provider" | "id">): string {
|
|
|
107
179
|
function resolveVerbosity(
|
|
108
180
|
config: VerbosityConfig,
|
|
109
181
|
model: Pick<ModelInfo, "provider" | "id">,
|
|
110
|
-
): {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
if (exactVerbosity) return { key: exactKey, verbosity: exactVerbosity };
|
|
114
|
-
|
|
115
|
-
const sharedVerbosity = config.models[model.id];
|
|
116
|
-
if (sharedVerbosity) return { key: model.id, verbosity: sharedVerbosity };
|
|
117
|
-
|
|
118
|
-
return {};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function setVerbosity(
|
|
122
|
-
config: VerbosityConfig,
|
|
123
|
-
key: string,
|
|
124
|
-
verbosity: VerbositySetting,
|
|
125
|
-
): VerbosityConfig {
|
|
126
|
-
const models = { ...config.models };
|
|
127
|
-
|
|
128
|
-
if (verbosity === "auto") delete models[key];
|
|
129
|
-
else models[key] = verbosity;
|
|
130
|
-
|
|
131
|
-
return { models };
|
|
182
|
+
): { verbosity?: Verbosity } {
|
|
183
|
+
const setting = config.models[getExactModelKey(model)] ?? config.models[model.id];
|
|
184
|
+
return { verbosity: setting === "auto" ? undefined : setting };
|
|
132
185
|
}
|
|
133
186
|
|
|
134
187
|
function updateStatus(ctx: ExtensionContext, config: VerbosityConfig): void {
|
|
@@ -144,6 +197,7 @@ function updateStatus(ctx: ExtensionContext, config: VerbosityConfig): void {
|
|
|
144
197
|
|
|
145
198
|
export default function openaiVerbosityExtension(pi: ExtensionAPI): void {
|
|
146
199
|
let config = emptyConfig();
|
|
200
|
+
const pending = new Set<Promise<void>>();
|
|
147
201
|
|
|
148
202
|
pi.registerCommand("verbosity", {
|
|
149
203
|
description: "Set OpenAI response verbosity for the current model",
|
|
@@ -162,27 +216,31 @@ export default function openaiVerbosityExtension(pi: ExtensionAPI): void {
|
|
|
162
216
|
}
|
|
163
217
|
|
|
164
218
|
if (!isSupportedModel(model)) {
|
|
165
|
-
ctx.ui.notify(
|
|
166
|
-
"Current model does not support verbosity control. Supported APIs: openai-responses, openai-codex-responses, azure-openai-responses.",
|
|
167
|
-
"warning",
|
|
168
|
-
);
|
|
219
|
+
ctx.ui.notify("Current model does not support OpenAI verbosity control.", "warning");
|
|
169
220
|
updateStatus(ctx, config);
|
|
170
221
|
return;
|
|
171
222
|
}
|
|
172
223
|
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
224
|
+
let saved = false;
|
|
225
|
+
const operation = saveConfig(model, verbosity, (nextConfig) => {
|
|
226
|
+
config = nextConfig;
|
|
227
|
+
saved = true;
|
|
228
|
+
});
|
|
229
|
+
pending.add(operation);
|
|
177
230
|
try {
|
|
178
|
-
await
|
|
231
|
+
await operation;
|
|
179
232
|
} catch (error) {
|
|
180
233
|
const message = error instanceof Error ? error.message : String(error);
|
|
181
|
-
|
|
234
|
+
if (saved) updateStatus(ctx, config);
|
|
235
|
+
ctx.ui.notify(
|
|
236
|
+
`${saved ? "Saved, but cleanup failed for" : "Failed to save"} ${getConfigPath()}: ${message}`,
|
|
237
|
+
saved ? "warning" : "error",
|
|
238
|
+
);
|
|
182
239
|
return;
|
|
240
|
+
} finally {
|
|
241
|
+
pending.delete(operation);
|
|
183
242
|
}
|
|
184
243
|
|
|
185
|
-
config = nextConfig;
|
|
186
244
|
updateStatus(ctx, config);
|
|
187
245
|
|
|
188
246
|
if (verbosity === "auto") {
|
|
@@ -194,6 +252,10 @@ export default function openaiVerbosityExtension(pi: ExtensionAPI): void {
|
|
|
194
252
|
},
|
|
195
253
|
});
|
|
196
254
|
|
|
255
|
+
pi.on("session_shutdown", async () => {
|
|
256
|
+
await Promise.allSettled(pending);
|
|
257
|
+
});
|
|
258
|
+
|
|
197
259
|
pi.on("session_start", async (_event, ctx) => {
|
|
198
260
|
config = await loadConfig();
|
|
199
261
|
updateStatus(ctx, config);
|
|
@@ -213,6 +275,10 @@ export default function openaiVerbosityExtension(pi: ExtensionAPI): void {
|
|
|
213
275
|
const payload = event.payload;
|
|
214
276
|
if (!isObject(payload)) return;
|
|
215
277
|
|
|
278
|
+
if (model.api === CHAT_COMPLETIONS_API) {
|
|
279
|
+
return { ...payload, verbosity };
|
|
280
|
+
}
|
|
281
|
+
|
|
216
282
|
const text = isObject(payload.text) ? payload.text : {};
|
|
217
283
|
return {
|
|
218
284
|
...payload,
|
package/extensions/review/fix.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type AgentEndMessage,
|
|
20
20
|
type AgentEndMessages,
|
|
21
21
|
type FixPassAgentTracker,
|
|
22
|
+
type ReviewRuntime,
|
|
22
23
|
} from "./runtime.js";
|
|
23
24
|
import type { ParsedRequest, ReviewMessageDetails } from "./schema.js";
|
|
24
25
|
|
|
@@ -60,16 +61,18 @@ export async function prepareFixReviewDetails(
|
|
|
60
61
|
pi: ExtensionAPI,
|
|
61
62
|
ctx: ExtensionCommandContext,
|
|
62
63
|
request: ParsedRequest,
|
|
64
|
+
runtime: ReviewRuntime,
|
|
63
65
|
forceFreshReview = false,
|
|
64
66
|
): Promise<ReviewMessageDetails | null> {
|
|
67
|
+
if (runtime.closed) return null;
|
|
65
68
|
let reviewDetails = forceFreshReview ? null : getLastMessageReviewDetails(ctx);
|
|
66
69
|
|
|
67
70
|
if (reviewDetails && reviewMatchesFixRequest(reviewDetails, request)) {
|
|
68
71
|
const currentFingerprint = await computeCurrentFingerprint(
|
|
69
|
-
|
|
70
|
-
ctx.cwd,
|
|
72
|
+
{ cwd: ctx.cwd },
|
|
71
73
|
reviewDetails.scope.mode === "working-tree" || reviewDetails.scope.mode === "folder",
|
|
72
74
|
);
|
|
75
|
+
if (runtime.closed) return null;
|
|
73
76
|
const hasStalePayload = reviewDetails.staleness?.status === "stale";
|
|
74
77
|
const isStaleNow = !fingerprintsEqual(reviewDetails.fingerprint, currentFingerprint);
|
|
75
78
|
if (hasStalePayload || isStaleNow) {
|
|
@@ -80,7 +83,8 @@ export async function prepareFixReviewDetails(
|
|
|
80
83
|
notify(ctx, REVIEW_STALE_REUSE_WARNING, "warning");
|
|
81
84
|
}
|
|
82
85
|
} else {
|
|
83
|
-
const reviewResult = await runReviewPipeline(pi, ctx, request, "fix");
|
|
86
|
+
const reviewResult = await runReviewPipeline(pi, ctx, request, "fix", runtime);
|
|
87
|
+
if (runtime.closed) return null;
|
|
84
88
|
if (!reviewResult.ok) {
|
|
85
89
|
if (reviewResult.error === REVIEW_CANCELLED_ERROR) {
|
|
86
90
|
notify(ctx, REVIEW_CANCELLED_ERROR, "error");
|
|
@@ -209,9 +213,10 @@ export async function runFixLoop(
|
|
|
209
213
|
request: ParsedRequest,
|
|
210
214
|
agentTracker: FixPassAgentTracker,
|
|
211
215
|
reviewMessageQueue: ReviewMessageQueue,
|
|
216
|
+
runtime: ReviewRuntime,
|
|
212
217
|
): Promise<void> {
|
|
213
|
-
|
|
214
|
-
const reviewDetails = await prepareFixReviewDetails(pi, ctx, request, true);
|
|
218
|
+
while (!runtime.closed) {
|
|
219
|
+
const reviewDetails = await prepareFixReviewDetails(pi, ctx, request, runtime, true);
|
|
215
220
|
if (!reviewDetails) return;
|
|
216
221
|
|
|
217
222
|
const beforeFixFingerprint = reviewDetails.fingerprint;
|
|
@@ -223,12 +228,16 @@ export async function runFixLoop(
|
|
|
223
228
|
agentTracker,
|
|
224
229
|
reviewMessageQueue,
|
|
225
230
|
);
|
|
231
|
+
if (runtime.closed) return;
|
|
226
232
|
if (wasLastAssistantAborted(fixMessages)) {
|
|
227
233
|
notify(ctx, "Fix loop stopped: fix pass was aborted.", "warning");
|
|
228
234
|
return;
|
|
229
235
|
}
|
|
230
236
|
|
|
231
|
-
const afterFixFingerprint = await computeCurrentFingerprint(
|
|
237
|
+
const afterFixFingerprint = await computeCurrentFingerprint(
|
|
238
|
+
{ cwd: ctx.cwd },
|
|
239
|
+
reviewDetails.scope.mode === "working-tree" || reviewDetails.scope.mode === "folder",
|
|
240
|
+
);
|
|
232
241
|
if (fingerprintsEqual(beforeFixFingerprint, afterFixFingerprint)) {
|
|
233
242
|
notify(ctx, "Fix loop stopped: fix pass made no repository changes.", "warning");
|
|
234
243
|
return;
|