tau-coding-agent 0.1.4 → 0.1.6

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 (51) hide show
  1. package/README.md +1 -1
  2. package/extensions/answer.ts +59 -47
  3. package/extensions/branch-term.ts +8 -13
  4. package/extensions/btw.ts +77 -6
  5. package/extensions/ghostty.ts +64 -11
  6. package/extensions/insights.ts +42 -4
  7. package/extensions/loop.ts +51 -12
  8. package/extensions/memory.ts +35 -17
  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 +62 -0
  25. package/extensions/review/submit-triage-tool.ts +60 -0
  26. package/extensions/review/triage.ts +746 -0
  27. package/extensions/sandbox/index.ts +976 -185
  28. package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
  29. package/extensions/{interlude.ts → stash.ts} +13 -11
  30. package/extensions/tool-display-mode.ts +2 -3
  31. package/extensions/usage/anthropic.ts +5 -6
  32. package/extensions/usage/github-copilot.ts +30 -9
  33. package/extensions/usage/index.ts +9 -5
  34. package/extensions/usage/openai-codex.ts +2 -2
  35. package/extensions/websearch/README.md +1 -1
  36. package/extensions/websearch/browser/chromium.ts +42 -31
  37. package/extensions/websearch/browser/discovery.ts +11 -9
  38. package/extensions/websearch/browser/firefox.ts +10 -7
  39. package/extensions/websearch/browser/fs.ts +10 -0
  40. package/extensions/websearch/browser/sqlite.ts +22 -15
  41. package/extensions/websearch/config.ts +2 -2
  42. package/extensions/websearch/index.ts +6 -6
  43. package/extensions/websearch/providers/pi-model.shared.ts +19 -1
  44. package/extensions/worktree.ts +33 -33
  45. package/package.json +4 -4
  46. package/skills/git-clean-history/SKILL.md +1 -0
  47. package/skills/git-commit/SKILL.md +2 -2
  48. package/skills/oracle/SKILL.md +3 -3
  49. package/skills/oracle/scripts/oracle +82 -61
  50. package/themes/tau-dark.json +2 -1
  51. package/extensions/review.ts +0 -4420
@@ -0,0 +1,275 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
4
+ ExtensionContext,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { matchesKey } from "@earendil-works/pi-tui";
7
+ import type { ChildProcess } from "node:child_process";
8
+
9
+ import type { ReviewRunOutcome, ReviewRunSource } from "./schema.js";
10
+
11
+ export const REVIEW_CANCELLED_ERROR = "Review aborted";
12
+
13
+ export const REVIEW_EVENT_START = "review:start";
14
+ export const REVIEW_EVENT_END = "review:end";
15
+
16
+ export const REVIEW_STATUS_KEY = "0-review";
17
+
18
+ export const STATUS_SPINNER_INTERVAL_MS = 80;
19
+
20
+ export const STATUS_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
21
+
22
+ export type ReviewExecutionControl = {
23
+ isCancelled: () => boolean;
24
+ registerProcess: (proc: ChildProcess) => () => void;
25
+ };
26
+
27
+ export type ManagedReviewRun = {
28
+ control: ReviewExecutionControl;
29
+ markSuccessful: () => void;
30
+ markCancelled: () => void;
31
+ };
32
+
33
+ export type AgentEndMessage = {
34
+ role?: string;
35
+ stopReason?: string;
36
+ errorMessage?: string;
37
+ };
38
+
39
+ export type AgentEndMessages = AgentEndMessage[];
40
+
41
+ export type AgentEndState = {
42
+ messages: AgentEndMessages;
43
+ };
44
+
45
+ export type FixPassAgentTracker = {
46
+ waitForNextSettled: () => Promise<void>;
47
+ waitForStartAfter: (lastSeenStartCount: number, timeoutMs: number) => Promise<boolean>;
48
+ getStartCount: () => number;
49
+ getLastEnd: () => AgentEndState | undefined;
50
+ };
51
+
52
+ export type AgentRunTracker = FixPassAgentTracker & {
53
+ handleStart: () => void;
54
+ handleEnd: (state: AgentEndState) => void;
55
+ handleSettled: () => void;
56
+ reset: () => void;
57
+ };
58
+
59
+ const runtimeState = {
60
+ activeReviewRuns: new Set<string>(),
61
+ activeReviewCancels: new Map<string, () => void>(),
62
+ activePromptCount: 0,
63
+ };
64
+
65
+ export async function withManagedReviewRun<T>(
66
+ pi: ExtensionAPI,
67
+ ctx: ExtensionCommandContext,
68
+ source: ReviewRunSource,
69
+ run: (managed: ManagedReviewRun) => Promise<T>,
70
+ ): Promise<T> {
71
+ const sessionKey = getReviewSessionKey(ctx);
72
+ const activeProcesses = new Set<ChildProcess>();
73
+ let cancelRequested = false;
74
+ let outcome: ReviewRunOutcome = "failed";
75
+
76
+ const cancelActiveProcesses = () => {
77
+ for (const proc of activeProcesses) {
78
+ try {
79
+ proc.kill("SIGKILL");
80
+ } catch {
81
+ // Best effort.
82
+ }
83
+ }
84
+ activeProcesses.clear();
85
+ };
86
+
87
+ const requestCancellation = () => {
88
+ if (cancelRequested) return;
89
+ cancelRequested = true;
90
+ cancelActiveProcesses();
91
+ };
92
+
93
+ const control: ReviewExecutionControl = {
94
+ isCancelled: () => cancelRequested,
95
+ registerProcess: (proc) => {
96
+ activeProcesses.add(proc);
97
+ return () => {
98
+ activeProcesses.delete(proc);
99
+ };
100
+ },
101
+ };
102
+
103
+ pi.events.emit(REVIEW_EVENT_START, { sessionKey, source });
104
+ runtimeState.activeReviewCancels.set(sessionKey, requestCancellation);
105
+ const unsubscribeInterrupt = ctx.hasUI
106
+ ? ctx.ui.onTerminalInput((data) => {
107
+ if (!matchesKey(data, "escape")) return undefined;
108
+ if (runtimeState.activePromptCount > 0) return undefined;
109
+ requestCancellation();
110
+ return { consume: true };
111
+ })
112
+ : undefined;
113
+
114
+ try {
115
+ return await run({
116
+ control,
117
+ markSuccessful: () => {
118
+ outcome = "success";
119
+ },
120
+ markCancelled: () => {
121
+ outcome = "cancelled";
122
+ },
123
+ });
124
+ } finally {
125
+ unsubscribeInterrupt?.();
126
+ cancelActiveProcesses();
127
+ if (runtimeState.activeReviewCancels.get(sessionKey) === requestCancellation) {
128
+ runtimeState.activeReviewCancels.delete(sessionKey);
129
+ }
130
+ pi.events.emit(REVIEW_EVENT_END, { sessionKey, source, outcome });
131
+ }
132
+ }
133
+
134
+ export function notify(
135
+ ctx: ExtensionContext,
136
+ message: string,
137
+ type: "info" | "warning" | "error" = "info",
138
+ ) {
139
+ if (!ctx.hasUI) return;
140
+ ctx.ui.notify(message, type);
141
+ }
142
+
143
+ export function getReviewSessionKey(ctx: ExtensionContext): string {
144
+ return ctx.sessionManager.getSessionFile() ?? `session:${ctx.sessionManager.getSessionId()}`;
145
+ }
146
+
147
+ export async function withSpinner<T>(
148
+ ctx: ExtensionContext,
149
+ buildStatusText: () => string,
150
+ run: () => Promise<T>,
151
+ ): Promise<T> {
152
+ if (!ctx.hasUI) return run();
153
+
154
+ let frame = 0;
155
+ const render = () => {
156
+ const spinner = STATUS_SPINNER_FRAMES[frame % STATUS_SPINNER_FRAMES.length];
157
+ ctx.ui.setStatus(REVIEW_STATUS_KEY, `${spinner} ${buildStatusText()}`);
158
+ };
159
+
160
+ render();
161
+ const timer = setInterval(() => {
162
+ frame = (frame + 1) % STATUS_SPINNER_FRAMES.length;
163
+ render();
164
+ }, STATUS_SPINNER_INTERVAL_MS);
165
+
166
+ try {
167
+ return await run();
168
+ } finally {
169
+ clearInterval(timer);
170
+ ctx.ui.setStatus(REVIEW_STATUS_KEY, undefined);
171
+ }
172
+ }
173
+
174
+ export function acquireReviewRunLock(ctx: ExtensionContext, busyMessage: string): string | null {
175
+ const sessionKey = getReviewSessionKey(ctx);
176
+ if (runtimeState.activeReviewRuns.has(sessionKey)) {
177
+ notify(ctx, busyMessage, "warning");
178
+ return null;
179
+ }
180
+
181
+ runtimeState.activeReviewRuns.add(sessionKey);
182
+ return sessionKey;
183
+ }
184
+
185
+ export function releaseReviewRunLock(sessionKey: string): void {
186
+ runtimeState.activeReviewRuns.delete(sessionKey);
187
+ }
188
+
189
+ export function createAgentRunTracker(): AgentRunTracker {
190
+ let resolveNextAgentSettled: (() => void) | undefined;
191
+ let resolveNextAgentStart: (() => void) | undefined;
192
+ let lastAgentEnd: AgentEndState | undefined;
193
+ let agentStartCount = 0;
194
+
195
+ function waitForNextSettled(): Promise<void> {
196
+ return new Promise((resolve) => {
197
+ resolveNextAgentSettled = resolve;
198
+ });
199
+ }
200
+
201
+ function waitForStartAfter(lastSeenStartCount: number, timeoutMs: number): Promise<boolean> {
202
+ if (agentStartCount > lastSeenStartCount) return Promise.resolve(true);
203
+
204
+ return new Promise((resolve) => {
205
+ let settled = false;
206
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
207
+ let onStart: (() => void) | undefined;
208
+
209
+ const finish = (started: boolean) => {
210
+ if (settled) return;
211
+ settled = true;
212
+ if (timeoutId) clearTimeout(timeoutId);
213
+ if (resolveNextAgentStart === onStart) resolveNextAgentStart = undefined;
214
+ resolve(started);
215
+ };
216
+
217
+ onStart = () => finish(true);
218
+ timeoutId = setTimeout(() => finish(false), Math.max(0, timeoutMs));
219
+ resolveNextAgentStart = onStart;
220
+ });
221
+ }
222
+
223
+ return {
224
+ waitForNextSettled,
225
+ waitForStartAfter,
226
+ getStartCount: () => agentStartCount,
227
+ getLastEnd: () => lastAgentEnd,
228
+ handleStart: () => {
229
+ agentStartCount += 1;
230
+ const resolve = resolveNextAgentStart;
231
+ if (!resolve) return;
232
+ resolveNextAgentStart = undefined;
233
+ resolve();
234
+ },
235
+ handleEnd: (state) => {
236
+ lastAgentEnd = state;
237
+ },
238
+ handleSettled: () => {
239
+ const resolve = resolveNextAgentSettled;
240
+ if (!resolve) return;
241
+ resolveNextAgentSettled = undefined;
242
+ resolve();
243
+ },
244
+ reset: () => {
245
+ resolveNextAgentSettled = undefined;
246
+ resolveNextAgentStart = undefined;
247
+ lastAgentEnd = undefined;
248
+ agentStartCount = 0;
249
+ },
250
+ };
251
+ }
252
+
253
+ export function recordPromptStart(): void {
254
+ runtimeState.activePromptCount += 1;
255
+ }
256
+
257
+ export function recordPromptEnd(): void {
258
+ runtimeState.activePromptCount = Math.max(0, runtimeState.activePromptCount - 1);
259
+ }
260
+
261
+ export function handleReviewSessionStart(ctx: ExtensionContext): void {
262
+ runtimeState.activePromptCount = 0;
263
+ const sessionKey = getReviewSessionKey(ctx);
264
+ for (const [key, cancel] of runtimeState.activeReviewCancels) {
265
+ if (key !== sessionKey) cancel();
266
+ }
267
+ }
268
+
269
+ export function handleReviewSessionShutdown(ctx: ExtensionContext): void {
270
+ const sessionKey = getReviewSessionKey(ctx);
271
+ runtimeState.activeReviewCancels.get(sessionKey)?.();
272
+ runtimeState.activeReviewCancels.delete(sessionKey);
273
+ runtimeState.activeReviewRuns.delete(sessionKey);
274
+ runtimeState.activePromptCount = 0;
275
+ }
@@ -0,0 +1,160 @@
1
+ export const REVIEW_COMMENT_SCHEMA = "pi.review.comment.v1" as const;
2
+
3
+ export const REVIEW_OUTPUT_FORMATS = ["inline", "jsonl", "html"] as const;
4
+
5
+ export type ReviewOutputFormat = (typeof REVIEW_OUTPUT_FORMATS)[number];
6
+
7
+ export type Priority = "P0" | "P1" | "P2" | "P3";
8
+
9
+ export const REVIEW_FOCUS_NAMES = [
10
+ "general",
11
+ "security",
12
+ "reuse",
13
+ "quality",
14
+ "testing",
15
+ "efficiency",
16
+ ] as const;
17
+
18
+ export type ReviewFocus = (typeof REVIEW_FOCUS_NAMES)[number];
19
+ export type ReviewRunSource = "review" | "fix" | "triage";
20
+ export type ReviewRunOutcome = "success" | "failed" | "cancelled";
21
+
22
+ export type ReviewTarget =
23
+ | { type: "auto" }
24
+ | { type: "uncommitted" }
25
+ | { type: "branch"; branch: string }
26
+ | { type: "commit"; sha: string }
27
+ | { type: "pr"; ref: string }
28
+ | { type: "folder"; paths: string[] }
29
+ | { type: "custom"; instructions: string };
30
+
31
+ export type ReviewRequestMode =
32
+ | "auto"
33
+ | "uncommitted"
34
+ | `branch:${string}`
35
+ | `commit:${string}`
36
+ | `pr:${string}`
37
+ | `folder:${string}`
38
+ | "custom";
39
+
40
+ export type ParsedRequest = {
41
+ target: ReviewTarget;
42
+ mode: ReviewRequestMode;
43
+ models: string[];
44
+ focuses: ReviewFocus[];
45
+ targetExplicit: boolean;
46
+ focusExplicit: boolean;
47
+ additionalContext?: string;
48
+ };
49
+
50
+ export type RequestSignaturePayload = {
51
+ target: unknown;
52
+ models: string[];
53
+ focuses: ReviewFocus[];
54
+ additionalContext?: string | null;
55
+ };
56
+
57
+ export type ReviewFingerprint = {
58
+ headSha: string;
59
+ branch: string;
60
+ trackedDiffHash: string;
61
+ untrackedHash: string;
62
+ };
63
+
64
+ export type FocusFinding = {
65
+ priority: Priority;
66
+ location: string;
67
+ finding: string;
68
+ suggestion: string;
69
+ };
70
+
71
+ export type FocusOutput = {
72
+ focus: ReviewFocus;
73
+ model: string;
74
+ findings: FocusFinding[];
75
+ };
76
+
77
+ export type ReviewReportFinding = {
78
+ priority: Priority;
79
+ location: string;
80
+ finding: string;
81
+ suggestion: string;
82
+ focus: string;
83
+ model: string;
84
+ };
85
+
86
+ export type ReviewDedupGroup = {
87
+ ids: number[];
88
+ };
89
+
90
+ export type ReviewStaleness = {
91
+ status: "stale";
92
+ warning: string;
93
+ nextStep: string;
94
+ };
95
+
96
+ export type ReviewScopeMode = "working-tree" | "branch-diff" | "commit" | "folder" | "custom";
97
+
98
+ export type ReviewMessageKind = "report" | "failure" | "triage" | "help";
99
+
100
+ export type ReviewMessageDetails = {
101
+ kind?: "report";
102
+ request: {
103
+ mode: ReviewRequestMode;
104
+ signature: string;
105
+ };
106
+ scope: {
107
+ mode: ReviewScopeMode;
108
+ description: string;
109
+ };
110
+ fingerprint: ReviewFingerprint;
111
+ staleness?: ReviewStaleness;
112
+ focusStatus: Array<{
113
+ focus: ReviewFocus;
114
+ model: string;
115
+ ok: boolean;
116
+ error?: string;
117
+ }>;
118
+ findings: ReviewReportFinding[];
119
+ };
120
+
121
+ export type ReviewRunResult =
122
+ | { ok: false; error: string }
123
+ | { ok: true; details: ReviewMessageDetails };
124
+
125
+ export type ReviewCommentSide = "old" | "new" | "snapshot";
126
+ export type ReviewCommentAuthor = "pi" | "user" | string;
127
+ export type ReviewCommentStatus = "open" | "resolved";
128
+
129
+ export type ReviewCommentRange = {
130
+ startLine: number;
131
+ endLine?: number;
132
+ startColumn?: number;
133
+ endColumn?: number;
134
+ };
135
+
136
+ export type ReviewCommentAnchor = {
137
+ fileHash?: string;
138
+ selectedText?: string;
139
+ prefix?: string[];
140
+ suffix?: string[];
141
+ diffHunk?: string;
142
+ };
143
+
144
+ export type ReviewComment = {
145
+ schema: typeof REVIEW_COMMENT_SCHEMA;
146
+ id: string;
147
+ reviewId: string;
148
+ author: ReviewCommentAuthor;
149
+ origin: string;
150
+ createdAt: string;
151
+ path: string;
152
+ side: ReviewCommentSide;
153
+ range?: ReviewCommentRange;
154
+ priority?: Priority;
155
+ severity?: "error" | "warning" | "information" | "hint";
156
+ status: ReviewCommentStatus;
157
+ body: string;
158
+ suggestion?: string;
159
+ anchor?: ReviewCommentAnchor;
160
+ };
@@ -0,0 +1,62 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Type } from "typebox";
5
+
6
+ const PRIORITIES = ["P0", "P1", "P2", "P3"] as const;
7
+
8
+ export const SUBMIT_REVIEW_EXTENSION_PATH = fileURLToPath(import.meta.url);
9
+
10
+ const SUBMIT_REVIEW_PARAMS = Type.Object(
11
+ {
12
+ findings: Type.Array(
13
+ Type.Object(
14
+ {
15
+ priority: StringEnum(PRIORITIES, {
16
+ description:
17
+ "Priority level: P0 critical/blocking, P1 urgent, P2 normal, P3 low/nice-to-have.",
18
+ }),
19
+ location: Type.String({
20
+ minLength: 1,
21
+ description:
22
+ "File path and line for the finding, e.g. src/file.ts:42. Use path only if no single line applies.",
23
+ }),
24
+ finding: Type.String({
25
+ minLength: 1,
26
+ description:
27
+ "Concise explanation of what is wrong, why it matters, and the concrete scenario where it fails.",
28
+ }),
29
+ suggestion: Type.String({
30
+ minLength: 1,
31
+ description: "Specific, actionable fix for this finding.",
32
+ }),
33
+ },
34
+ { additionalProperties: false },
35
+ ),
36
+ { description: "Final review findings" },
37
+ ),
38
+ },
39
+ { additionalProperties: false },
40
+ );
41
+
42
+ const submitReviewTool = defineTool({
43
+ name: "submit_review",
44
+ label: "Submit Review",
45
+ description: "Submit the final review findings. Use exactly once as your final action.",
46
+ promptSnippet: "Submit the final review findings",
47
+ promptGuidelines: ["Use submit_review exactly once as your final action."],
48
+ parameters: SUBMIT_REVIEW_PARAMS,
49
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
50
+
51
+ async execute(_toolCallId, params) {
52
+ return {
53
+ content: [{ type: "text", text: "Review submitted." }],
54
+ details: params,
55
+ terminate: true,
56
+ };
57
+ },
58
+ });
59
+
60
+ export default function reviewSubmitExtension(pi: ExtensionAPI): void {
61
+ pi.registerTool(submitReviewTool);
62
+ }
@@ -0,0 +1,60 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Type } from "typebox";
5
+
6
+ const TRIAGE_DECISIONS = ["address", "push_back", "research", "ignore"] as const;
7
+
8
+ export const SUBMIT_TRIAGE_EXTENSION_PATH = fileURLToPath(import.meta.url);
9
+
10
+ const SUBMIT_TRIAGE_PARAMS = Type.Object(
11
+ {
12
+ items: Type.Array(
13
+ Type.Object(
14
+ {
15
+ id: Type.String({ minLength: 1, description: "Input feedback id being triaged." }),
16
+ decision: StringEnum(TRIAGE_DECISIONS, {
17
+ description: "Triage decision: address, push_back, research, or ignore.",
18
+ }),
19
+ summary: Type.String({
20
+ minLength: 1,
21
+ description: "Brief description of the feedback item.",
22
+ }),
23
+ rationale: Type.String({
24
+ minLength: 1,
25
+ description: "Why this triage decision is correct.",
26
+ }),
27
+ action: Type.String({
28
+ minLength: 1,
29
+ description: "What to do next for this feedback item.",
30
+ }),
31
+ },
32
+ { additionalProperties: false },
33
+ ),
34
+ { description: "Final triage items" },
35
+ ),
36
+ },
37
+ { additionalProperties: false },
38
+ );
39
+
40
+ const submitTriageTool = defineTool({
41
+ name: "submit_triage",
42
+ label: "Submit Triage",
43
+ description: "Submit the final PR feedback triage. Use exactly once as your final action.",
44
+ promptSnippet: "Submit the final PR feedback triage",
45
+ promptGuidelines: ["Use submit_triage exactly once as your final action."],
46
+ parameters: SUBMIT_TRIAGE_PARAMS,
47
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
48
+
49
+ async execute(_toolCallId, params) {
50
+ return {
51
+ content: [{ type: "text", text: "Triage submitted." }],
52
+ details: params,
53
+ terminate: true,
54
+ };
55
+ },
56
+ });
57
+
58
+ export default function triageSubmitExtension(pi: ExtensionAPI): void {
59
+ pi.registerTool(submitTriageTool);
60
+ }