tau-all-agent 0.2.0 → 0.3.1
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/extensions/answer.ts +115 -85
- package/extensions/branch-term/index.ts +9 -5
- package/extensions/btw.ts +70 -36
- package/extensions/insights.ts +70 -56
- package/extensions/loop.ts +14 -52
- package/extensions/memory.ts +65 -33
- package/extensions/notify.ts +5 -3
- package/extensions/review/fix.ts +10 -16
- package/extensions/review/git.ts +7 -1
- package/extensions/review/index.ts +2 -2
- package/extensions/review/models.ts +12 -2
- package/extensions/review/review.ts +3 -5
- package/extensions/review/runtime.ts +9 -8
- package/extensions/review/triage.ts +2 -4
- package/extensions/sandbox/README.md +7 -0
- package/extensions/sandbox/bash.ts +83 -85
- package/extensions/sandbox/command.ts +15 -1
- package/extensions/sandbox/config.ts +26 -15
- package/extensions/sandbox/index.ts +21 -11
- package/extensions/sandbox/permissions/network.ts +8 -3
- package/extensions/sandbox/permissions/unsandboxed.ts +1 -240
- package/extensions/sandbox/runtime.ts +54 -20
- package/extensions/subagent/README.md +58 -1
- package/extensions/subagent/config.ts +118 -0
- package/extensions/subagent/index.ts +54 -22
- package/extensions/telegram/README.md +2 -0
- package/extensions/telegram/assistant-result.d.mts +4 -1
- package/extensions/telegram/assistant-result.mjs +31 -16
- package/extensions/telegram/daemon.mjs +146 -194
- package/extensions/telegram/index.ts +11 -7
- package/extensions/tool-display-mode.ts +29 -50
- package/extensions/usage/README.md +5 -0
- package/extensions/usage/index.ts +341 -349
- package/extensions/websearch/README.md +8 -1
- package/extensions/websearch/config.ts +2 -1
- package/extensions/websearch/index.ts +18 -9
- package/extensions/websearch/providers/{openai-codex.pi.ts → openai.pi.ts} +51 -16
- package/extensions/websearch/providers/shared.ts +10 -3
- package/extensions/websearch/types.ts +2 -1
- package/extensions/worktree.ts +33 -89
- package/package.json +1 -1
- package/skills/oracle/SKILL.md +14 -8
- package/skills/oracle/scripts/oracle +43 -19
- package/skills/update-changelog/SKILL.md +8 -4
|
@@ -8,7 +8,14 @@ Optionally create `websearch.json` in your Pi agent directory (normally `~/.pi/a
|
|
|
8
8
|
|
|
9
9
|
```json
|
|
10
10
|
{
|
|
11
|
-
"routes": [
|
|
11
|
+
"routes": [
|
|
12
|
+
"pi:openai-codex",
|
|
13
|
+
"pi:openai",
|
|
14
|
+
"pi:anthropic",
|
|
15
|
+
"pi:gemini",
|
|
16
|
+
"firefox:gemini",
|
|
17
|
+
"chromium:gemini"
|
|
18
|
+
],
|
|
12
19
|
"profiles": {
|
|
13
20
|
"firefox": "default-release",
|
|
14
21
|
"chromium": "Default"
|
|
@@ -8,6 +8,7 @@ const WEBSEARCH_CONFIG_PATH = path.join(getAgentDir(), "websearch.json");
|
|
|
8
8
|
|
|
9
9
|
const DEFAULT_ROUTES: WebsearchRouteId[] = [
|
|
10
10
|
"pi:openai-codex",
|
|
11
|
+
"pi:openai",
|
|
11
12
|
"pi:anthropic",
|
|
12
13
|
"pi:gemini",
|
|
13
14
|
"firefox:gemini",
|
|
@@ -26,7 +27,7 @@ function sanitizeRoutes(value: unknown): WebsearchRouteId[] {
|
|
|
26
27
|
for (const item of value) {
|
|
27
28
|
if (item === "firefox:openai-codex" || item === "chromium:openai-codex") {
|
|
28
29
|
throw new Error(
|
|
29
|
-
`Invalid websearch route: ${item} (removed). Use pi:openai-codex and sign in with Pi /login.`,
|
|
30
|
+
`Invalid websearch route: ${item} (removed). Use pi:openai-codex or pi:openai and sign in with Pi /login.`,
|
|
30
31
|
);
|
|
31
32
|
}
|
|
32
33
|
if (typeof item !== "string" || !valid.has(item as WebsearchRouteId)) {
|
|
@@ -19,7 +19,11 @@ import { limitOutput } from "./output.js";
|
|
|
19
19
|
import { isPiAnthropicModel, searchWithPiAnthropic } from "./providers/anthropic.pi.js";
|
|
20
20
|
import { browserGemini } from "./providers/gemini.browser.js";
|
|
21
21
|
import { isPiGeminiModel, searchWithPiGemini } from "./providers/gemini.pi.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
isPiOpenAICodexModel,
|
|
24
|
+
isPiOpenAIModel,
|
|
25
|
+
searchWithPiOpenAI,
|
|
26
|
+
} from "./providers/openai.pi.js";
|
|
23
27
|
import type { PiModelSelection } from "./providers/pi-model.shared.js";
|
|
24
28
|
import { getPiModelCandidates, selectNextPiModel } from "./providers/pi-model.shared.js";
|
|
25
29
|
import { isModelUnavailableError } from "./providers/shared.js";
|
|
@@ -37,8 +41,13 @@ type PiRouteId = `pi:${WebsearchBackendId}`;
|
|
|
37
41
|
const PI_ROUTE_HANDLERS: Record<PiRouteId, PiRouteHandler> = {
|
|
38
42
|
"pi:openai-codex": {
|
|
39
43
|
predicate: isPiOpenAICodexModel,
|
|
40
|
-
search:
|
|
41
|
-
fallbackModels: ["gpt-
|
|
44
|
+
search: searchWithPiOpenAI,
|
|
45
|
+
fallbackModels: ["gpt-6-luna", "gpt-6.1-sol"],
|
|
46
|
+
},
|
|
47
|
+
"pi:openai": {
|
|
48
|
+
predicate: isPiOpenAIModel,
|
|
49
|
+
search: searchWithPiOpenAI,
|
|
50
|
+
fallbackModels: ["gpt-6-luna", "gpt-6.1-sol"],
|
|
42
51
|
},
|
|
43
52
|
"pi:anthropic": {
|
|
44
53
|
predicate: isPiAnthropicModel,
|
|
@@ -246,24 +255,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
246
255
|
query: Type.String({ description: "What to search for" }),
|
|
247
256
|
}),
|
|
248
257
|
renderShell: "self",
|
|
249
|
-
renderCall(args, theme) {
|
|
250
|
-
return new Text(formatWebsearchCall(args.query, theme),
|
|
258
|
+
renderCall(args, theme, { outputPad }) {
|
|
259
|
+
return new Text(formatWebsearchCall(args.query, theme), outputPad, 0);
|
|
251
260
|
},
|
|
252
|
-
renderResult(result, { expanded }, theme) {
|
|
261
|
+
renderResult(result, { expanded }, theme, { outputPad }) {
|
|
253
262
|
const content = result.content.find((item) => item.type === "text");
|
|
254
263
|
const text = content?.type === "text" ? content.text : "";
|
|
255
264
|
|
|
256
265
|
if (!expanded) {
|
|
257
|
-
return new Text(formatCollapsedWebsearchResult(text, theme),
|
|
266
|
+
return new Text(formatCollapsedWebsearchResult(text, theme), outputPad, 0);
|
|
258
267
|
}
|
|
259
268
|
|
|
260
269
|
if (!text.trim()) {
|
|
261
|
-
return new Text(`\n${theme.fg("muted", "(no output)")}`,
|
|
270
|
+
return new Text(`\n${theme.fg("muted", "(no output)")}`, outputPad, 0);
|
|
262
271
|
}
|
|
263
272
|
|
|
264
273
|
const container = new Container();
|
|
265
274
|
container.addChild(new Spacer(1));
|
|
266
|
-
container.addChild(new Markdown(text.trim(),
|
|
275
|
+
container.addChild(new Markdown(text.trim(), outputPad, 0, getMarkdownTheme()));
|
|
267
276
|
return container;
|
|
268
277
|
},
|
|
269
278
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -6,19 +6,20 @@ import type { PiModelSelection } from "./pi-model.shared.js";
|
|
|
6
6
|
import { buildWebsearchPrompt, WEBSEARCH_SYSTEM_PROMPT } from "./search-prompt.shared.js";
|
|
7
7
|
import { applyResolvedHeaders, getResolvedHeader, readEventStream, withTimeout } from "./shared.js";
|
|
8
8
|
|
|
9
|
-
export async function
|
|
9
|
+
export async function searchWithPiOpenAI(
|
|
10
10
|
selection: PiModelSelection,
|
|
11
11
|
query: string,
|
|
12
12
|
signal?: AbortSignal,
|
|
13
13
|
): Promise<WebsearchResult> {
|
|
14
14
|
const apiKey = resolveApiKey(selection);
|
|
15
15
|
if (!apiKey) {
|
|
16
|
-
throw new Error("OpenAI
|
|
16
|
+
throw new Error("OpenAI auth is not configured.");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const backend = selection.model.provider === "openai-codex" ? "openai-codex" : "openai";
|
|
20
|
+
const result = await runOpenAISearch({
|
|
20
21
|
apiKey,
|
|
21
|
-
|
|
22
|
+
backend,
|
|
22
23
|
model: selection.model.id,
|
|
23
24
|
query,
|
|
24
25
|
baseUrl: selection.model.baseUrl,
|
|
@@ -27,13 +28,17 @@ export async function searchWithPiOpenAICodex(
|
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
return {
|
|
30
|
-
backend
|
|
31
|
+
backend,
|
|
31
32
|
authSource: "pi",
|
|
32
33
|
answer: result.answer,
|
|
33
34
|
sources: result.sources,
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
export function isPiOpenAIModel(model: Model<Api>): boolean {
|
|
39
|
+
return model.api === "openai-responses" && model.provider === "openai";
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
export function isPiOpenAICodexModel(model: Model<Api>): boolean {
|
|
38
43
|
return model.api === "openai-codex-responses" && model.provider === "openai-codex";
|
|
39
44
|
}
|
|
@@ -48,25 +53,27 @@ function resolveApiKey(selection: PiModelSelection): string | undefined {
|
|
|
48
53
|
return match?.[1];
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
async function
|
|
56
|
+
async function runOpenAISearch(options: {
|
|
52
57
|
apiKey: string;
|
|
58
|
+
backend: "openai" | "openai-codex";
|
|
53
59
|
model: string;
|
|
54
60
|
query: string;
|
|
55
61
|
baseUrl?: string;
|
|
56
|
-
accountId?: string;
|
|
57
62
|
headers?: Record<string, string | null>;
|
|
58
63
|
signal?: AbortSignal;
|
|
59
64
|
}): Promise<{ answer: string; sources: WebsearchSource[] }> {
|
|
60
|
-
const
|
|
65
|
+
const codex = options.backend === "openai-codex";
|
|
66
|
+
const accountId = codex ? decodeJwtAccountId(options.apiKey) : undefined;
|
|
67
|
+
const url = codex ? resolveCodexUrl(options.baseUrl) : resolveResponsesUrl(options.baseUrl);
|
|
68
|
+
const response = await fetch(url, {
|
|
61
69
|
method: "POST",
|
|
62
70
|
headers: applyResolvedHeaders(
|
|
63
71
|
{
|
|
64
72
|
authorization: `Bearer ${options.apiKey}`,
|
|
65
|
-
...(
|
|
73
|
+
...(accountId ? { "chatgpt-account-id": accountId } : {}),
|
|
66
74
|
"content-type": "application/json",
|
|
67
75
|
accept: "text/event-stream",
|
|
68
|
-
"OpenAI-Beta": "responses=experimental",
|
|
69
|
-
originator: "pi-websearch",
|
|
76
|
+
...(codex ? { "OpenAI-Beta": "responses=experimental", originator: "pi-websearch" } : {}),
|
|
70
77
|
},
|
|
71
78
|
options.headers,
|
|
72
79
|
),
|
|
@@ -82,6 +89,7 @@ async function runOpenAICodexSearch(options: {
|
|
|
82
89
|
signal: withTimeout(options.signal, 120_000),
|
|
83
90
|
});
|
|
84
91
|
|
|
92
|
+
const sources: WebsearchSource[] = [];
|
|
85
93
|
let answer = "";
|
|
86
94
|
let fallbackAnswer = "";
|
|
87
95
|
let completed = false;
|
|
@@ -110,6 +118,7 @@ async function runOpenAICodexSearch(options: {
|
|
|
110
118
|
.filter((text): text is string => typeof text === "string")
|
|
111
119
|
.join("\n");
|
|
112
120
|
if (fullText) fallbackAnswer = fullText;
|
|
121
|
+
sources.push(...extractAnnotationSources(content));
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
if (
|
|
@@ -119,7 +128,7 @@ async function runOpenAICodexSearch(options: {
|
|
|
119
128
|
) {
|
|
120
129
|
const result = event.response as Record<string, unknown> | undefined;
|
|
121
130
|
if (event.type === "response.incomplete" || result?.status !== "completed") {
|
|
122
|
-
throw new Error("OpenAI
|
|
131
|
+
throw new Error("OpenAI search did not complete successfully.");
|
|
123
132
|
}
|
|
124
133
|
completed = true;
|
|
125
134
|
}
|
|
@@ -137,7 +146,7 @@ async function runOpenAICodexSearch(options: {
|
|
|
137
146
|
throw new Error((error as Record<string, unknown>).message as string);
|
|
138
147
|
}
|
|
139
148
|
}
|
|
140
|
-
throw new Error(eventMessage ?? "OpenAI
|
|
149
|
+
throw new Error(eventMessage ?? "OpenAI search failed.");
|
|
141
150
|
}
|
|
142
151
|
} catch (error) {
|
|
143
152
|
if (error instanceof SyntaxError) return;
|
|
@@ -146,20 +155,46 @@ async function runOpenAICodexSearch(options: {
|
|
|
146
155
|
});
|
|
147
156
|
|
|
148
157
|
if (!completed) {
|
|
149
|
-
throw new Error("OpenAI
|
|
158
|
+
throw new Error("OpenAI stream ended before search completed.");
|
|
150
159
|
}
|
|
151
160
|
|
|
152
161
|
const finalAnswer = (answer || fallbackAnswer).trim();
|
|
153
162
|
if (!finalAnswer) {
|
|
154
|
-
throw new Error("OpenAI
|
|
163
|
+
throw new Error("OpenAI returned an empty response.");
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
return {
|
|
158
167
|
answer: finalAnswer,
|
|
159
|
-
sources: dedupeSources(extractMarkdownSources(finalAnswer)),
|
|
168
|
+
sources: dedupeSources([...sources, ...extractMarkdownSources(finalAnswer)]),
|
|
160
169
|
};
|
|
161
170
|
}
|
|
162
171
|
|
|
172
|
+
function extractAnnotationSources(content: unknown[]): WebsearchSource[] {
|
|
173
|
+
const sources: WebsearchSource[] = [];
|
|
174
|
+
for (const part of content) {
|
|
175
|
+
if (!part || typeof part !== "object" || !("annotations" in part)) continue;
|
|
176
|
+
if (!Array.isArray(part.annotations)) continue;
|
|
177
|
+
for (const annotation of part.annotations) {
|
|
178
|
+
if (
|
|
179
|
+
annotation?.type !== "url_citation" ||
|
|
180
|
+
typeof annotation.url !== "string" ||
|
|
181
|
+
!/^https?:\/\//i.test(annotation.url)
|
|
182
|
+
)
|
|
183
|
+
continue;
|
|
184
|
+
sources.push({
|
|
185
|
+
url: annotation.url,
|
|
186
|
+
title: typeof annotation.title === "string" ? annotation.title : "",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return sources;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resolveResponsesUrl(baseUrl?: string): string {
|
|
194
|
+
const normalized = (baseUrl || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
195
|
+
return normalized.endsWith("/responses") ? normalized : `${normalized}/responses`;
|
|
196
|
+
}
|
|
197
|
+
|
|
163
198
|
function resolveCodexUrl(baseUrl = "https://chatgpt.com/backend-api"): string {
|
|
164
199
|
const normalized = String(baseUrl || "https://chatgpt.com/backend-api").replace(/\/+$/, "");
|
|
165
200
|
if (normalized.endsWith("/codex/responses")) return normalized;
|
|
@@ -7,15 +7,22 @@ export function isModelUnavailableError(error: unknown, model: Model<Api>): bool
|
|
|
7
7
|
if (!isRecord(error.body)) return false;
|
|
8
8
|
|
|
9
9
|
const detail = isRecord(error.body.error) ? error.body.error : undefined;
|
|
10
|
-
|
|
10
|
+
const codex = model.provider === "openai-codex" && model.api === "openai-codex-responses";
|
|
11
|
+
const openai = model.provider === "openai" && model.api === "openai-responses";
|
|
12
|
+
if (codex || openai) {
|
|
11
13
|
return (
|
|
12
|
-
(
|
|
14
|
+
(codex &&
|
|
15
|
+
error.status === 400 &&
|
|
13
16
|
Boolean(model.id) &&
|
|
14
17
|
error.body.detail ===
|
|
15
18
|
`The '${model.id}' model is not supported when using Codex with a ChatGPT account.`) ||
|
|
16
19
|
detail?.code === "model_not_found" ||
|
|
17
20
|
detail?.code === "model_not_supported" ||
|
|
18
|
-
detail?.code === "unsupported_model"
|
|
21
|
+
detail?.code === "unsupported_model" ||
|
|
22
|
+
(openai &&
|
|
23
|
+
error.status === 400 &&
|
|
24
|
+
detail?.code === "subscription_sharing_unsupported_capability" &&
|
|
25
|
+
detail.param === "model")
|
|
19
26
|
);
|
|
20
27
|
}
|
|
21
28
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export type WebsearchBackendId = "openai-codex" | "anthropic" | "gemini";
|
|
1
|
+
export type WebsearchBackendId = "openai-codex" | "openai" | "anthropic" | "gemini";
|
|
2
2
|
export type WebsearchAuthSource = "pi" | "firefox" | "chromium";
|
|
3
3
|
export type WebsearchBrowserFamily = "firefox" | "chromium";
|
|
4
4
|
export type WebsearchRouteId =
|
|
5
5
|
| "pi:openai-codex"
|
|
6
|
+
| "pi:openai"
|
|
6
7
|
| "pi:anthropic"
|
|
7
8
|
| "pi:gemini"
|
|
8
9
|
| "firefox:gemini"
|
package/extensions/worktree.ts
CHANGED
|
@@ -19,25 +19,14 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
SessionManager,
|
|
25
|
-
highlightCode,
|
|
26
|
-
type SessionHeader,
|
|
27
|
-
} from "@earendil-works/pi-coding-agent";
|
|
28
|
-
import {
|
|
29
|
-
Box,
|
|
30
|
-
Container,
|
|
31
|
-
type SelectItem,
|
|
32
|
-
SelectList,
|
|
33
|
-
Text,
|
|
34
|
-
matchesKey,
|
|
35
|
-
} from "@earendil-works/pi-tui";
|
|
22
|
+
import { SessionManager, highlightCode, type SessionHeader } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { Box, Text } from "@earendil-works/pi-tui";
|
|
36
24
|
import { spawnSync } from "node:child_process";
|
|
37
25
|
import * as fs from "node:fs";
|
|
38
26
|
import * as path from "node:path";
|
|
39
27
|
|
|
40
28
|
const STATUS_KEY = "0-worktree";
|
|
29
|
+
const LIST_ENTRY_TYPE = "worktree-list";
|
|
41
30
|
const MANUAL_OPEN_ENTRY_TYPE = "worktree-open-command";
|
|
42
31
|
const RESTORE_STASH_ENTRY_TYPE = "worktree-restore-command";
|
|
43
32
|
const SCRIPT_RERUN_ENTRY_TYPE = "worktree-script-rerun-command";
|
|
@@ -200,7 +189,7 @@ function showCommandMessage(
|
|
|
200
189
|
command: string,
|
|
201
190
|
): void {
|
|
202
191
|
const copiedToClipboard = copyToClipboard(command);
|
|
203
|
-
if (
|
|
192
|
+
if (ctx.mode === "print") {
|
|
204
193
|
console.log(formatCommandMessageIntro(intro, copiedToClipboard));
|
|
205
194
|
console.log(command);
|
|
206
195
|
return;
|
|
@@ -1262,8 +1251,8 @@ function hasValidSessionFile(sessionFile: string): boolean {
|
|
|
1262
1251
|
function createFreshSessionFile(targetCwd: string, sessionDir?: string): string {
|
|
1263
1252
|
const session = SessionManager.create(targetCwd, sessionDir);
|
|
1264
1253
|
const sessionFile = session.getSessionFile()!;
|
|
1265
|
-
// Pi
|
|
1266
|
-
//
|
|
1254
|
+
// Pi persists the session on its first user or assistant message. Persist the
|
|
1255
|
+
// native header so switchSession can open an otherwise empty conversation.
|
|
1267
1256
|
fs.writeFileSync(sessionFile, `${JSON.stringify(session.getHeader())}\n`, { flag: "wx" });
|
|
1268
1257
|
return sessionFile;
|
|
1269
1258
|
}
|
|
@@ -1308,9 +1297,13 @@ async function switchToWorktree(
|
|
|
1308
1297
|
// including when the source has not been persisted yet.
|
|
1309
1298
|
const storage = SessionManager.create(ctx.sessionManager.getCwd(), currentSessionDir);
|
|
1310
1299
|
const sessionDir = storage.usesDefaultSessionDir() ? undefined : currentSessionDir;
|
|
1311
|
-
const
|
|
1300
|
+
const hasPersistedMessage = ctx.sessionManager
|
|
1312
1301
|
.getEntries()
|
|
1313
|
-
.some(
|
|
1302
|
+
.some(
|
|
1303
|
+
(entry) =>
|
|
1304
|
+
entry.type === "message" &&
|
|
1305
|
+
(entry.message.role === "user" || entry.message.role === "assistant"),
|
|
1306
|
+
);
|
|
1314
1307
|
|
|
1315
1308
|
let sessionFile: string;
|
|
1316
1309
|
if (hasValidSessionFile(currentSessionFile)) {
|
|
@@ -1326,13 +1319,13 @@ async function switchToWorktree(
|
|
|
1326
1319
|
}
|
|
1327
1320
|
sessionFile = forkedSessionFile;
|
|
1328
1321
|
} else {
|
|
1329
|
-
if (
|
|
1322
|
+
if (hasPersistedMessage) {
|
|
1330
1323
|
throw new Error(`Current session file is missing or invalid: ${currentSessionFile}`);
|
|
1331
1324
|
}
|
|
1332
1325
|
|
|
1333
1326
|
const message = "Current session has no persisted history yet. Switching with a fresh session.";
|
|
1334
1327
|
if (ctx.hasUI) ctx.ui.notify(message, "warning");
|
|
1335
|
-
else console.
|
|
1328
|
+
else console.error(message);
|
|
1336
1329
|
|
|
1337
1330
|
sessionFile = createFreshSessionFile(targetPath, sessionDir);
|
|
1338
1331
|
}
|
|
@@ -1965,6 +1958,11 @@ async function handleList(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promi
|
|
|
1965
1958
|
return { repo, items };
|
|
1966
1959
|
});
|
|
1967
1960
|
|
|
1961
|
+
if (ctx.mode === "json") {
|
|
1962
|
+
pi.appendEntry(LIST_ENTRY_TYPE, { items });
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1968
1966
|
if (ctx.mode !== "tui") {
|
|
1969
1967
|
const output = items
|
|
1970
1968
|
.map((item) => {
|
|
@@ -1985,80 +1983,26 @@ async function handleList(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promi
|
|
|
1985
1983
|
return;
|
|
1986
1984
|
}
|
|
1987
1985
|
|
|
1988
|
-
const
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
value: item.wt.path,
|
|
1992
|
-
label: formatWorktreeLabel(item, theme),
|
|
1993
|
-
}));
|
|
1994
|
-
|
|
1995
|
-
const itemByValue = new Map(items.map((item) => [item.wt.path, item]));
|
|
1986
|
+
const options = items.map((item) => formatWorktreeLabel(item, ctx.ui.theme));
|
|
1987
|
+
const choice = await ctx.ui.select("Choose worktree", options);
|
|
1988
|
+
if (choice === undefined) return;
|
|
1996
1989
|
|
|
1997
|
-
|
|
1990
|
+
const item = items[options.indexOf(choice)];
|
|
1991
|
+
if (!item) return;
|
|
1998
1992
|
|
|
1999
|
-
const
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
selectedText: (t) => theme.fg("accent", t),
|
|
2006
|
-
description: (t) => t,
|
|
2007
|
-
scrollInfo: (t) => theme.fg("dim", t),
|
|
2008
|
-
noMatch: (t) => theme.fg("warning", t),
|
|
2009
|
-
});
|
|
2010
|
-
selectList.onSelect = (si) => {
|
|
2011
|
-
const item = itemByValue.get(si.value);
|
|
2012
|
-
if (item) done({ action: "switch", item });
|
|
2013
|
-
else done(null);
|
|
2014
|
-
};
|
|
2015
|
-
selectList.onCancel = () => done(null);
|
|
2016
|
-
container.addChild(selectList);
|
|
2017
|
-
|
|
2018
|
-
container.addChild(
|
|
2019
|
-
new Text(theme.fg("dim", " ↑↓ navigate enter switch a archive esc close"), 0, 0),
|
|
2020
|
-
);
|
|
2021
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("borderMuted", s)));
|
|
2022
|
-
|
|
2023
|
-
return {
|
|
2024
|
-
render: (w) => container.render(w),
|
|
2025
|
-
invalidate: () => container.invalidate(),
|
|
2026
|
-
handleInput: (data) => {
|
|
2027
|
-
if (matchesKey(data, "a")) {
|
|
2028
|
-
const si = selectList.getSelectedItem();
|
|
2029
|
-
if (si) {
|
|
2030
|
-
const item = itemByValue.get(si.value);
|
|
2031
|
-
if (item) {
|
|
2032
|
-
done({ action: "archive", item });
|
|
2033
|
-
return;
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
selectList.handleInput(data);
|
|
2038
|
-
tui.requestRender();
|
|
2039
|
-
},
|
|
2040
|
-
};
|
|
2041
|
-
});
|
|
2042
|
-
|
|
2043
|
-
if (!result) return;
|
|
2044
|
-
|
|
2045
|
-
if (result.action === "switch") {
|
|
2046
|
-
await switchToWorktree(pi, ctx, result.item.wt.path, repo.currentRoot);
|
|
1993
|
+
const action = await ctx.ui.select(`Worktree: ${collapsePath(item.wt.path)}`, [
|
|
1994
|
+
"Switch",
|
|
1995
|
+
"Archive",
|
|
1996
|
+
]);
|
|
1997
|
+
if (action === "Switch") {
|
|
1998
|
+
await switchToWorktree(pi, ctx, item.wt.path, repo.currentRoot);
|
|
2047
1999
|
return;
|
|
2048
2000
|
}
|
|
2049
2001
|
|
|
2050
|
-
if (
|
|
2051
|
-
await withSpinnerStatus(ctx, `archiving ${
|
|
2002
|
+
if (action === "Archive") {
|
|
2003
|
+
await withSpinnerStatus(ctx, `archiving ${item.branch}`, async () => {
|
|
2052
2004
|
const defaultMain = await getDefaultMainBranch(pi, repo.mainRoot);
|
|
2053
|
-
await archiveWorktree(
|
|
2054
|
-
pi,
|
|
2055
|
-
ctx,
|
|
2056
|
-
repo,
|
|
2057
|
-
result.item.branch,
|
|
2058
|
-
"prompt",
|
|
2059
|
-
defaultMain,
|
|
2060
|
-
result.item.wt,
|
|
2061
|
-
);
|
|
2005
|
+
await archiveWorktree(pi, ctx, repo, item.branch, "prompt", defaultMain, item.wt);
|
|
2062
2006
|
});
|
|
2063
2007
|
}
|
|
2064
2008
|
}
|
package/package.json
CHANGED
package/skills/oracle/SKILL.md
CHANGED
|
@@ -7,11 +7,13 @@ description: "Get independent reviews, second opinions, or help getting unstuck
|
|
|
7
7
|
|
|
8
8
|
Use this skill when you want a second opinion from a capable model in a different family than the current session.
|
|
9
9
|
|
|
10
|
-
The oracle workflow bundles selected files into a standalone prompt, selects an alternate model with `pi --list-models`, and sends the bundle to `pi -p --no-tools`.
|
|
10
|
+
The oracle workflow bundles selected files into a standalone prompt, selects an alternate model with `pi --list-models`, and sends the bundle to `pi -p --no-tools`.
|
|
11
|
+
|
|
12
|
+
Discovery and generation disable extensions, skills, prompt templates, themes, and context files (`AGENTS.md` / `CLAUDE.md`). A fixed system prompt and empty append prompt replace discovered `SYSTEM.md` / `APPEND_SYSTEM.md`. The bundle supplies the task and project context. Pi retains normal authentication and configuration, but has no tools or persisted session. This isolates instructions, not the process. It is not a filesystem sandbox or a read-only guarantee.
|
|
11
13
|
|
|
12
14
|
## Model selection
|
|
13
15
|
|
|
14
|
-
The
|
|
16
|
+
The `scripts/oracle` wrapper chooses the strongest known model it can find in the first usable family from this order:
|
|
15
17
|
|
|
16
18
|
| Current family | Oracle preference |
|
|
17
19
|
| ------------------ | ----------------------------------------------------- |
|
|
@@ -33,24 +35,28 @@ If the current model is not obvious to the script, pass it explicitly with `--cu
|
|
|
33
35
|
|
|
34
36
|
## Commands
|
|
35
37
|
|
|
36
|
-
Run
|
|
38
|
+
Run from the repository being reviewed, not this skill directory. Use the installed scripts' absolute paths. In the examples, replace `/absolute/path/to/oracle` with the directory containing this `SKILL.md`.
|
|
39
|
+
|
|
40
|
+
The wrapper resolves the invocation's Git root once (or uses the invocation directory outside Git) and uses it for model discovery, bundling, and generation. Relative file selectors are rooted there, even when invoked from a repository subdirectory.
|
|
37
41
|
|
|
38
42
|
```bash
|
|
39
43
|
# Preview selected files
|
|
40
|
-
|
|
44
|
+
/absolute/path/to/oracle/scripts/oracle-bundle --dry-run \
|
|
45
|
+
-p "<task>" --file "src/**" --file "!**/*.test.*"
|
|
41
46
|
|
|
42
47
|
# Preview the full bundle
|
|
43
|
-
|
|
48
|
+
/absolute/path/to/oracle/scripts/oracle-bundle \
|
|
49
|
+
-p "<task>" --file "src/**" --file "!**/*.test.*"
|
|
44
50
|
|
|
45
51
|
# Show which oracle model would be selected
|
|
46
|
-
|
|
52
|
+
/absolute/path/to/oracle/scripts/oracle --list-models --current openai-codex/gpt-6-astra
|
|
47
53
|
|
|
48
54
|
# Ask the automatically selected oracle model
|
|
49
|
-
|
|
55
|
+
/absolute/path/to/oracle/scripts/oracle --current openai-codex/gpt-6-astra \
|
|
50
56
|
-p "<task>" --file "src/**" --file "!**/*.test.*"
|
|
51
57
|
|
|
52
58
|
# Override the oracle model when the automatic choice is wrong
|
|
53
|
-
|
|
59
|
+
/absolute/path/to/oracle/scripts/oracle --model anthropic/claude-fable-5-1 \
|
|
54
60
|
-p "<task>" --file "src/**" --file "!**/*.test.*"
|
|
55
61
|
```
|
|
56
62
|
|