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.
- package/README.md +15 -12
- package/extensions/answer.ts +129 -79
- package/extensions/branch-term/README.md +7 -0
- package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
- package/extensions/btw.ts +17 -2
- 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 +109 -191
- package/extensions/loop.ts +149 -145
- package/extensions/memory.ts +138 -86
- 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 +41 -38
- package/extensions/review/review.ts +161 -166
- package/extensions/review/runner.ts +139 -147
- package/extensions/review/runtime.ts +173 -120
- 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 +132 -3338
- 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/index.ts +249 -356
- 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 +33 -39
- 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 +3 -3
- package/skills/oracle/scripts/oracle +10 -1
- 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
package/extensions/review/git.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
1
|
import { createHash } from "node:crypto";
|
|
4
2
|
import { promises as fs } from "node:fs";
|
|
5
3
|
import path from "node:path";
|
|
6
4
|
|
|
7
5
|
import { parsePrReference } from "./request.js";
|
|
6
|
+
import { runReviewCommand, type ReviewCommandContext } from "./runner.js";
|
|
7
|
+
import { joinAll } from "./runtime.js";
|
|
8
8
|
import type { ReviewFingerprint, ReviewTarget } from "./schema.js";
|
|
9
9
|
|
|
10
10
|
export type ResolvedScope =
|
|
@@ -46,44 +46,40 @@ function hashString(value: string): string {
|
|
|
46
46
|
return createHash("sha256").update(value).digest("hex");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
args: string[],
|
|
52
|
-
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
53
|
-
const { stdout, stderr, code } = await pi.exec("git", args);
|
|
54
|
-
return { stdout, stderr, code };
|
|
49
|
+
function runGit(context: ReviewCommandContext, args: string[]) {
|
|
50
|
+
return runReviewCommand(context, "git", args);
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
export async function isGitRepo(
|
|
58
|
-
const result = await runGit(
|
|
53
|
+
export async function isGitRepo(context: ReviewCommandContext): Promise<boolean> {
|
|
54
|
+
const result = await runGit(context, ["rev-parse", "--git-dir"]);
|
|
59
55
|
return result.code === 0;
|
|
60
56
|
}
|
|
61
57
|
|
|
62
|
-
async function getCurrentBranch(
|
|
63
|
-
const { stdout, code } = await runGit(
|
|
58
|
+
async function getCurrentBranch(context: ReviewCommandContext): Promise<string | null> {
|
|
59
|
+
const { stdout, code } = await runGit(context, ["branch", "--show-current"]);
|
|
64
60
|
if (code !== 0) return null;
|
|
65
61
|
const branch = stdout.trim();
|
|
66
62
|
return branch.length > 0 ? branch : null;
|
|
67
63
|
}
|
|
68
64
|
|
|
69
|
-
async function resolveHeadSha(
|
|
70
|
-
const { code, stdout } = await runGit(
|
|
65
|
+
async function resolveHeadSha(context: ReviewCommandContext): Promise<string | null> {
|
|
66
|
+
const { code, stdout } = await runGit(context, ["rev-parse", "--verify", "HEAD"]);
|
|
71
67
|
if (code !== 0) return null;
|
|
72
68
|
const sha = stdout.trim();
|
|
73
69
|
return sha.length > 0 ? sha : null;
|
|
74
70
|
}
|
|
75
71
|
|
|
76
|
-
async function hasHeadCommit(
|
|
77
|
-
return (await resolveHeadSha(
|
|
72
|
+
async function hasHeadCommit(context: ReviewCommandContext): Promise<boolean> {
|
|
73
|
+
return (await resolveHeadSha(context)) !== null;
|
|
78
74
|
}
|
|
79
75
|
|
|
80
|
-
async function getDefaultBranch(
|
|
81
|
-
const remoteHead = await runGit(
|
|
76
|
+
async function getDefaultBranch(context: ReviewCommandContext): Promise<string> {
|
|
77
|
+
const remoteHead = await runGit(context, ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"]);
|
|
82
78
|
if (remoteHead.code === 0 && remoteHead.stdout.trim()) {
|
|
83
79
|
return remoteHead.stdout.trim().replace(/^origin\//, "");
|
|
84
80
|
}
|
|
85
81
|
|
|
86
|
-
const branches = await runGit(
|
|
82
|
+
const branches = await runGit(context, ["branch", "--format=%(refname:short)"]);
|
|
87
83
|
if (branches.code === 0) {
|
|
88
84
|
const names = parseGitFileList(branches.stdout);
|
|
89
85
|
if (names.includes("main")) return "main";
|
|
@@ -94,8 +90,8 @@ async function getDefaultBranch(pi: ExtensionAPI): Promise<string> {
|
|
|
94
90
|
return "main";
|
|
95
91
|
}
|
|
96
92
|
|
|
97
|
-
async function getMergeBase(
|
|
98
|
-
const { stdout, code } = await runGit(
|
|
93
|
+
async function getMergeBase(context: ReviewCommandContext, branch: string): Promise<string | null> {
|
|
94
|
+
const { stdout, code } = await runGit(context, ["merge-base", "HEAD", branch]);
|
|
99
95
|
if (code !== 0) return null;
|
|
100
96
|
const sha = stdout.trim();
|
|
101
97
|
return sha.length > 0 ? sha : null;
|
|
@@ -108,12 +104,15 @@ function parseGitFileList(stdout: string): string[] {
|
|
|
108
104
|
.filter(Boolean);
|
|
109
105
|
}
|
|
110
106
|
|
|
111
|
-
async function getTrackedChangedFiles(
|
|
112
|
-
|
|
107
|
+
async function getTrackedChangedFiles(
|
|
108
|
+
context: ReviewCommandContext,
|
|
109
|
+
hasHead?: boolean,
|
|
110
|
+
): Promise<string[]> {
|
|
111
|
+
const headAvailable = hasHead ?? (await hasHeadCommit(context));
|
|
113
112
|
if (!headAvailable) {
|
|
114
|
-
const [staged, unstaged] = await
|
|
115
|
-
runGit(
|
|
116
|
-
runGit(
|
|
113
|
+
const [staged, unstaged] = await joinAll([
|
|
114
|
+
runGit(context, ["diff", "--cached", "--name-only"]),
|
|
115
|
+
runGit(context, ["diff", "--name-only"]),
|
|
117
116
|
]);
|
|
118
117
|
|
|
119
118
|
const files = new Set<string>([
|
|
@@ -123,35 +122,38 @@ async function getTrackedChangedFiles(pi: ExtensionAPI, hasHead?: boolean): Prom
|
|
|
123
122
|
return Array.from(files).sort((a, b) => a.localeCompare(b));
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
const { stdout, code } = await runGit(
|
|
125
|
+
const { stdout, code } = await runGit(context, ["diff", "HEAD", "--name-only"]);
|
|
127
126
|
if (code !== 0) return [];
|
|
128
127
|
return parseGitFileList(stdout);
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
async function getUntrackedFiles(
|
|
132
|
-
const { stdout, code } = await runGit(
|
|
130
|
+
async function getUntrackedFiles(context: ReviewCommandContext): Promise<string[]> {
|
|
131
|
+
const { stdout, code } = await runGit(context, ["ls-files", "--others", "--exclude-standard"]);
|
|
133
132
|
if (code !== 0) return [];
|
|
134
133
|
return parseGitFileList(stdout);
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
async function getDiffFilesInRange(
|
|
138
|
-
|
|
136
|
+
async function getDiffFilesInRange(
|
|
137
|
+
context: ReviewCommandContext,
|
|
138
|
+
range: string,
|
|
139
|
+
): Promise<string[]> {
|
|
140
|
+
const { stdout, code } = await runGit(context, ["diff", "--name-only", range]);
|
|
139
141
|
if (code !== 0) return [];
|
|
140
142
|
return parseGitFileList(stdout);
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
async function hasPendingTrackedChanges(
|
|
144
|
-
const { stdout, code } = await runGit(
|
|
145
|
+
async function hasPendingTrackedChanges(context: ReviewCommandContext): Promise<boolean> {
|
|
146
|
+
const { stdout, code } = await runGit(context, ["status", "--porcelain"]);
|
|
145
147
|
if (code !== 0) return false;
|
|
146
148
|
const lines = stdout.split("\n").filter((line) => line.length > 0);
|
|
147
149
|
return lines.some((line) => !line.startsWith("??"));
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
async function getPrInfo(
|
|
151
|
-
|
|
153
|
+
context: ReviewCommandContext,
|
|
152
154
|
prNumber: number,
|
|
153
155
|
): Promise<{ baseBranch: string; headBranch: string } | null> {
|
|
154
|
-
const { stdout, code } = await
|
|
156
|
+
const { stdout, code } = await runReviewCommand(context, "gh", [
|
|
155
157
|
"pr",
|
|
156
158
|
"view",
|
|
157
159
|
String(prNumber),
|
|
@@ -174,16 +176,16 @@ async function getPrInfo(
|
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
async function checkoutPr(
|
|
177
|
-
|
|
179
|
+
context: ReviewCommandContext,
|
|
178
180
|
prNumber: number,
|
|
179
181
|
): Promise<{ ok: boolean; error?: string }> {
|
|
180
|
-
const fetch = await runGit(
|
|
182
|
+
const fetch = await runGit(context, ["fetch", "origin", `refs/pull/${prNumber}/head`]);
|
|
181
183
|
if (fetch.code !== 0) {
|
|
182
184
|
const error = (fetch.stderr || fetch.stdout || "Failed to fetch PR").trim();
|
|
183
185
|
return { ok: false, error };
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
const checkout = await runGit(
|
|
188
|
+
const checkout = await runGit(context, ["switch", "--detach", "FETCH_HEAD"]);
|
|
187
189
|
if (checkout.code !== 0) {
|
|
188
190
|
const error = (checkout.stderr || checkout.stdout || "Failed to checkout PR").trim();
|
|
189
191
|
return { ok: false, error };
|
|
@@ -192,27 +194,29 @@ async function checkoutPr(
|
|
|
192
194
|
return { ok: true };
|
|
193
195
|
}
|
|
194
196
|
|
|
195
|
-
export async function getPrCheckoutBlockedError(
|
|
196
|
-
|
|
197
|
+
export async function getPrCheckoutBlockedError(
|
|
198
|
+
context: ReviewCommandContext,
|
|
199
|
+
): Promise<string | null> {
|
|
200
|
+
return (await hasPendingTrackedChanges(context))
|
|
197
201
|
? "Cannot checkout PR with pending tracked changes. Commit or stash first."
|
|
198
202
|
: null;
|
|
199
203
|
}
|
|
200
204
|
|
|
201
205
|
export async function preparePrCheckoutScope(
|
|
202
|
-
|
|
206
|
+
context: ReviewCommandContext,
|
|
203
207
|
reportStatus: GitStatusReporter | undefined,
|
|
204
208
|
details: { prNumber: number; baseBranch: string; headBranch: string },
|
|
205
209
|
): Promise<
|
|
206
210
|
| { ok: false; error: string }
|
|
207
211
|
| { ok: true; scope: Extract<ResolvedScope, { kind: "branch-diff" }> }
|
|
208
212
|
> {
|
|
209
|
-
const blockedError = await getPrCheckoutBlockedError(
|
|
213
|
+
const blockedError = await getPrCheckoutBlockedError(context);
|
|
210
214
|
if (blockedError) {
|
|
211
215
|
return { ok: false, error: blockedError };
|
|
212
216
|
}
|
|
213
217
|
|
|
214
218
|
reportStatus?.(`Checking out PR #${details.prNumber}...`, "info");
|
|
215
|
-
const checkout = await checkoutPr(
|
|
219
|
+
const checkout = await checkoutPr(context, details.prNumber);
|
|
216
220
|
if (!checkout.ok) {
|
|
217
221
|
return {
|
|
218
222
|
ok: false,
|
|
@@ -221,7 +225,7 @@ export async function preparePrCheckoutScope(
|
|
|
221
225
|
}
|
|
222
226
|
reportStatus?.(`Checked out PR #${details.prNumber} (${details.headBranch}).`, "info");
|
|
223
227
|
|
|
224
|
-
const resolvedScope = await resolveBranchDiffScope(
|
|
228
|
+
const resolvedScope = await resolveBranchDiffScope(context, {
|
|
225
229
|
baseBranch: details.baseBranch,
|
|
226
230
|
description: (diffFileCount) =>
|
|
227
231
|
`PR #${details.prNumber} diff vs ${details.baseBranch} (${diffFileCount} files)`,
|
|
@@ -238,17 +242,21 @@ export async function preparePrCheckoutScope(
|
|
|
238
242
|
return { ok: true, scope: resolvedScope.scope };
|
|
239
243
|
}
|
|
240
244
|
|
|
241
|
-
export async function loadProjectReviewGuidelines(
|
|
245
|
+
export async function loadProjectReviewGuidelines({
|
|
246
|
+
cwd,
|
|
247
|
+
signal,
|
|
248
|
+
}: ReviewCommandContext): Promise<string | null> {
|
|
242
249
|
let currentDir = path.resolve(cwd);
|
|
243
250
|
|
|
244
251
|
while (true) {
|
|
252
|
+
signal?.throwIfAborted();
|
|
245
253
|
const piDir = path.join(currentDir, ".pi");
|
|
246
254
|
const guidelinesPath = path.join(currentDir, "REVIEW_GUIDELINES.md");
|
|
247
255
|
|
|
248
256
|
const piStats = await fs.stat(piDir).catch(() => null);
|
|
249
257
|
if (piStats?.isDirectory()) {
|
|
250
258
|
try {
|
|
251
|
-
const content = await fs.readFile(guidelinesPath, "utf8");
|
|
259
|
+
const content = await fs.readFile(guidelinesPath, { encoding: "utf8", signal });
|
|
252
260
|
const trimmed = content.trim();
|
|
253
261
|
return trimmed.length > 0 ? trimmed : null;
|
|
254
262
|
} catch {
|
|
@@ -262,13 +270,13 @@ export async function loadProjectReviewGuidelines(cwd: string): Promise<string |
|
|
|
262
270
|
}
|
|
263
271
|
}
|
|
264
272
|
|
|
265
|
-
async function hashGitDiff(
|
|
266
|
-
const head = await runGit(
|
|
273
|
+
async function hashGitDiff(context: ReviewCommandContext): Promise<string> {
|
|
274
|
+
const head = await runGit(context, ["diff", "--no-ext-diff", "HEAD"]);
|
|
267
275
|
if (head.code === 0) return hashString(head.stdout);
|
|
268
276
|
|
|
269
|
-
const [staged, unstaged] = await
|
|
270
|
-
runGit(
|
|
271
|
-
runGit(
|
|
277
|
+
const [staged, unstaged] = await joinAll([
|
|
278
|
+
runGit(context, ["diff", "--no-ext-diff", "--cached"]),
|
|
279
|
+
runGit(context, ["diff", "--no-ext-diff"]),
|
|
272
280
|
]);
|
|
273
281
|
const stagedHash = staged.code === 0 ? hashString(staged.stdout) : null;
|
|
274
282
|
const unstagedHash = unstaged.code === 0 ? hashString(unstaged.stdout) : null;
|
|
@@ -277,53 +285,36 @@ async function hashGitDiff(pi: ExtensionAPI): Promise<string> {
|
|
|
277
285
|
}
|
|
278
286
|
|
|
279
287
|
async function computeUntrackedContentHash(
|
|
280
|
-
|
|
281
|
-
cwd: string,
|
|
288
|
+
context: ReviewCommandContext,
|
|
282
289
|
precomputedUntrackedFiles?: string[],
|
|
283
290
|
): Promise<string> {
|
|
284
|
-
const untrackedFiles = [
|
|
285
|
-
(
|
|
286
|
-
);
|
|
291
|
+
const untrackedFiles = [
|
|
292
|
+
...(precomputedUntrackedFiles ?? (await getUntrackedFiles(context))),
|
|
293
|
+
].sort((a, b) => a.localeCompare(b));
|
|
287
294
|
if (untrackedFiles.length === 0) return hashString("");
|
|
288
295
|
|
|
289
|
-
const
|
|
296
|
+
const { stdout, code } = await runReviewCommand(
|
|
297
|
+
context,
|
|
298
|
+
"git",
|
|
299
|
+
["hash-object", "--stdin-paths"],
|
|
300
|
+
untrackedFiles.join("\n"),
|
|
301
|
+
);
|
|
302
|
+
const hashes = code === 0 ? stdout.trim().split("\n") : [];
|
|
290
303
|
const entries = untrackedFiles.map((file, i) => `${file}\0${hashes[i] ?? "missing"}`);
|
|
291
304
|
return hashString(entries.join("\n"));
|
|
292
305
|
}
|
|
293
306
|
|
|
294
|
-
function hashObjectBatch(cwd: string, files: string[]): Promise<string[]> {
|
|
295
|
-
return new Promise((resolve) => {
|
|
296
|
-
const proc = spawn("git", ["hash-object", "--stdin-paths"], {
|
|
297
|
-
cwd,
|
|
298
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
299
|
-
});
|
|
300
|
-
let stdout = "";
|
|
301
|
-
proc.stdout.on("data", (chunk: Buffer) => {
|
|
302
|
-
stdout += chunk.toString();
|
|
303
|
-
});
|
|
304
|
-
proc.on("error", () => resolve([]));
|
|
305
|
-
proc.on("close", (code) => {
|
|
306
|
-
resolve(code === 0 ? stdout.trim().split("\n") : []);
|
|
307
|
-
});
|
|
308
|
-
proc.stdin.on("error", () => {
|
|
309
|
-
/* ignore broken pipe */
|
|
310
|
-
});
|
|
311
|
-
proc.stdin.end(files.join("\n"));
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
307
|
export async function computeCurrentFingerprint(
|
|
316
|
-
|
|
317
|
-
cwd: string,
|
|
308
|
+
context: ReviewCommandContext,
|
|
318
309
|
includeUntracked: boolean,
|
|
319
310
|
precomputedUntrackedFiles?: string[],
|
|
320
311
|
): Promise<ReviewFingerprint> {
|
|
321
|
-
const [headSha, branch, trackedDiffHash, untrackedHash] = await
|
|
322
|
-
resolveHeadSha(
|
|
323
|
-
getCurrentBranch(
|
|
324
|
-
hashGitDiff(
|
|
312
|
+
const [headSha, branch, trackedDiffHash, untrackedHash] = await joinAll([
|
|
313
|
+
resolveHeadSha(context).then((sha) => sha ?? ""),
|
|
314
|
+
getCurrentBranch(context).then((branch) => branch ?? ""),
|
|
315
|
+
hashGitDiff(context),
|
|
325
316
|
includeUntracked
|
|
326
|
-
? computeUntrackedContentHash(
|
|
317
|
+
? computeUntrackedContentHash(context, precomputedUntrackedFiles)
|
|
327
318
|
: Promise.resolve(REVIEW_UNTRACKED_HASH_DISABLED),
|
|
328
319
|
]);
|
|
329
320
|
|
|
@@ -349,16 +340,16 @@ type BranchDiffScopeOptions = {
|
|
|
349
340
|
};
|
|
350
341
|
|
|
351
342
|
async function resolveBranchDiffScope(
|
|
352
|
-
|
|
343
|
+
context: ReviewCommandContext,
|
|
353
344
|
options: BranchDiffScopeOptions,
|
|
354
345
|
): Promise<{ scope?: ResolvedScope; error?: string }> {
|
|
355
|
-
const mergeBase = await getMergeBase(
|
|
346
|
+
const mergeBase = await getMergeBase(context, options.baseBranch);
|
|
356
347
|
if (!mergeBase) {
|
|
357
348
|
return { error: options.mergeBaseError };
|
|
358
349
|
}
|
|
359
350
|
|
|
360
351
|
const range = `${mergeBase}..HEAD`;
|
|
361
|
-
const diffFiles = await getDiffFilesInRange(
|
|
352
|
+
const diffFiles = await getDiffFilesInRange(context, range);
|
|
362
353
|
if (diffFiles.length === 0) {
|
|
363
354
|
return { error: options.emptyDiffError };
|
|
364
355
|
}
|
|
@@ -375,20 +366,19 @@ async function resolveBranchDiffScope(
|
|
|
375
366
|
}
|
|
376
367
|
|
|
377
368
|
export async function resolveScope(
|
|
378
|
-
|
|
369
|
+
context: ReviewCommandContext,
|
|
379
370
|
target: ReviewTarget,
|
|
380
371
|
reportStatus?: GitStatusReporter,
|
|
381
372
|
): Promise<{ scope?: ResolvedScope; error?: string }> {
|
|
382
373
|
switch (target.type) {
|
|
383
374
|
case "auto":
|
|
384
375
|
case "uncommitted": {
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const [trackedFiles, untrackedFiles] = await Promise.all([
|
|
389
|
-
getTrackedChangedFiles(pi, hasHead),
|
|
390
|
-
untrackedPromise,
|
|
376
|
+
const [headSha, untrackedFiles] = await joinAll([
|
|
377
|
+
resolveHeadSha(context),
|
|
378
|
+
getUntrackedFiles(context),
|
|
391
379
|
]);
|
|
380
|
+
const hasHead = headSha !== null;
|
|
381
|
+
const trackedFiles = await getTrackedChangedFiles(context, hasHead);
|
|
392
382
|
if (trackedFiles.length > 0 || untrackedFiles.length > 0) {
|
|
393
383
|
return {
|
|
394
384
|
scope: {
|
|
@@ -405,8 +395,8 @@ export async function resolveScope(
|
|
|
405
395
|
return { error: "No uncommitted changes to review." };
|
|
406
396
|
}
|
|
407
397
|
|
|
408
|
-
const baseBranch = await getDefaultBranch(
|
|
409
|
-
return resolveBranchDiffScope(
|
|
398
|
+
const baseBranch = await getDefaultBranch(context);
|
|
399
|
+
return resolveBranchDiffScope(context, {
|
|
410
400
|
baseBranch,
|
|
411
401
|
description: (diffFileCount) => `branch diff vs ${baseBranch} (${diffFileCount} files)`,
|
|
412
402
|
mergeBaseError: `Could not determine merge-base against ${baseBranch}`,
|
|
@@ -414,7 +404,7 @@ export async function resolveScope(
|
|
|
414
404
|
});
|
|
415
405
|
}
|
|
416
406
|
case "branch": {
|
|
417
|
-
return resolveBranchDiffScope(
|
|
407
|
+
return resolveBranchDiffScope(context, {
|
|
418
408
|
baseBranch: target.branch,
|
|
419
409
|
description: (diffFileCount) => `branch diff vs ${target.branch} (${diffFileCount} files)`,
|
|
420
410
|
mergeBaseError: `Could not determine merge-base against ${target.branch}`,
|
|
@@ -431,7 +421,7 @@ export async function resolveScope(
|
|
|
431
421
|
};
|
|
432
422
|
}
|
|
433
423
|
case "pr": {
|
|
434
|
-
const blockedError = await getPrCheckoutBlockedError(
|
|
424
|
+
const blockedError = await getPrCheckoutBlockedError(context);
|
|
435
425
|
if (blockedError) {
|
|
436
426
|
return { error: blockedError };
|
|
437
427
|
}
|
|
@@ -442,14 +432,14 @@ export async function resolveScope(
|
|
|
442
432
|
}
|
|
443
433
|
|
|
444
434
|
reportStatus?.(`Fetching PR #${prNumber} information...`, "info");
|
|
445
|
-
const prInfo = await getPrInfo(
|
|
435
|
+
const prInfo = await getPrInfo(context, prNumber);
|
|
446
436
|
if (!prInfo) {
|
|
447
437
|
return {
|
|
448
438
|
error: `Could not load PR #${prNumber}. Ensure gh is authenticated and PR exists.`,
|
|
449
439
|
};
|
|
450
440
|
}
|
|
451
441
|
|
|
452
|
-
const preparedPrScope = await preparePrCheckoutScope(
|
|
442
|
+
const preparedPrScope = await preparePrCheckoutScope(context, reportStatus, {
|
|
453
443
|
prNumber,
|
|
454
444
|
baseBranch: prInfo.baseBranch,
|
|
455
445
|
headBranch: prInfo.headBranch,
|
|
@@ -495,7 +485,7 @@ export function buildScopeInstructions(scope: ResolvedScope): string {
|
|
|
495
485
|
: "- There are no tracked-file diffs.";
|
|
496
486
|
const untracked =
|
|
497
487
|
scope.untrackedFiles.length > 0
|
|
498
|
-
? `- Also review untracked files as
|
|
488
|
+
? `- Also review untracked files as additions by reading their current contents directly.\n- Untracked files (${scope.untrackedFiles.length}):\n${scope.untrackedFiles.map((f) => ` - ${f}`).join("\n")}`
|
|
499
489
|
: "- There are no untracked files.";
|
|
500
490
|
return `Scope: working tree review.\n${tracked}\n${untracked}`;
|
|
501
491
|
}
|
|
@@ -503,7 +493,7 @@ export function buildScopeInstructions(scope: ResolvedScope): string {
|
|
|
503
493
|
return `Scope: branch diff review against base branch ${scope.baseBranch}.\n- Merge base: ${scope.mergeBase}\n- First capture the full diff with: \`git diff ${scope.mergeBase}..HEAD\` (treat this diff as mandatory review context).\n- Files in diff (${scope.diffFiles.length}):\n${scope.diffFiles.map((f) => ` - ${f}`).join("\n")}`;
|
|
504
494
|
}
|
|
505
495
|
case "commit": {
|
|
506
|
-
return `Scope: commit review for ${scope.sha}.\n- First capture the full commit patch with: \`git show --stat --patch ${scope.sha}\` (treat this patch as mandatory review context)
|
|
496
|
+
return `Scope: commit review for ${scope.sha}.\n- First capture the full commit patch with: \`git show --stat --patch ${scope.sha}\` (treat this patch as mandatory review context).`;
|
|
507
497
|
}
|
|
508
498
|
case "folder": {
|
|
509
499
|
return `Scope: snapshot review of selected paths (not a diff).\n- Paths:\n${scope.paths
|
|
@@ -29,14 +29,9 @@ import {
|
|
|
29
29
|
} from "./request.js";
|
|
30
30
|
import { runReviewPipeline } from "./review.js";
|
|
31
31
|
import {
|
|
32
|
-
acquireReviewRunLock,
|
|
33
32
|
createAgentRunTracker,
|
|
34
|
-
|
|
35
|
-
handleReviewSessionStart,
|
|
33
|
+
createReviewRuntime,
|
|
36
34
|
notify,
|
|
37
|
-
recordPromptEnd,
|
|
38
|
-
recordPromptStart,
|
|
39
|
-
releaseReviewRunLock,
|
|
40
35
|
type AgentEndMessages,
|
|
41
36
|
} from "./runtime.js";
|
|
42
37
|
import { runTriagePipeline } from "./triage.js";
|
|
@@ -66,69 +61,39 @@ function stopAndFlushReviewQueue(
|
|
|
66
61
|
|
|
67
62
|
type BackgroundRunResult = { ok: true } | { ok: false; error: string };
|
|
68
63
|
|
|
69
|
-
function startQueuedBackgroundRun(
|
|
70
|
-
ctx: ExtensionCommandContext,
|
|
71
|
-
reviewMessageQueue: ReviewMessageQueue,
|
|
72
|
-
options: {
|
|
73
|
-
busyMessage: string;
|
|
74
|
-
startMessage: string;
|
|
75
|
-
failurePrefix: string;
|
|
76
|
-
run: () => Promise<BackgroundRunResult>;
|
|
77
|
-
},
|
|
78
|
-
): void {
|
|
79
|
-
const sessionKey = acquireReviewRunLock(ctx, options.busyMessage);
|
|
80
|
-
if (!sessionKey) return;
|
|
81
|
-
|
|
82
|
-
const stopReviewQueue = reviewMessageQueue.start(ctx);
|
|
83
|
-
notify(ctx, options.startMessage, "info");
|
|
84
|
-
void (async () => {
|
|
85
|
-
try {
|
|
86
|
-
const result = await options.run();
|
|
87
|
-
if (!result.ok) {
|
|
88
|
-
notify(ctx, result.error, "error");
|
|
89
|
-
}
|
|
90
|
-
} catch (error) {
|
|
91
|
-
notify(
|
|
92
|
-
ctx,
|
|
93
|
-
`${options.failurePrefix}: ${error instanceof Error ? error.message : String(error)}`,
|
|
94
|
-
"error",
|
|
95
|
-
);
|
|
96
|
-
} finally {
|
|
97
|
-
releaseReviewRunLock(sessionKey);
|
|
98
|
-
stopAndFlushReviewQueue(ctx, reviewMessageQueue, stopReviewQueue);
|
|
99
|
-
}
|
|
100
|
-
})();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
64
|
export default function reviewExtension(pi: ExtensionAPI) {
|
|
104
65
|
const reviewMessageQueue = createReviewMessageQueue(pi);
|
|
105
66
|
const agentTracker = createAgentRunTracker();
|
|
67
|
+
const runtime = createReviewRuntime(pi, (ctx) => {
|
|
68
|
+
reviewMessageQueue.retain(ctx);
|
|
69
|
+
});
|
|
70
|
+
let background: Promise<void> | undefined;
|
|
106
71
|
|
|
107
|
-
pi.
|
|
108
|
-
|
|
72
|
+
pi.on("ui_prompt_start", async () => {
|
|
73
|
+
runtime.setPromptActive(true);
|
|
109
74
|
});
|
|
110
75
|
|
|
111
|
-
pi.
|
|
112
|
-
|
|
76
|
+
pi.on("ui_prompt_end", async () => {
|
|
77
|
+
runtime.setPromptActive(false);
|
|
113
78
|
});
|
|
114
79
|
|
|
115
80
|
pi.on("input", async (event, ctx) => {
|
|
116
|
-
|
|
117
|
-
return { action: "handled" };
|
|
81
|
+
return reviewMessageQueue.handleInput(event, ctx) ?? { action: "continue" };
|
|
118
82
|
});
|
|
119
83
|
|
|
120
84
|
pi.on("agent_start", async () => {
|
|
121
85
|
agentTracker.handleStart();
|
|
122
86
|
});
|
|
123
87
|
|
|
124
|
-
pi.on("session_start", async (
|
|
125
|
-
handleReviewSessionStart(ctx);
|
|
88
|
+
pi.on("session_start", async () => {
|
|
126
89
|
agentTracker.reset();
|
|
127
90
|
});
|
|
128
91
|
|
|
129
92
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
130
|
-
|
|
93
|
+
const closing = runtime.shutdown();
|
|
131
94
|
reviewMessageQueue.clear(ctx);
|
|
95
|
+
await closing;
|
|
96
|
+
await background;
|
|
132
97
|
agentTracker.reset();
|
|
133
98
|
});
|
|
134
99
|
|
|
@@ -153,11 +118,11 @@ export default function reviewExtension(pi: ExtensionAPI) {
|
|
|
153
118
|
return;
|
|
154
119
|
}
|
|
155
120
|
|
|
156
|
-
startQueuedBackgroundRun(ctx,
|
|
121
|
+
startQueuedBackgroundRun(ctx, {
|
|
157
122
|
busyMessage: "A /review run is already active in this session.",
|
|
158
123
|
startMessage: "Starting review in background...",
|
|
159
124
|
failurePrefix: "Review run failed",
|
|
160
|
-
run: () => runReviewPipeline(pi, ctx, parsed.value, "review"),
|
|
125
|
+
run: () => runReviewPipeline(pi, ctx, parsed.value, "review", runtime),
|
|
161
126
|
});
|
|
162
127
|
},
|
|
163
128
|
});
|
|
@@ -172,12 +137,12 @@ export default function reviewExtension(pi: ExtensionAPI) {
|
|
|
172
137
|
return;
|
|
173
138
|
}
|
|
174
139
|
|
|
175
|
-
startQueuedBackgroundRun(ctx,
|
|
140
|
+
startQueuedBackgroundRun(ctx, {
|
|
176
141
|
busyMessage:
|
|
177
142
|
"A review-related run is already active in this session. Wait for it to finish before /triage.",
|
|
178
143
|
startMessage: "Starting PR triage in background...",
|
|
179
144
|
failurePrefix: "PR triage failed",
|
|
180
|
-
run: () => runTriagePipeline(pi, ctx, parsed.value),
|
|
145
|
+
run: () => runTriagePipeline(pi, ctx, parsed.value, runtime),
|
|
181
146
|
});
|
|
182
147
|
},
|
|
183
148
|
});
|
|
@@ -199,21 +164,23 @@ export default function reviewExtension(pi: ExtensionAPI) {
|
|
|
199
164
|
return;
|
|
200
165
|
}
|
|
201
166
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
167
|
+
if (
|
|
168
|
+
!runtime.acquire(
|
|
169
|
+
ctx,
|
|
170
|
+
"A /review run is active in this session. Wait for it to finish before /fix.",
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
return;
|
|
207
174
|
|
|
208
175
|
const stopReviewQueue = reviewMessageQueue.start(ctx);
|
|
209
176
|
try {
|
|
210
177
|
if (loop) {
|
|
211
178
|
notify(ctx, "Starting fix loop...", "info");
|
|
212
|
-
await runFixLoop(pi, ctx, request, agentTracker, reviewMessageQueue);
|
|
179
|
+
await runFixLoop(pi, ctx, request, agentTracker, reviewMessageQueue, runtime);
|
|
213
180
|
return;
|
|
214
181
|
}
|
|
215
182
|
|
|
216
|
-
const reviewDetails = await prepareFixReviewDetails(pi, ctx, request);
|
|
183
|
+
const reviewDetails = await prepareFixReviewDetails(pi, ctx, request, runtime);
|
|
217
184
|
if (!reviewDetails) return;
|
|
218
185
|
await runFixPassFromReview(
|
|
219
186
|
pi,
|
|
@@ -224,9 +191,40 @@ export default function reviewExtension(pi: ExtensionAPI) {
|
|
|
224
191
|
reviewMessageQueue,
|
|
225
192
|
);
|
|
226
193
|
} finally {
|
|
227
|
-
|
|
228
|
-
stopAndFlushReviewQueue(ctx, reviewMessageQueue, stopReviewQueue);
|
|
194
|
+
runtime.release();
|
|
195
|
+
if (!runtime.closed) stopAndFlushReviewQueue(ctx, reviewMessageQueue, stopReviewQueue);
|
|
229
196
|
}
|
|
230
197
|
},
|
|
231
198
|
});
|
|
199
|
+
|
|
200
|
+
function startQueuedBackgroundRun(
|
|
201
|
+
ctx: ExtensionCommandContext,
|
|
202
|
+
options: {
|
|
203
|
+
busyMessage: string;
|
|
204
|
+
startMessage: string;
|
|
205
|
+
failurePrefix: string;
|
|
206
|
+
run: () => Promise<BackgroundRunResult>;
|
|
207
|
+
},
|
|
208
|
+
): void {
|
|
209
|
+
if (!runtime.acquire(ctx, options.busyMessage)) return;
|
|
210
|
+
const stopReviewQueue = reviewMessageQueue.start(ctx);
|
|
211
|
+
notify(ctx, options.startMessage, "info");
|
|
212
|
+
background = (async () => {
|
|
213
|
+
try {
|
|
214
|
+
const result = await options.run();
|
|
215
|
+
if (!result.ok && !runtime.closed) notify(ctx, result.error, "error");
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (!runtime.closed) {
|
|
218
|
+
notify(
|
|
219
|
+
ctx,
|
|
220
|
+
`${options.failurePrefix}: ${error instanceof Error ? error.message : String(error)}`,
|
|
221
|
+
"error",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
} finally {
|
|
225
|
+
runtime.release();
|
|
226
|
+
if (!runtime.closed) stopAndFlushReviewQueue(ctx, reviewMessageQueue, stopReviewQueue);
|
|
227
|
+
}
|
|
228
|
+
})();
|
|
229
|
+
}
|
|
232
230
|
}
|