tau-coding-agent 0.1.3 → 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 +49 -43
- package/extensions/branch-term.ts +11 -16
- package/extensions/btw.ts +8 -8
- package/extensions/ghostty.ts +65 -12
- package/extensions/git-diff-stats.ts +1 -1
- package/extensions/git-pr-status.ts +1 -1
- package/extensions/insights.ts +6 -5
- package/extensions/loop.ts +39 -11
- package/extensions/memory.ts +6 -5
- package/extensions/notify.ts +25 -6
- 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 +428 -103
- package/extensions/{interlude.ts → stash.ts} +13 -11
- package/extensions/tool-display-mode.ts +173 -17
- package/extensions/usage/anthropic.ts +1 -1
- package/extensions/usage/github-copilot.ts +1 -1
- package/extensions/usage/index.ts +9 -5
- package/extensions/usage/minimax.ts +1 -1
- package/extensions/usage/openai-codex.ts +1 -1
- package/extensions/usage/openrouter.ts +1 -1
- package/extensions/usage/providers.ts +0 -2
- package/extensions/usage/types.ts +1 -2
- package/extensions/usage/zai.ts +1 -1
- 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 +9 -9
- package/extensions/websearch/providers/anthropic.pi.ts +1 -1
- package/extensions/websearch/providers/gemini.pi.ts +1 -1
- package/extensions/websearch/providers/openai-codex.pi.ts +1 -1
- package/extensions/websearch/providers/pi-model.shared.ts +2 -2
- package/extensions/worktree.ts +36 -36
- package/package.json +7 -7
- package/skills/git-clean-history/SKILL.md +1 -0
- package/skills/git-commit/SKILL.md +2 -2
- package/skills/oracle/scripts/oracle +28 -12
- package/themes/tau-dark.json +2 -1
- package/extensions/review.ts +0 -4347
- package/extensions/usage/google-gemini-cli.ts +0 -232
package/extensions/loop.ts
CHANGED
|
@@ -7,15 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { Type } from "typebox";
|
|
10
|
-
import { complete
|
|
10
|
+
import { complete } from "@earendil-works/pi-ai/compat";
|
|
11
|
+
import type { Api, Model, UserMessage } from "@earendil-works/pi-ai";
|
|
11
12
|
import {
|
|
12
13
|
compact,
|
|
13
14
|
defineTool,
|
|
14
15
|
DynamicBorder,
|
|
15
16
|
type ExtensionAPI,
|
|
16
17
|
type ExtensionContext,
|
|
17
|
-
} from "@
|
|
18
|
-
import { Container, type SelectItem, SelectList, Text } from "@
|
|
18
|
+
} from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
|
|
19
20
|
|
|
20
21
|
const LOOP_PRESETS = [
|
|
21
22
|
{ value: "tests", label: "Until tests pass", description: "" },
|
|
@@ -213,6 +214,8 @@ async function loadState(ctx: ExtensionContext): Promise<LoopStateData> {
|
|
|
213
214
|
|
|
214
215
|
export default function loopExtension(pi: ExtensionAPI): void {
|
|
215
216
|
let loopState: LoopStateData = { active: false };
|
|
217
|
+
let lastAgentEndMessages: Array<{ role?: string; stopReason?: string }> = [];
|
|
218
|
+
let pendingLoopPrompt: ReturnType<typeof setImmediate> | undefined;
|
|
216
219
|
|
|
217
220
|
function persistState(state: LoopStateData): void {
|
|
218
221
|
pi.appendEntry(LOOP_STATE_ENTRY, state);
|
|
@@ -271,6 +274,23 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
271
274
|
);
|
|
272
275
|
}
|
|
273
276
|
|
|
277
|
+
function scheduleLoopPrompt(ctx: ExtensionContext): void {
|
|
278
|
+
if (pendingLoopPrompt) return;
|
|
279
|
+
|
|
280
|
+
pendingLoopPrompt = setImmediate(() => {
|
|
281
|
+
pendingLoopPrompt = undefined;
|
|
282
|
+
if (!loopState.active || !ctx.isIdle()) return;
|
|
283
|
+
triggerLoopPrompt(ctx);
|
|
284
|
+
});
|
|
285
|
+
pendingLoopPrompt.unref?.();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function cancelScheduledLoopPrompt(): void {
|
|
289
|
+
if (!pendingLoopPrompt) return;
|
|
290
|
+
clearImmediate(pendingLoopPrompt);
|
|
291
|
+
pendingLoopPrompt = undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
274
294
|
async function showLoopSelector(ctx: ExtensionContext): Promise<LoopStateData | null> {
|
|
275
295
|
const items: SelectItem[] = LOOP_PRESETS.map((preset) => ({
|
|
276
296
|
value: preset.value,
|
|
@@ -385,11 +405,11 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
385
405
|
name: "signal_loop_success",
|
|
386
406
|
label: "Signal Loop Success",
|
|
387
407
|
description:
|
|
388
|
-
"Stop the active loop when the breakout condition is satisfied. Only call
|
|
408
|
+
"Stop the active loop when the breakout condition is satisfied. Only call signal_loop_success when explicitly instructed to do so by the user, tool, or system prompt.",
|
|
389
409
|
promptSnippet: "Signal that the active /loop breakout condition has been satisfied",
|
|
390
410
|
promptGuidelines: [
|
|
391
|
-
"Call
|
|
392
|
-
"Do not call
|
|
411
|
+
"Call signal_loop_success only when the active loop's breakout condition is actually satisfied.",
|
|
412
|
+
"Do not call signal_loop_success unless a loop is currently active.",
|
|
393
413
|
],
|
|
394
414
|
parameters: Type.Object({}),
|
|
395
415
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
@@ -416,7 +436,7 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
416
436
|
handler: async (args, ctx) => {
|
|
417
437
|
let nextState = parseArgs(args);
|
|
418
438
|
if (!nextState) {
|
|
419
|
-
if (
|
|
439
|
+
if (ctx.mode !== "tui") {
|
|
420
440
|
ctx.ui.notify("Usage: /loop tests | /loop custom <condition> | /loop self", "warning");
|
|
421
441
|
return;
|
|
422
442
|
}
|
|
@@ -458,10 +478,14 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
458
478
|
},
|
|
459
479
|
});
|
|
460
480
|
|
|
461
|
-
pi.on("agent_end", async (event
|
|
481
|
+
pi.on("agent_end", async (event) => {
|
|
482
|
+
lastAgentEndMessages = event.messages;
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
462
486
|
if (!loopState.active) return;
|
|
463
487
|
|
|
464
|
-
if (ctx.hasUI && wasLastAssistantAborted(
|
|
488
|
+
if (ctx.hasUI && wasLastAssistantAborted(lastAgentEndMessages)) {
|
|
465
489
|
const confirm = await withPromptSignal(pi, () =>
|
|
466
490
|
ctx.ui.confirm("Break active loop?", "Operation aborted. Break out of the loop?"),
|
|
467
491
|
);
|
|
@@ -471,13 +495,13 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
471
495
|
}
|
|
472
496
|
}
|
|
473
497
|
|
|
474
|
-
|
|
498
|
+
scheduleLoopPrompt(ctx);
|
|
475
499
|
});
|
|
476
500
|
|
|
477
501
|
pi.on("session_before_compact", async (event, ctx) => {
|
|
478
502
|
if (!loopState.active || !loopState.mode || !ctx.model) return;
|
|
479
503
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
|
|
480
|
-
if (!auth.ok
|
|
504
|
+
if (!auth.ok) return;
|
|
481
505
|
|
|
482
506
|
const instructionParts = [
|
|
483
507
|
event.customInstructions,
|
|
@@ -526,4 +550,8 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
526
550
|
pi.on("session_start", async (_event, ctx) => {
|
|
527
551
|
await restoreLoopState(ctx);
|
|
528
552
|
});
|
|
553
|
+
|
|
554
|
+
pi.on("session_shutdown", async () => {
|
|
555
|
+
cancelScheduledLoopPrompt();
|
|
556
|
+
});
|
|
529
557
|
}
|
package/extensions/memory.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { complete
|
|
1
|
+
import { complete } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
import { StringEnum, type Api, type Model, type UserMessage } from "@earendil-works/pi-ai";
|
|
2
3
|
import {
|
|
3
4
|
defineTool,
|
|
4
5
|
withFileMutationQueue,
|
|
5
6
|
type ExtensionAPI,
|
|
6
7
|
type ExtensionCommandContext,
|
|
7
8
|
type ExtensionContext,
|
|
8
|
-
} from "@
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import { Type } from "typebox";
|
|
10
11
|
import fs from "node:fs/promises";
|
|
11
12
|
import path from "node:path";
|
|
@@ -1704,7 +1705,7 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
1704
1705
|
description: `Update one .agents/memory/core block while enforcing the shared ${CORE_LINE_CAP}-line and ${CORE_CHAR_CAP}-character caps`,
|
|
1705
1706
|
promptSnippet: `Update one core memory block in .agents/memory/core with ${CORE_LINE_CAP}-line and ${CORE_CHAR_CAP}-character cap enforcement`,
|
|
1706
1707
|
promptGuidelines: [
|
|
1707
|
-
"Use
|
|
1708
|
+
"Use memory_update_block when updating .agents/memory/core/directives.md, context.md, focus.md, or pending.md.",
|
|
1708
1709
|
`If a write would exceed the ${CORE_LINE_CAP}-line or ${CORE_CHAR_CAP}-character core cap, run memory_dream or remove content first.`,
|
|
1709
1710
|
],
|
|
1710
1711
|
parameters: MEMORY_UPDATE_BLOCK_PARAMS,
|
|
@@ -1735,7 +1736,7 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
1735
1736
|
description: "Append an entry to .agents/memory/log.md using the repo memory log format",
|
|
1736
1737
|
promptSnippet: "Append an entry to the repo memory log at .agents/memory/log.md",
|
|
1737
1738
|
promptGuidelines: [
|
|
1738
|
-
"Use
|
|
1739
|
+
"Use memory_append_log for important decisions, discoveries, plans, experiments, prompt ingests, and attachment additions worth remembering.",
|
|
1739
1740
|
"Use importance labels high, medium, or low.",
|
|
1740
1741
|
"If this entry replaces or corrects prior memory, set supersedes and/or invalidates links explicitly.",
|
|
1741
1742
|
"The memory log is append-only. Do not rewrite or truncate older entries.",
|
|
@@ -1773,7 +1774,7 @@ export default function memoryExtension(pi: ExtensionAPI): void {
|
|
|
1773
1774
|
promptSnippet: "Consolidate repo memory with dream-based core compression",
|
|
1774
1775
|
promptGuidelines: [
|
|
1775
1776
|
"Dream is the only consolidation mechanism for .agents/memory/core.",
|
|
1776
|
-
"Use
|
|
1777
|
+
"Use memory_dream when logs have accumulated, core needs compression, or recent compaction context should be folded into memory.",
|
|
1777
1778
|
],
|
|
1778
1779
|
parameters: MEMORY_DREAM_PARAMS,
|
|
1779
1780
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
package/extensions/notify.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* - Windows toast: Windows Terminal (WSL)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
14
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
15
|
|
|
15
16
|
const REVIEW_EVENT_START = "review:start";
|
|
16
17
|
const REVIEW_EVENT_END = "review:end";
|
|
@@ -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
|
+
}
|