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
@@ -1,9 +1,10 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { readFile } from "node:fs/promises";
2
2
  import { DatabaseSync } from "node:sqlite";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
 
6
6
  import type { BrowserCookie, BrowserProfile } from "../types.js";
7
+ import { pathExists } from "./fs.js";
7
8
  import { withSqliteSnapshot } from "./sqlite.js";
8
9
 
9
10
  function firefoxBaseDir(): string | null {
@@ -45,7 +46,9 @@ function parseIni(text: string): Array<Record<string, string>> {
45
46
  return sections;
46
47
  }
47
48
 
48
- export function discoverFirefoxProfiles(preferredProfileName?: string): BrowserProfile[] {
49
+ export async function discoverFirefoxProfiles(
50
+ preferredProfileName?: string,
51
+ ): Promise<BrowserProfile[]> {
49
52
  const baseDir = firefoxBaseDir();
50
53
  if (!baseDir) return [];
51
54
 
@@ -53,7 +56,7 @@ export function discoverFirefoxProfiles(preferredProfileName?: string): BrowserP
53
56
 
54
57
  let sections: Array<Record<string, string>> = [];
55
58
  try {
56
- sections = parseIni(readFileSync(profilesIniPath, "utf8"));
59
+ sections = parseIni(await readFile(profilesIniPath, "utf8"));
57
60
  } catch {
58
61
  return [];
59
62
  }
@@ -67,7 +70,7 @@ export function discoverFirefoxProfiles(preferredProfileName?: string): BrowserP
67
70
  const profilePath =
68
71
  section.IsRelative === "1" ? path.join(baseDir, profilePathValue) : profilePathValue;
69
72
  const cookiesPath = path.join(profilePath, "cookies.sqlite");
70
- if (!existsSync(cookiesPath)) continue;
73
+ if (!(await pathExists(cookiesPath))) continue;
71
74
 
72
75
  profiles.push({
73
76
  family: "firefox",
@@ -100,12 +103,12 @@ function isPreferredFirefoxProfile(
100
103
  );
101
104
  }
102
105
 
103
- export function loadFirefoxCookies(profile: BrowserProfile): BrowserCookie[] {
106
+ export async function loadFirefoxCookies(profile: BrowserProfile): Promise<BrowserCookie[]> {
104
107
  const cookiesPath = path.join(profile.profilePath, "cookies.sqlite");
105
- if (!existsSync(cookiesPath)) return [];
108
+ if (!(await pathExists(cookiesPath))) return [];
106
109
 
107
110
  try {
108
- return withSqliteSnapshot(cookiesPath, "websearch-firefox", (tempDbPath) => {
111
+ return await withSqliteSnapshot(cookiesPath, "websearch-firefox", (tempDbPath) => {
109
112
  const db = new DatabaseSync(tempDbPath, { readOnly: true });
110
113
  try {
111
114
  const rows = db
@@ -0,0 +1,10 @@
1
+ import { access } from "node:fs/promises";
2
+
3
+ export async function pathExists(filePath: string): Promise<boolean> {
4
+ try {
5
+ await access(filePath);
6
+ return true;
7
+ } catch {
8
+ return false;
9
+ }
10
+ }
@@ -1,33 +1,40 @@
1
- import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
1
+ import { copyFile, mkdtemp, rm } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
 
5
- export function withSqliteSnapshot<T>(
5
+ import { pathExists } from "./fs.js";
6
+
7
+ export async function withSqliteSnapshot<T>(
6
8
  sourceDbPath: string,
7
9
  tempPrefix: string,
8
- operation: (snapshotPath: string) => T,
9
- ): T {
10
- const tempDir = path.join(os.tmpdir(), `${tempPrefix}-${process.pid}-${Date.now()}`);
10
+ operation: (snapshotPath: string) => T | Promise<T>,
11
+ ): Promise<T> {
12
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), `${tempPrefix}-${process.pid}-`));
11
13
  const snapshotPath = path.join(tempDir, path.basename(sourceDbPath));
12
14
 
13
15
  try {
14
- mkdirSync(tempDir, { recursive: true });
15
- copyFileSync(sourceDbPath, snapshotPath);
16
- copySidecar(sourceDbPath, snapshotPath, "-wal");
17
- copySidecar(sourceDbPath, snapshotPath, "-shm");
18
- copySidecar(sourceDbPath, snapshotPath, "-journal");
19
- return operation(snapshotPath);
16
+ await copyFile(sourceDbPath, snapshotPath);
17
+ await Promise.all([
18
+ copySidecar(sourceDbPath, snapshotPath, "-wal"),
19
+ copySidecar(sourceDbPath, snapshotPath, "-shm"),
20
+ copySidecar(sourceDbPath, snapshotPath, "-journal"),
21
+ ]);
22
+ return await operation(snapshotPath);
20
23
  } finally {
21
- rmSync(tempDir, { recursive: true, force: true });
24
+ await rm(tempDir, { recursive: true, force: true });
22
25
  }
23
26
  }
24
27
 
25
- export function copySidecar(sourceDbPath: string, targetDbPath: string, suffix: string): void {
28
+ export async function copySidecar(
29
+ sourceDbPath: string,
30
+ targetDbPath: string,
31
+ suffix: string,
32
+ ): Promise<void> {
26
33
  const source = `${sourceDbPath}${suffix}`;
27
- if (!existsSync(source)) return;
34
+ if (!(await pathExists(source))) return;
28
35
 
29
36
  try {
30
- copyFileSync(source, `${targetDbPath}${suffix}`);
37
+ await copyFile(source, `${targetDbPath}${suffix}`);
31
38
  } catch (error) {
32
39
  const code =
33
40
  error && typeof error === "object" && "code" in error
@@ -1,10 +1,10 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
2
  import path from "node:path";
3
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
4
 
5
5
  import type { WebsearchConfig, WebsearchRouteId } from "./types.js";
6
6
 
7
- const WEBSEARCH_CONFIG_PATH = path.join(homedir(), ".pi", "websearch.json");
7
+ const WEBSEARCH_CONFIG_PATH = path.join(getAgentDir(), "websearch.json");
8
8
 
9
9
  const DEFAULT_ROUTES: WebsearchRouteId[] = [
10
10
  "pi:openai-codex",
@@ -128,15 +128,15 @@ function throwIfAborted(signal?: AbortSignal): void {
128
128
  throw signal.reason ?? new Error("The operation was aborted.");
129
129
  }
130
130
 
131
- function getBrowserProfiles(
131
+ async function getBrowserProfiles(
132
132
  cache: Partial<Record<WebsearchBrowserFamily, BrowserProfile[]>>,
133
133
  family: WebsearchBrowserFamily,
134
134
  config: WebsearchConfig,
135
- ): BrowserProfile[] {
135
+ ): Promise<BrowserProfile[]> {
136
136
  const cached = cache[family];
137
137
  if (cached) return cached;
138
138
 
139
- const profiles = discoverProfiles([family], config.profiles);
139
+ const profiles = await discoverProfiles([family], config.profiles);
140
140
  cache[family] = profiles;
141
141
  return profiles;
142
142
  }
@@ -153,7 +153,7 @@ async function searchBrowserRoute(
153
153
  : "chromium";
154
154
  const browserProvider = route.endsWith(":gemini") ? browserGemini : browserOpenAICodex;
155
155
  const configuredProfileName = config.profiles[profileFamily];
156
- const profiles = getBrowserProfiles(profileCache, profileFamily, config);
156
+ const profiles = await getBrowserProfiles(profileCache, profileFamily, config);
157
157
  let lastError: string | null = null;
158
158
 
159
159
  if (configuredProfileName && profiles.length === 0) {
@@ -164,7 +164,7 @@ async function searchBrowserRoute(
164
164
  throwIfAborted(signal);
165
165
 
166
166
  try {
167
- const session = createBrowserSession(profile, browserProvider.domains);
167
+ const session = await createBrowserSession(profile, browserProvider.domains);
168
168
  if (!session) {
169
169
  if (configuredProfileName) {
170
170
  throw new Error(
@@ -265,7 +265,7 @@ export default function (pi: ExtensionAPI) {
265
265
  promptSnippet:
266
266
  "Search the web for current or external information unavailable in local files",
267
267
  promptGuidelines: [
268
- "Use this for recent facts, live service behavior, or external documentation that is not already present in the repo.",
268
+ "Use websearch for recent facts, live service behavior, or external documentation that is not already present in the repo.",
269
269
  "Do not use websearch when repository files or supplied context already answer the question.",
270
270
  ],
271
271
  parameters: Type.Object({
@@ -5,6 +5,7 @@ export interface PiModelSelection {
5
5
  model: Model<Api>;
6
6
  apiKey?: string;
7
7
  headers?: Record<string, string>;
8
+ env?: Record<string, string>;
8
9
  }
9
10
 
10
11
  export async function selectCurrentPiModel(
@@ -42,7 +43,24 @@ async function resolvePiModelSelection(
42
43
  ctx: Pick<ExtensionContext, "modelRegistry">,
43
44
  ): Promise<PiModelSelection | null> {
44
45
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
45
- return auth.ok ? { model, apiKey: auth.apiKey, headers: auth.headers } : null;
46
+ return auth.ok
47
+ ? {
48
+ model: resolveModelEnv(model, auth.env),
49
+ apiKey: auth.apiKey,
50
+ headers: auth.headers,
51
+ env: auth.env,
52
+ }
53
+ : null;
54
+ }
55
+
56
+ function resolveModelEnv(model: Model<Api>, env?: Record<string, string>): Model<Api> {
57
+ if (!model.baseUrl || !env) return model;
58
+
59
+ let baseUrl = model.baseUrl;
60
+ for (const [name, value] of Object.entries(env)) {
61
+ baseUrl = baseUrl.replaceAll(`{${name}}`, value);
62
+ }
63
+ return baseUrl === model.baseUrl ? model : { ...model, baseUrl };
46
64
  }
47
65
 
48
66
  function rankPiModels(models: Model<Api>[]): Model<Api>[] {
@@ -40,9 +40,9 @@ import * as fs from "node:fs";
40
40
  import * as path from "node:path";
41
41
 
42
42
  const STATUS_KEY = "0-worktree";
43
- const MANUAL_OPEN_MESSAGE_TYPE = "worktree-open-command";
44
- const RESTORE_STASH_MESSAGE_TYPE = "worktree-restore-command";
45
- const SCRIPT_RERUN_MESSAGE_TYPE = "worktree-script-rerun-command";
43
+ const MANUAL_OPEN_ENTRY_TYPE = "worktree-open-command";
44
+ const RESTORE_STASH_ENTRY_TYPE = "worktree-restore-command";
45
+ const SCRIPT_RERUN_ENTRY_TYPE = "worktree-script-rerun-command";
46
46
  const MANUAL_OPEN_INTRO =
47
47
  "Worktree ready. Open it in a separate terminal or tmux pane with this command";
48
48
 
@@ -224,15 +224,10 @@ function showCommandMessage(
224
224
  return;
225
225
  }
226
226
 
227
- pi.sendMessage({
228
- customType,
229
- content: intro,
230
- display: true,
231
- details: {
232
- intro,
233
- command,
234
- copiedToClipboard,
235
- } satisfies CommandMessageDetails,
227
+ pi.appendEntry<CommandMessageDetails>(customType, {
228
+ intro,
229
+ command,
230
+ copiedToClipboard,
236
231
  });
237
232
  }
238
233
 
@@ -241,7 +236,7 @@ function showManualOpenCommand(
241
236
  ctx: ExtensionCommandContext,
242
237
  command: string,
243
238
  ): void {
244
- showCommandMessage(pi, ctx, MANUAL_OPEN_MESSAGE_TYPE, MANUAL_OPEN_INTRO, command);
239
+ showCommandMessage(pi, ctx, MANUAL_OPEN_ENTRY_TYPE, MANUAL_OPEN_INTRO, command);
245
240
  }
246
241
 
247
242
  function showRestoreStashCommand(
@@ -253,7 +248,7 @@ function showRestoreStashCommand(
253
248
  showCommandMessage(
254
249
  pi,
255
250
  ctx,
256
- RESTORE_STASH_MESSAGE_TYPE,
251
+ RESTORE_STASH_ENTRY_TYPE,
257
252
  `Main-worktree changes were stashed as ${stashSpec}. Restore them in the new worktree with this command`,
258
253
  `cd ${shellQuoteCompact(targetPath)} && git stash apply ${shellQuote(stashSpec)}`,
259
254
  );
@@ -270,7 +265,7 @@ function showScriptRerunCommand(
270
265
  showCommandMessage(
271
266
  pi,
272
267
  ctx,
273
- SCRIPT_RERUN_MESSAGE_TYPE,
268
+ SCRIPT_RERUN_ENTRY_TYPE,
274
269
  `${label} failed (exit ${exitCode}). Re-run it manually with this command`,
275
270
  `cd ${shellQuoteCompact(worktreeRoot)} && ${command}`,
276
271
  );
@@ -2008,18 +2003,23 @@ async function handleList(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promi
2008
2003
  return { repo, items };
2009
2004
  });
2010
2005
 
2011
- if (!ctx.hasUI) {
2012
- for (const item of items) {
2013
- const dirty = item.status === "dirty" ? " *" : "";
2014
- const meta: string[] = [];
2015
- if (item.tracked) meta.push("↑");
2016
- if (item.isMain) meta.push("[primary]");
2017
- if (item.pathState !== "ok") meta.push(item.pathState);
2018
- if (item.wt.locked)
2019
- meta.push(item.wt.lockedReason ? `locked:${item.wt.lockedReason}` : "locked");
2020
- const suffix = meta.length > 0 ? ` ${meta.join(" ")}` : "";
2021
- console.log(` ${item.branch}${dirty}${suffix} ${collapsePath(item.wt.path)}`);
2022
- }
2006
+ if (ctx.mode !== "tui") {
2007
+ const output = items
2008
+ .map((item) => {
2009
+ const dirty = item.status === "dirty" ? " *" : "";
2010
+ const meta: string[] = [];
2011
+ if (item.tracked) meta.push("↑");
2012
+ if (item.isMain) meta.push("[primary]");
2013
+ if (item.pathState !== "ok") meta.push(item.pathState);
2014
+ if (item.wt.locked)
2015
+ meta.push(item.wt.lockedReason ? `locked:${item.wt.lockedReason}` : "locked");
2016
+ const suffix = meta.length > 0 ? ` ${meta.join(" ")}` : "";
2017
+ return ` ${item.branch}${dirty}${suffix} ${collapsePath(item.wt.path)}`;
2018
+ })
2019
+ .join("\n");
2020
+
2021
+ if (ctx.mode === "print") console.log(output);
2022
+ else ctx.ui.notify(output, "info");
2023
2023
  return;
2024
2024
  }
2025
2025
 
@@ -2114,14 +2114,14 @@ function parseSubcommand(args: string): { subcommand: Subcommand | null; rest: s
2114
2114
 
2115
2115
  export default function worktreeExtension(pi: ExtensionAPI) {
2116
2116
  for (const customType of [
2117
- MANUAL_OPEN_MESSAGE_TYPE,
2118
- RESTORE_STASH_MESSAGE_TYPE,
2119
- SCRIPT_RERUN_MESSAGE_TYPE,
2117
+ MANUAL_OPEN_ENTRY_TYPE,
2118
+ RESTORE_STASH_ENTRY_TYPE,
2119
+ SCRIPT_RERUN_ENTRY_TYPE,
2120
2120
  ]) {
2121
- pi.registerMessageRenderer(customType, (message, _options, theme) => {
2122
- const details = message.details as CommandMessageDetails | undefined;
2121
+ pi.registerEntryRenderer<CommandMessageDetails>(customType, (entry, _options, theme) => {
2122
+ const details = entry.data;
2123
2123
  if (!details || typeof details.intro !== "string" || typeof details.command !== "string") {
2124
- return new Text(typeof message.content === "string" ? message.content : "", 0, 0);
2124
+ return undefined;
2125
2125
  }
2126
2126
 
2127
2127
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tau-coding-agent",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Opinionated Pi package with web search, review workflows, sandboxing, memory, usage reporting, skills, and a theme",
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "type": "module",
29
29
  "dependencies": {
30
- "@anthropic-ai/sandbox-runtime": "^0.0.49",
30
+ "@anthropic-ai/sandbox-runtime": "^0.0.67",
31
31
  "@mozilla/readability": "^0.6.0",
32
32
  "jsdom": "^29.0.2",
33
33
  "puppeteer-core": "^24.40.0",
@@ -53,13 +53,13 @@
53
53
  "extensions/git-diff-stats.ts",
54
54
  "extensions/git-pr-status.ts",
55
55
  "extensions/insights.ts",
56
- "extensions/interlude.ts",
56
+ "extensions/stash.ts",
57
57
  "extensions/loop.ts",
58
58
  "extensions/memory.ts",
59
59
  "extensions/notify.ts",
60
60
  "extensions/openai-fast.ts",
61
61
  "extensions/openai-verbosity.ts",
62
- "extensions/review.ts",
62
+ "extensions/review/index.ts",
63
63
  "extensions/sandbox/index.ts",
64
64
  "extensions/tool-display-mode.ts",
65
65
  "extensions/usage/index.ts",
@@ -12,6 +12,7 @@ Use this skill to reimplement the current branch on a new branch with a clean, n
12
12
  1. **Validate the source branch**
13
13
  - Ensure no uncommitted changes or merge conflicts
14
14
  - Confirm it is up to date with `main`
15
+ - Confirm `main` is up to date with `origin/main`
15
16
 
16
17
  2. **Analyze the diff**
17
18
  - Study all changes between source branch and `main`
@@ -11,7 +11,7 @@ Use this skill as the playbook for producing reviewable commits and a clean, con
11
11
 
12
12
  1. Check what files have changed
13
13
  2. If there are no changes to commit, inform the user and stop
14
- 3. If there are unstaged changes, stage them with `git add`
14
+ 3. If there are unstaged changes, stage relevant hunks or files with `git add -p` or `git add`
15
15
 
16
16
  Rules:
17
17
 
@@ -21,7 +21,7 @@ Rules:
21
21
 
22
22
  ## Phase 2: Generate commit message
23
23
 
24
- 1. Remember commit messages best practices
24
+ 1. Remember commit message best practices
25
25
  2. Write a short subject line:
26
26
  - Up to 50 chars
27
27
  - Start subject with a capital letter, don’t end with a period
@@ -43,14 +43,14 @@ Run commands from this skill directory:
43
43
  ./scripts/oracle-bundle -p "<task>" --file "src/**" --file "!**/*.test.*"
44
44
 
45
45
  # Show which oracle model would be selected
46
- ./scripts/oracle --list-models --current openai-codex/gpt-5.5
46
+ ./scripts/oracle --list-models --current openai-codex/gpt-5.6-sol
47
47
 
48
48
  # Ask the automatically selected oracle model
49
- ./scripts/oracle --current openai-codex/gpt-5.5 \
49
+ ./scripts/oracle --current openai-codex/gpt-5.6-sol \
50
50
  -p "<task>" --file "src/**" --file "!**/*.test.*"
51
51
 
52
52
  # Override the oracle model when the automatic choice is wrong
53
- ./scripts/oracle --model github-copilot/claude-opus-4.7 \
53
+ ./scripts/oracle --model anthropic/claude-fable-5 \
54
54
  -p "<task>" --file "src/**" --file "!**/*.test.*"
55
55
  ```
56
56
 
@@ -1,12 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
- import { homedir } from "node:os";
5
4
  import path from "node:path";
5
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
- const THINKING_LEVEL = "xhigh";
8
+ const THINKING_LEVEL = "max";
9
9
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
10
+ const BARE_PI_ARGS = [
11
+ "-p",
12
+ "--no-session",
13
+ "--no-tools",
14
+ "--no-extensions",
15
+ "--no-skills",
16
+ "--no-prompt-templates",
17
+ "--no-themes",
18
+ ];
10
19
 
11
20
  const FAMILY_ORDER = {
12
21
  openai: ["anthropic", "google", "openai"],
@@ -47,11 +56,15 @@ if (bundle.status !== 0) {
47
56
  process.exit(bundle.status ?? 1);
48
57
  }
49
58
 
50
- const pi = spawnSync("pi", ["-p", "--no-tools", "--model", oracleModel, "--thinking", THINKING_LEVEL], {
51
- cwd: repoRoot(),
52
- input: bundle.stdout,
53
- stdio: ["pipe", "inherit", "inherit"],
54
- });
59
+ const pi = spawnSync(
60
+ "pi",
61
+ [...BARE_PI_ARGS, "--model", oracleModel, "--thinking", THINKING_LEVEL],
62
+ {
63
+ cwd: repoRoot(),
64
+ input: bundle.stdout,
65
+ stdio: ["pipe", "inherit", "inherit"],
66
+ },
67
+ );
55
68
 
56
69
  if (pi.error) die(pi.error.message);
57
70
  process.exit(pi.status ?? 1);
@@ -75,7 +88,12 @@ function parseArgs(argv) {
75
88
  }
76
89
  }
77
90
 
78
- return { bundleArgs, currentModel: currentModel ? normalizeModelSpec(currentModel) : undefined, model, listModels };
91
+ return {
92
+ bundleArgs,
93
+ currentModel: currentModel ? normalizeModelSpec(currentModel) : undefined,
94
+ model,
95
+ listModels,
96
+ };
79
97
  }
80
98
 
81
99
  function requireValue(argv, index, flag) {
@@ -85,8 +103,7 @@ function requireValue(argv, index, flag) {
85
103
  }
86
104
 
87
105
  function readSettings() {
88
- const agentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(homedir(), ".pi", "agent");
89
- const settingsPath = path.join(agentDir, "settings.json");
106
+ const settingsPath = path.join(getAgentDir(), "settings.json");
90
107
  if (!existsSync(settingsPath)) return {};
91
108
 
92
109
  try {
@@ -127,82 +144,86 @@ function selectOracleModel(models, enabledModels, currentFamily) {
127
144
 
128
145
  function bestModelForFamily(models, enabledModels, family) {
129
146
  return models
130
- .map((model) => ({ model, score: scoreModel(model, enabledModels, family) }))
131
- .filter((entry) => entry.score >= 0)
132
- .sort((a, b) => b.score - a.score || a.model.spec.localeCompare(b.model.spec))[0]?.model;
147
+ .filter((model) => modelFamily(model.spec) === family)
148
+ .sort((left, right) => compareModels(left, right, enabledModels, family))[0];
133
149
  }
134
150
 
135
- function scoreModel(entry, enabledModels, targetFamily) {
136
- if (modelFamily(entry.spec) !== targetFamily) return -1;
137
-
138
- let score = enabledModels.has(normalizeModelSpec(entry.spec)) ? 10_000 : 0;
139
- score += providerScore(entry.provider, targetFamily);
140
- score += nameScore(entry.model, targetFamily);
141
- return score;
151
+ function compareModels(left, right, enabledModels, family) {
152
+ return (
153
+ Number(enabledModels.has(normalizeModelSpec(right.spec))) -
154
+ Number(enabledModels.has(normalizeModelSpec(left.spec))) ||
155
+ modelRank(right.model, family) - modelRank(left.model, family) ||
156
+ providerRank(right.provider, family) - providerRank(left.provider, family) ||
157
+ left.spec.localeCompare(right.spec)
158
+ );
142
159
  }
143
160
 
144
- function providerScore(provider, family) {
161
+ function providerRank(provider, family) {
145
162
  const normalized = provider.toLowerCase();
146
163
  if (family === "openai") {
147
- if (normalized === "openai-codex") return 700;
148
- if (normalized === "openai") return 600;
149
- if (normalized === "github-copilot") return 500;
150
- if (normalized.includes("azure")) return 450;
151
- if (normalized === "openrouter") return 300;
152
- return 100;
164
+ if (normalized === "openai-codex") return 5;
165
+ if (normalized === "openai") return 4;
166
+ if (normalized === "github-copilot") return 3;
167
+ if (normalized.includes("azure")) return 2;
168
+ if (normalized === "openrouter") return 1;
169
+ return 0;
153
170
  }
154
171
 
155
172
  if (family === "anthropic") {
156
- if (normalized === "github-copilot") return 700;
157
- if (normalized === "anthropic") return 600;
158
- if (normalized === "amazon-bedrock") return 450;
159
- if (normalized === "openrouter") return 300;
160
- return 100;
173
+ if (normalized === "github-copilot") return 4;
174
+ if (normalized === "anthropic") return 3;
175
+ if (normalized === "amazon-bedrock") return 2;
176
+ if (normalized === "openrouter") return 1;
177
+ return 0;
161
178
  }
162
179
 
163
180
  if (family === "google") {
164
- if (normalized === "google") return 600;
165
- if (normalized === "github-copilot") return 500;
166
- if (normalized.includes("vertex")) return 450;
167
- if (normalized === "openrouter") return 300;
168
- return 100;
181
+ if (normalized === "google") return 4;
182
+ if (normalized === "github-copilot") return 3;
183
+ if (normalized.includes("vertex")) return 2;
184
+ if (normalized === "openrouter") return 1;
185
+ return 0;
169
186
  }
170
187
 
171
188
  return 0;
172
189
  }
173
190
 
174
- function nameScore(model, family) {
191
+ function modelRank(model, family) {
175
192
  const normalized = model.toLowerCase();
176
193
 
177
194
  if (family === "openai") {
178
- if (/gpt-5\.5/.test(normalized)) return 1_000;
179
- if (/gpt-5\.4/.test(normalized)) return 950;
180
- if (/gpt-5\.3.*codex/.test(normalized)) return 925;
181
- if (/gpt-5\.3/.test(normalized)) return 900;
182
- if (/gpt-5\.2/.test(normalized)) return 850;
183
- if (/gpt-5\.1/.test(normalized)) return 800;
184
- if (/gpt-5/.test(normalized)) return 750;
185
- if (/gpt-4\.1/.test(normalized)) return 500;
186
- return 100;
195
+ if (/gpt-5\.6[-_.]sol/.test(normalized)) return 10;
196
+ if (/gpt-5\.6/.test(normalized)) return 9;
197
+ if (/gpt-5\.5/.test(normalized)) return 8;
198
+ if (/gpt-5\.4/.test(normalized)) return 7;
199
+ if (/gpt-5\.3.*codex/.test(normalized)) return 6;
200
+ if (/gpt-5\.3/.test(normalized)) return 5;
201
+ if (/gpt-5\.2/.test(normalized)) return 4;
202
+ if (/gpt-5\.1/.test(normalized)) return 3;
203
+ if (/gpt-5/.test(normalized)) return 2;
204
+ if (/gpt-4\.1/.test(normalized)) return 1;
205
+ return 0;
187
206
  }
188
207
 
189
208
  if (family === "anthropic") {
190
- if (/claude[-_.]opus[-_.]4[-_.]7/.test(normalized)) return 1_000;
191
- if (/claude[-_.]opus[-_.]4[-_.]6/.test(normalized)) return 950;
192
- if (/claude[-_.]opus[-_.]4[-_.]5/.test(normalized)) return 900;
193
- if (/claude[-_.]opus[-_.]4[-_.]1/.test(normalized)) return 850;
194
- if (/claude[-_.]opus[-_.]4/.test(normalized)) return 800;
195
- if (/claude[-_.]sonnet[-_.]4[-_.]6/.test(normalized)) return 700;
196
- if (/claude[-_.]sonnet[-_.]4[-_.]5/.test(normalized)) return 650;
197
- if (/claude[-_.]sonnet[-_.]4/.test(normalized)) return 600;
198
- return 100;
209
+ if (/claude[-_.]fable[-_.]5/.test(normalized)) return 10;
210
+ if (/claude[-_.]opus[-_.]4[-_.]8/.test(normalized)) return 9;
211
+ if (/claude[-_.]opus[-_.]4[-_.]7/.test(normalized)) return 8;
212
+ if (/claude[-_.]opus[-_.]4[-_.]6/.test(normalized)) return 7;
213
+ if (/claude[-_.]opus[-_.]4[-_.]5/.test(normalized)) return 6;
214
+ if (/claude[-_.]opus[-_.]4[-_.]1/.test(normalized)) return 5;
215
+ if (/claude[-_.]opus[-_.]4/.test(normalized)) return 4;
216
+ if (/claude[-_.]sonnet[-_.]4[-_.]6/.test(normalized)) return 3;
217
+ if (/claude[-_.]sonnet[-_.]4[-_.]5/.test(normalized)) return 2;
218
+ if (/claude[-_.]sonnet[-_.]4/.test(normalized)) return 1;
219
+ return 0;
199
220
  }
200
221
 
201
222
  if (family === "google") {
202
- if (/gemini[-_.]3\.1[-_.]pro/.test(normalized)) return 1_000;
203
- if (/gemini[-_.]3[-_.]pro/.test(normalized)) return 950;
204
- if (/gemini[-_.]2\.5[-_.]pro/.test(normalized)) return 800;
205
- return 100;
223
+ if (/gemini[-_.]3\.1[-_.]pro/.test(normalized)) return 3;
224
+ if (/gemini[-_.]3[-_.]pro/.test(normalized)) return 2;
225
+ if (/gemini[-_.]2\.5[-_.]pro/.test(normalized)) return 1;
226
+ return 0;
206
227
  }
207
228
 
208
229
  return 0;
@@ -218,7 +239,7 @@ function modelFamily(spec) {
218
239
  }
219
240
 
220
241
  function normalizeModelSpec(spec) {
221
- return String(spec).trim().replace(/:(off|minimal|low|medium|high|xhigh)$/i, "");
242
+ return String(spec).trim().replace(/:(off|minimal|low|medium|high|xhigh|max)$/i, "");
222
243
  }
223
244
 
224
245
  function printModelPlan(models, enabledModels, currentModel, currentFamily) {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
3
  "name": "tau-dark",
4
4
  "vars": {
5
5
  "accent": "#8abeb7",
@@ -74,6 +74,7 @@
74
74
  "thinkingMedium": "#6f9bb3",
75
75
  "thinkingHigh": "#78a8a1",
76
76
  "thinkingXhigh": "accent",
77
+ "thinkingMax": "#a8d8d1",
77
78
 
78
79
  "bashMode": "green"
79
80
  },