tau-coding-agent 0.1.4 → 0.1.5
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 +1 -1
- package/extensions/answer.ts +48 -42
- package/extensions/branch-term.ts +8 -13
- package/extensions/btw.ts +4 -4
- package/extensions/ghostty.ts +64 -11
- package/extensions/insights.ts +4 -3
- package/extensions/loop.ts +37 -9
- package/extensions/memory.ts +5 -10
- package/extensions/notify.ts +24 -5
- package/extensions/openai-fast.ts +6 -4
- package/extensions/openai-verbosity.ts +6 -4
- package/extensions/review/fix.ts +239 -0
- package/extensions/review/git.ts +517 -0
- package/extensions/review/index.ts +232 -0
- package/extensions/review/message-queue.ts +249 -0
- package/extensions/review/models.ts +589 -0
- package/extensions/review/prompts.ts +337 -0
- package/extensions/review/renderers/inline.ts +126 -0
- package/extensions/review/request.ts +504 -0
- package/extensions/review/review.ts +1305 -0
- package/extensions/review/runner.ts +522 -0
- package/extensions/review/runtime.ts +275 -0
- package/extensions/review/schema.ts +160 -0
- package/extensions/review/submit-review-tool.ts +66 -0
- package/extensions/review/submit-triage-tool.ts +59 -0
- package/extensions/review/triage.ts +746 -0
- package/extensions/sandbox/index.ts +399 -102
- package/extensions/{interlude.ts → stash.ts} +13 -11
- package/extensions/tool-display-mode.ts +2 -3
- package/extensions/usage/index.ts +8 -4
- package/extensions/websearch/README.md +1 -1
- package/extensions/websearch/browser/chromium.ts +42 -31
- package/extensions/websearch/browser/discovery.ts +11 -9
- package/extensions/websearch/browser/firefox.ts +10 -7
- package/extensions/websearch/browser/fs.ts +10 -0
- package/extensions/websearch/browser/sqlite.ts +22 -15
- package/extensions/websearch/config.ts +2 -2
- package/extensions/websearch/index.ts +6 -6
- package/extensions/worktree.ts +33 -33
- package/package.json +3 -3
- package/skills/git-clean-history/SKILL.md +1 -0
- package/skills/git-commit/SKILL.md +2 -2
- package/skills/oracle/scripts/oracle +28 -11
- package/themes/tau-dark.json +2 -1
- package/extensions/review.ts +0 -4420
package/extensions/notify.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - Windows toast: Windows Terminal (WSL)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
13
14
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
15
|
|
|
15
16
|
const REVIEW_EVENT_START = "review:start";
|
|
@@ -39,7 +40,6 @@ function notifyOSC99(title: string, body: string): void {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function notifyWindows(title: string, body: string): void {
|
|
42
|
-
const { execFile } = require("child_process");
|
|
43
43
|
execFile("powershell.exe", ["-NoProfile", "-Command", windowsToastScript(title, body)]);
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -92,13 +92,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
92
92
|
let pendingPromptCount = 0;
|
|
93
93
|
const activeReviewSessions = new Set<string>();
|
|
94
94
|
let currentSessionKey: string | undefined;
|
|
95
|
+
let pendingReadyNotification: ReturnType<typeof setImmediate> | undefined;
|
|
95
96
|
|
|
96
97
|
const hasCurrentSessionReviewRun = () => {
|
|
97
98
|
if (!currentSessionKey) return false;
|
|
98
99
|
return activeReviewSessions.has(currentSessionKey);
|
|
99
100
|
};
|
|
100
101
|
|
|
102
|
+
const cancelReadyNotification = () => {
|
|
103
|
+
if (!pendingReadyNotification) return;
|
|
104
|
+
clearImmediate(pendingReadyNotification);
|
|
105
|
+
pendingReadyNotification = undefined;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const scheduleReadyNotification = (ctx: ExtensionContext) => {
|
|
109
|
+
cancelReadyNotification();
|
|
110
|
+
pendingReadyNotification = setImmediate(() => {
|
|
111
|
+
pendingReadyNotification = undefined;
|
|
112
|
+
if (!ctx.isIdle()) return;
|
|
113
|
+
if (pendingPromptCount > 0) return;
|
|
114
|
+
if (hasCurrentSessionReviewRun()) return;
|
|
115
|
+
notify("Pi", "Ready for input");
|
|
116
|
+
});
|
|
117
|
+
pendingReadyNotification.unref?.();
|
|
118
|
+
};
|
|
119
|
+
|
|
101
120
|
pi.on("session_start", async (_event, ctx) => {
|
|
121
|
+
cancelReadyNotification();
|
|
102
122
|
currentSessionKey = getSessionKey(ctx);
|
|
103
123
|
});
|
|
104
124
|
|
|
@@ -143,14 +163,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
143
163
|
notify("Pi", "Review failed");
|
|
144
164
|
});
|
|
145
165
|
|
|
146
|
-
pi.on("
|
|
166
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
147
167
|
currentSessionKey = getSessionKey(ctx);
|
|
148
|
-
|
|
149
|
-
if (hasCurrentSessionReviewRun()) return;
|
|
150
|
-
notify("Pi", "Ready for input");
|
|
168
|
+
scheduleReadyNotification(ctx);
|
|
151
169
|
});
|
|
152
170
|
|
|
153
171
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
172
|
+
cancelReadyNotification();
|
|
154
173
|
pendingPromptCount = 0;
|
|
155
174
|
const sessionKey = getSessionKey(ctx);
|
|
156
175
|
activeReviewSessions.delete(sessionKey);
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
getAgentDir,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionContext,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
8
|
|
|
6
9
|
const STATUS_KEY = "openai-fast";
|
|
7
10
|
const SUPPORTED_APIS = new Set([
|
|
@@ -23,8 +26,7 @@ function emptyConfig(): FastConfig {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
function getConfigPath(): string {
|
|
26
|
-
|
|
27
|
-
return path.join(agentDir, "openai-fast.json");
|
|
29
|
+
return path.join(getAgentDir(), "openai-fast.json");
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
function isObject(value: unknown): value is JsonObject {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
getAgentDir,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionContext,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
8
|
|
|
6
9
|
const STATUS_KEY = "openai-verbosity";
|
|
7
10
|
const SUPPORTED_APIS = new Set([
|
|
@@ -23,8 +26,7 @@ function emptyConfig(): VerbosityConfig {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
function getConfigPath(): string {
|
|
26
|
-
|
|
27
|
-
return path.join(agentDir, "openai-verbosity.json");
|
|
29
|
+
return path.join(getAgentDir(), "openai-verbosity.json");
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
function isObject(value: unknown): value is JsonObject {
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import type { ReviewMessageQueue } from "./message-queue.js";
|
|
8
|
+
import { buildAdditionalContextSection, FIX_PROMPT } from "./prompts.js";
|
|
9
|
+
import { reviewMatchesFixRequest } from "./request.js";
|
|
10
|
+
import { computeCurrentFingerprint, fingerprintsEqual } from "./git.js";
|
|
11
|
+
import {
|
|
12
|
+
buildStalePayloadStaleness,
|
|
13
|
+
getLastMessageReviewDetails,
|
|
14
|
+
runReviewPipeline,
|
|
15
|
+
} from "./review.js";
|
|
16
|
+
import {
|
|
17
|
+
notify,
|
|
18
|
+
REVIEW_CANCELLED_ERROR,
|
|
19
|
+
type AgentEndMessage,
|
|
20
|
+
type AgentEndMessages,
|
|
21
|
+
type FixPassAgentTracker,
|
|
22
|
+
} from "./runtime.js";
|
|
23
|
+
import type { ParsedRequest, ReviewMessageDetails } from "./schema.js";
|
|
24
|
+
|
|
25
|
+
const FIX_PASS_START_GRACE_MS = 1_000;
|
|
26
|
+
|
|
27
|
+
const REVIEW_STALE_REUSE_WARNING =
|
|
28
|
+
"Last review is stale. Continuing with /fix. Run /review first to refresh it.";
|
|
29
|
+
|
|
30
|
+
function buildFixPrompt(
|
|
31
|
+
reviewMessageDetails: ReviewMessageDetails,
|
|
32
|
+
additionalContext: string | undefined,
|
|
33
|
+
): string {
|
|
34
|
+
const worklistPayload = JSON.stringify(
|
|
35
|
+
{
|
|
36
|
+
scope: reviewMessageDetails.scope,
|
|
37
|
+
...(reviewMessageDetails.staleness ? { staleness: reviewMessageDetails.staleness } : {}),
|
|
38
|
+
findings: reviewMessageDetails.findings.map(
|
|
39
|
+
({ priority, location, finding, suggestion, focus }) => ({
|
|
40
|
+
priority,
|
|
41
|
+
location,
|
|
42
|
+
finding,
|
|
43
|
+
suggestion,
|
|
44
|
+
focus,
|
|
45
|
+
}),
|
|
46
|
+
),
|
|
47
|
+
},
|
|
48
|
+
null,
|
|
49
|
+
2,
|
|
50
|
+
);
|
|
51
|
+
const additionalContextSection = buildAdditionalContextSection(additionalContext);
|
|
52
|
+
|
|
53
|
+
return FIX_PROMPT.replace(
|
|
54
|
+
"{FIX_ADDITIONAL_CONTEXT_SECTION}",
|
|
55
|
+
() => additionalContextSection,
|
|
56
|
+
).replace("{REVIEW_FINDINGS_JSON}", () => worklistPayload);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function prepareFixReviewDetails(
|
|
60
|
+
pi: ExtensionAPI,
|
|
61
|
+
ctx: ExtensionCommandContext,
|
|
62
|
+
request: ParsedRequest,
|
|
63
|
+
forceFreshReview = false,
|
|
64
|
+
): Promise<ReviewMessageDetails | null> {
|
|
65
|
+
let reviewDetails = forceFreshReview ? null : getLastMessageReviewDetails(ctx);
|
|
66
|
+
|
|
67
|
+
if (reviewDetails && reviewMatchesFixRequest(reviewDetails, request)) {
|
|
68
|
+
const currentFingerprint = await computeCurrentFingerprint(
|
|
69
|
+
pi,
|
|
70
|
+
ctx.cwd,
|
|
71
|
+
reviewDetails.scope.mode === "working-tree" || reviewDetails.scope.mode === "folder",
|
|
72
|
+
);
|
|
73
|
+
const hasStalePayload = reviewDetails.staleness?.status === "stale";
|
|
74
|
+
const isStaleNow = !fingerprintsEqual(reviewDetails.fingerprint, currentFingerprint);
|
|
75
|
+
if (hasStalePayload || isStaleNow) {
|
|
76
|
+
reviewDetails = {
|
|
77
|
+
...reviewDetails,
|
|
78
|
+
staleness: reviewDetails.staleness ?? buildStalePayloadStaleness(),
|
|
79
|
+
};
|
|
80
|
+
notify(ctx, REVIEW_STALE_REUSE_WARNING, "warning");
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
const reviewResult = await runReviewPipeline(pi, ctx, request, "fix");
|
|
84
|
+
if (!reviewResult.ok) {
|
|
85
|
+
if (reviewResult.error === REVIEW_CANCELLED_ERROR) {
|
|
86
|
+
notify(ctx, REVIEW_CANCELLED_ERROR, "error");
|
|
87
|
+
} else {
|
|
88
|
+
notify(ctx, `Cannot continue to /fix: ${reviewResult.error}`, "error");
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
reviewDetails = reviewResult.details;
|
|
93
|
+
if (reviewDetails.staleness?.status === "stale") {
|
|
94
|
+
notify(ctx, reviewDetails.staleness.nextStep, "warning");
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (reviewDetails.findings.length === 0) {
|
|
100
|
+
const failedFocusCount = countFailedFocusRuns(reviewDetails);
|
|
101
|
+
if (failedFocusCount > 0) {
|
|
102
|
+
notify(
|
|
103
|
+
ctx,
|
|
104
|
+
`Latest partial review had ${failedFocusCount} failed focus run(s) and no findings. Rerun /review before /fix.`,
|
|
105
|
+
"warning",
|
|
106
|
+
);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
notify(ctx, "Review produced no findings. Nothing to fix.", "info");
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return reviewDetails;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function countFailedFocusRuns(reviewDetails: ReviewMessageDetails): number {
|
|
118
|
+
return reviewDetails.focusStatus.filter((focus) => !focus.ok).length;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function notifyFixUsedPartialReview(ctx: ExtensionContext, failedFocusCount: number): void {
|
|
122
|
+
if (failedFocusCount === 0) return;
|
|
123
|
+
const failedLabel = `review${failedFocusCount === 1 ? "" : "s"}`;
|
|
124
|
+
notify(
|
|
125
|
+
ctx,
|
|
126
|
+
`Fix pass used partial review results: ${failedFocusCount} ${failedLabel} failed.`,
|
|
127
|
+
"warning",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function queueFixPass(
|
|
132
|
+
pi: ExtensionAPI,
|
|
133
|
+
ctx: ExtensionCommandContext,
|
|
134
|
+
reviewDetails: ReviewMessageDetails,
|
|
135
|
+
additionalContext: string | undefined,
|
|
136
|
+
options: { forceFollowUp?: boolean } = {},
|
|
137
|
+
): { startsImmediately: boolean } {
|
|
138
|
+
const fixPrompt = buildFixPrompt(reviewDetails, additionalContext);
|
|
139
|
+
const startsImmediately = ctx.isIdle();
|
|
140
|
+
const delivery =
|
|
141
|
+
options.forceFollowUp || !startsImmediately || ctx.hasPendingMessages()
|
|
142
|
+
? { deliverAs: "followUp" as const }
|
|
143
|
+
: undefined;
|
|
144
|
+
pi.sendUserMessage(fixPrompt, delivery);
|
|
145
|
+
notify(ctx, "Queued autonomous fix pass from review findings.", "info");
|
|
146
|
+
return { startsImmediately };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getLastAssistantMessage(messages: AgentEndMessages): AgentEndMessage | undefined {
|
|
150
|
+
return messages.findLast((message) => message?.role === "assistant");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function wasLastAssistantAborted(messages: AgentEndMessages): boolean {
|
|
154
|
+
return getLastAssistantMessage(messages)?.stopReason === "aborted";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function waitForPromptStartIfImmediate(
|
|
158
|
+
agentTracker: FixPassAgentTracker,
|
|
159
|
+
startCountBeforePrompt: number,
|
|
160
|
+
startsImmediately: boolean,
|
|
161
|
+
): Promise<void> {
|
|
162
|
+
if (!startsImmediately) return;
|
|
163
|
+
await agentTracker.waitForStartAfter(startCountBeforePrompt, FIX_PASS_START_GRACE_MS);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function waitForFixPassCompletion(
|
|
167
|
+
agentTracker: FixPassAgentTracker,
|
|
168
|
+
): Promise<AgentEndMessages> {
|
|
169
|
+
await agentTracker.waitForNextSettled();
|
|
170
|
+
return agentTracker.getLastEnd()?.messages ?? [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function runFixPassFromReview(
|
|
174
|
+
pi: ExtensionAPI,
|
|
175
|
+
ctx: ExtensionCommandContext,
|
|
176
|
+
reviewDetails: ReviewMessageDetails,
|
|
177
|
+
additionalContext: string | undefined,
|
|
178
|
+
agentTracker: FixPassAgentTracker,
|
|
179
|
+
reviewMessageQueue: ReviewMessageQueue,
|
|
180
|
+
): Promise<AgentEndMessages> {
|
|
181
|
+
const failedFocusCount = countFailedFocusRuns(reviewDetails);
|
|
182
|
+
const fixPassFinished = waitForFixPassCompletion(agentTracker);
|
|
183
|
+
|
|
184
|
+
const startCountBeforeSteering = agentTracker.getStartCount();
|
|
185
|
+
const steeringStartsImmediately = ctx.isIdle();
|
|
186
|
+
const sentSteering = reviewMessageQueue.flushSteering(ctx, { forceFollowUp: true });
|
|
187
|
+
await waitForPromptStartIfImmediate(
|
|
188
|
+
agentTracker,
|
|
189
|
+
startCountBeforeSteering,
|
|
190
|
+
sentSteering && steeringStartsImmediately,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const startCountBeforeFix = agentTracker.getStartCount();
|
|
194
|
+
const fixPass = queueFixPass(pi, ctx, reviewDetails, additionalContext, {
|
|
195
|
+
forceFollowUp: sentSteering,
|
|
196
|
+
});
|
|
197
|
+
await waitForPromptStartIfImmediate(agentTracker, startCountBeforeFix, fixPass.startsImmediately);
|
|
198
|
+
|
|
199
|
+
const fixMessages = await fixPassFinished;
|
|
200
|
+
if (!wasLastAssistantAborted(fixMessages)) {
|
|
201
|
+
notifyFixUsedPartialReview(ctx, failedFocusCount);
|
|
202
|
+
}
|
|
203
|
+
return fixMessages;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function runFixLoop(
|
|
207
|
+
pi: ExtensionAPI,
|
|
208
|
+
ctx: ExtensionCommandContext,
|
|
209
|
+
request: ParsedRequest,
|
|
210
|
+
agentTracker: FixPassAgentTracker,
|
|
211
|
+
reviewMessageQueue: ReviewMessageQueue,
|
|
212
|
+
): Promise<void> {
|
|
213
|
+
for (;;) {
|
|
214
|
+
const reviewDetails = await prepareFixReviewDetails(pi, ctx, request, true);
|
|
215
|
+
if (!reviewDetails) return;
|
|
216
|
+
|
|
217
|
+
const beforeFixFingerprint = reviewDetails.fingerprint;
|
|
218
|
+
const fixMessages = await runFixPassFromReview(
|
|
219
|
+
pi,
|
|
220
|
+
ctx,
|
|
221
|
+
reviewDetails,
|
|
222
|
+
request.additionalContext,
|
|
223
|
+
agentTracker,
|
|
224
|
+
reviewMessageQueue,
|
|
225
|
+
);
|
|
226
|
+
if (wasLastAssistantAborted(fixMessages)) {
|
|
227
|
+
notify(ctx, "Fix loop stopped: fix pass was aborted.", "warning");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const afterFixFingerprint = await computeCurrentFingerprint(pi, ctx.cwd, true);
|
|
232
|
+
if (fingerprintsEqual(beforeFixFingerprint, afterFixFingerprint)) {
|
|
233
|
+
notify(ctx, "Fix loop stopped: fix pass made no repository changes.", "warning");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
notify(ctx, "Fix loop continuing with a fresh review...", "info");
|
|
238
|
+
}
|
|
239
|
+
}
|