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.
Files changed (44) hide show
  1. package/extensions/answer.ts +115 -85
  2. package/extensions/branch-term/index.ts +9 -5
  3. package/extensions/btw.ts +70 -36
  4. package/extensions/insights.ts +70 -56
  5. package/extensions/loop.ts +14 -52
  6. package/extensions/memory.ts +65 -33
  7. package/extensions/notify.ts +5 -3
  8. package/extensions/review/fix.ts +10 -16
  9. package/extensions/review/git.ts +7 -1
  10. package/extensions/review/index.ts +2 -2
  11. package/extensions/review/models.ts +12 -2
  12. package/extensions/review/review.ts +3 -5
  13. package/extensions/review/runtime.ts +9 -8
  14. package/extensions/review/triage.ts +2 -4
  15. package/extensions/sandbox/README.md +7 -0
  16. package/extensions/sandbox/bash.ts +83 -85
  17. package/extensions/sandbox/command.ts +15 -1
  18. package/extensions/sandbox/config.ts +26 -15
  19. package/extensions/sandbox/index.ts +21 -11
  20. package/extensions/sandbox/permissions/network.ts +8 -3
  21. package/extensions/sandbox/permissions/unsandboxed.ts +1 -240
  22. package/extensions/sandbox/runtime.ts +54 -20
  23. package/extensions/subagent/README.md +58 -1
  24. package/extensions/subagent/config.ts +118 -0
  25. package/extensions/subagent/index.ts +54 -22
  26. package/extensions/telegram/README.md +2 -0
  27. package/extensions/telegram/assistant-result.d.mts +4 -1
  28. package/extensions/telegram/assistant-result.mjs +31 -16
  29. package/extensions/telegram/daemon.mjs +146 -194
  30. package/extensions/telegram/index.ts +11 -7
  31. package/extensions/tool-display-mode.ts +29 -50
  32. package/extensions/usage/README.md +5 -0
  33. package/extensions/usage/index.ts +341 -349
  34. package/extensions/websearch/README.md +8 -1
  35. package/extensions/websearch/config.ts +2 -1
  36. package/extensions/websearch/index.ts +18 -9
  37. package/extensions/websearch/providers/{openai-codex.pi.ts → openai.pi.ts} +51 -16
  38. package/extensions/websearch/providers/shared.ts +10 -3
  39. package/extensions/websearch/types.ts +2 -1
  40. package/extensions/worktree.ts +33 -89
  41. package/package.json +1 -1
  42. package/skills/oracle/SKILL.md +14 -8
  43. package/skills/oracle/scripts/oracle +43 -19
  44. 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": ["pi:openai-codex", "pi:anthropic", "pi:gemini", "firefox:gemini", "chromium:gemini"],
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 { isPiOpenAICodexModel, searchWithPiOpenAICodex } from "./providers/openai-codex.pi.js";
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: searchWithPiOpenAICodex,
41
- fallbackModels: ["gpt-5.6-luna", "gpt-5.5"],
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), 0, 0);
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), 0, 0);
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)")}`, 0, 0);
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(), 0, 0, getMarkdownTheme()));
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 searchWithPiOpenAICodex(
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 Codex auth is not configured.");
16
+ throw new Error("OpenAI auth is not configured.");
17
17
  }
18
18
 
19
- const result = await runOpenAICodexSearch({
19
+ const backend = selection.model.provider === "openai-codex" ? "openai-codex" : "openai";
20
+ const result = await runOpenAISearch({
20
21
  apiKey,
21
- accountId: decodeJwtAccountId(apiKey),
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: "openai-codex",
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 runOpenAICodexSearch(options: {
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 response = await fetch(resolveCodexUrl(options.baseUrl), {
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
- ...(options.accountId ? { "chatgpt-account-id": options.accountId } : {}),
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 Codex search did not complete successfully.");
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 Codex search failed.");
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 Codex stream ended before search completed.");
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 Codex returned an empty response.");
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
- if (model.provider === "openai-codex" && model.api === "openai-codex-responses") {
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
- (error.status === 400 &&
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"
@@ -19,25 +19,14 @@
19
19
  */
20
20
 
21
21
  import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
22
- import {
23
- DynamicBorder,
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 (!ctx.hasUI) {
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 defers its first write until an assistant reply. Persist the native header
1266
- // now so switchSession can open an otherwise empty conversation.
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 hasAssistantReply = ctx.sessionManager
1300
+ const hasPersistedMessage = ctx.sessionManager
1312
1301
  .getEntries()
1313
- .some((entry) => entry.type === "message" && entry.message.role === "assistant");
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 (hasAssistantReply) {
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.log(message);
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 theme = ctx.ui.theme;
1989
-
1990
- const selectItems: SelectItem[] = items.map((item) => ({
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
- type ListResult = { action: "switch" | "archive"; item: WorktreeDisplayItem } | null;
1990
+ const item = items[options.indexOf(choice)];
1991
+ if (!item) return;
1998
1992
 
1999
- const result = await ctx.ui.custom<ListResult>((tui, theme, _kb, done) => {
2000
- const container = new Container();
2001
- container.addChild(new DynamicBorder((s: string) => theme.fg("borderMuted", s)));
2002
-
2003
- const selectList = new SelectList(selectItems, Math.min(selectItems.length, 15), {
2004
- selectedPrefix: (t) => theme.fg("accent", t),
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 (result.action === "archive") {
2051
- await withSpinnerStatus(ctx, `archiving ${result.item.branch}`, async () => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tau-all-agent",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Full Tau package for Pi with coding workflows, web search, sandboxing, memory, Telegram, Home Assistant, OpenSCAD, skills, and a theme",
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -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`. It is read-only by default because the oracle receives the selected context directly and does not get 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 `./scripts/oracle` wrapper chooses the strongest known model it can find in the first usable family from this order:
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 commands from this skill directory:
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
- ./scripts/oracle-bundle --dry-run -p "<task>" --file "src/**" --file "!**/*.test.*"
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
- ./scripts/oracle-bundle -p "<task>" --file "src/**" --file "!**/*.test.*"
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
- ./scripts/oracle --list-models --current openai-codex/gpt-6-astra
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
- ./scripts/oracle --current openai-codex/gpt-6-astra \
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
- ./scripts/oracle --model anthropic/claude-fable-5 \
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