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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/extensions/answer.ts +48 -42
  3. package/extensions/branch-term.ts +8 -13
  4. package/extensions/btw.ts +4 -4
  5. package/extensions/ghostty.ts +64 -11
  6. package/extensions/insights.ts +4 -3
  7. package/extensions/loop.ts +37 -9
  8. package/extensions/memory.ts +5 -10
  9. package/extensions/notify.ts +24 -5
  10. package/extensions/openai-fast.ts +6 -4
  11. package/extensions/openai-verbosity.ts +6 -4
  12. package/extensions/review/fix.ts +239 -0
  13. package/extensions/review/git.ts +517 -0
  14. package/extensions/review/index.ts +232 -0
  15. package/extensions/review/message-queue.ts +249 -0
  16. package/extensions/review/models.ts +589 -0
  17. package/extensions/review/prompts.ts +337 -0
  18. package/extensions/review/renderers/inline.ts +126 -0
  19. package/extensions/review/request.ts +504 -0
  20. package/extensions/review/review.ts +1305 -0
  21. package/extensions/review/runner.ts +522 -0
  22. package/extensions/review/runtime.ts +275 -0
  23. package/extensions/review/schema.ts +160 -0
  24. package/extensions/review/submit-review-tool.ts +66 -0
  25. package/extensions/review/submit-triage-tool.ts +59 -0
  26. package/extensions/review/triage.ts +746 -0
  27. package/extensions/sandbox/index.ts +399 -102
  28. package/extensions/{interlude.ts → stash.ts} +13 -11
  29. package/extensions/tool-display-mode.ts +2 -3
  30. package/extensions/usage/index.ts +8 -4
  31. package/extensions/websearch/README.md +1 -1
  32. package/extensions/websearch/browser/chromium.ts +42 -31
  33. package/extensions/websearch/browser/discovery.ts +11 -9
  34. package/extensions/websearch/browser/firefox.ts +10 -7
  35. package/extensions/websearch/browser/fs.ts +10 -0
  36. package/extensions/websearch/browser/sqlite.ts +22 -15
  37. package/extensions/websearch/config.ts +2 -2
  38. package/extensions/websearch/index.ts +6 -6
  39. package/extensions/worktree.ts +33 -33
  40. package/package.json +3 -3
  41. package/skills/git-clean-history/SKILL.md +1 -0
  42. package/skills/git-commit/SKILL.md +2 -2
  43. package/skills/oracle/scripts/oracle +28 -11
  44. package/themes/tau-dark.json +2 -1
  45. package/extensions/review.ts +0 -4420
@@ -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("agent_end", async (_event, ctx) => {
166
+ pi.on("agent_settled", async (_event, ctx) => {
147
167
  currentSessionKey = getSessionKey(ctx);
148
- if (pendingPromptCount > 0) return;
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 type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
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
- const agentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
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 type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
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
- const agentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
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
+ }