tau-coding-agent 0.1.5 → 0.2.0

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 (86) hide show
  1. package/README.md +15 -12
  2. package/extensions/answer.ts +129 -73
  3. package/extensions/branch-term/README.md +7 -0
  4. package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
  5. package/extensions/btw.ts +90 -4
  6. package/extensions/caffeinate/README.md +5 -0
  7. package/extensions/caffeinate/index.ts +144 -0
  8. package/extensions/fast.ts +292 -0
  9. package/extensions/ghostty.ts +214 -211
  10. package/extensions/git-diff-stats.ts +124 -84
  11. package/extensions/git-pr-status.ts +274 -208
  12. package/extensions/insights.ts +138 -183
  13. package/extensions/loop.ts +150 -135
  14. package/extensions/memory.ts +172 -97
  15. package/extensions/notify.ts +14 -26
  16. package/extensions/openai-verbosity.ts +108 -42
  17. package/extensions/review/fix.ts +15 -6
  18. package/extensions/review/git.ts +93 -103
  19. package/extensions/review/index.ts +60 -62
  20. package/extensions/review/interrupt.ts +117 -0
  21. package/extensions/review/message-queue.ts +45 -12
  22. package/extensions/review/models.ts +79 -3
  23. package/extensions/review/prompts.ts +43 -40
  24. package/extensions/review/review.ts +161 -166
  25. package/extensions/review/runner.ts +139 -147
  26. package/extensions/review/runtime.ts +173 -120
  27. package/extensions/review/submit-review-tool.ts +1 -5
  28. package/extensions/review/submit-triage-tool.ts +1 -0
  29. package/extensions/review/triage.ts +41 -44
  30. package/extensions/sandbox/bash.ts +775 -0
  31. package/extensions/sandbox/command.ts +605 -0
  32. package/extensions/sandbox/config.ts +764 -0
  33. package/extensions/sandbox/index.ts +138 -2850
  34. package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
  35. package/extensions/sandbox/permissions/dialog.ts +118 -0
  36. package/extensions/sandbox/permissions/filesystem.ts +559 -0
  37. package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
  38. package/extensions/sandbox/permissions/network.ts +164 -0
  39. package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
  40. package/extensions/sandbox/runtime.ts +616 -0
  41. package/extensions/stash.ts +28 -15
  42. package/extensions/subagent/README.md +73 -0
  43. package/extensions/subagent/index.ts +822 -0
  44. package/extensions/subagent/interrupt.ts +117 -0
  45. package/extensions/subagent/permissions.ts +101 -0
  46. package/extensions/subagent/rpc.ts +177 -0
  47. package/extensions/tool-display-mode.ts +267 -64
  48. package/extensions/usage/anthropic.ts +5 -6
  49. package/extensions/usage/github-copilot.ts +30 -9
  50. package/extensions/usage/index.ts +250 -357
  51. package/extensions/usage/openai-codex.ts +2 -2
  52. package/extensions/usage/openrouter.ts +10 -2
  53. package/extensions/websearch/README.md +12 -47
  54. package/extensions/websearch/config.ts +5 -2
  55. package/extensions/websearch/index.ts +94 -109
  56. package/extensions/websearch/output.ts +51 -0
  57. package/extensions/websearch/providers/anthropic.pi.ts +27 -44
  58. package/extensions/websearch/providers/gemini.browser.ts +68 -77
  59. package/extensions/websearch/providers/gemini.pi.ts +17 -17
  60. package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
  61. package/extensions/websearch/providers/pi-model.shared.ts +44 -32
  62. package/extensions/websearch/providers/shared.ts +87 -2
  63. package/extensions/websearch/types.ts +0 -3
  64. package/extensions/worktree.ts +132 -172
  65. package/package.json +10 -5
  66. package/skills/browser-tools/SKILL.md +29 -234
  67. package/skills/browser-tools/references/cookies.md +36 -0
  68. package/skills/browser-tools/references/interaction.md +90 -0
  69. package/skills/browser-tools/references/logging.md +34 -0
  70. package/skills/git-clean-history/SKILL.md +6 -6
  71. package/skills/git-commit/SKILL.md +5 -3
  72. package/skills/github-pull-request/SKILL.md +60 -0
  73. package/skills/github-pull-request/references/create.md +40 -0
  74. package/skills/github-pull-request/references/stewardship.md +60 -0
  75. package/skills/oracle/SKILL.md +4 -4
  76. package/skills/oracle/scripts/oracle +64 -51
  77. package/skills/sentry/SKILL.md +15 -185
  78. package/skills/sentry/references/events.md +79 -0
  79. package/skills/sentry/references/issues.md +62 -0
  80. package/skills/sentry/references/logs.md +46 -0
  81. package/skills/update-changelog/SKILL.md +27 -121
  82. package/skills/web-design/SKILL.md +16 -105
  83. package/themes/tau-dark.json +4 -0
  84. package/extensions/openai-fast.ts +0 -229
  85. package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
  86. package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
@@ -20,7 +20,6 @@
20
20
 
21
21
  import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
22
22
  import {
23
- CURRENT_SESSION_VERSION,
24
23
  DynamicBorder,
25
24
  SessionManager,
26
25
  highlightCode,
@@ -35,7 +34,6 @@ import {
35
34
  matchesKey,
36
35
  } from "@earendil-works/pi-tui";
37
36
  import { spawnSync } from "node:child_process";
38
- import { randomUUID } from "node:crypto";
39
37
  import * as fs from "node:fs";
40
38
  import * as path from "node:path";
41
39
 
@@ -46,22 +44,6 @@ const SCRIPT_RERUN_ENTRY_TYPE = "worktree-script-rerun-command";
46
44
  const MANUAL_OPEN_INTRO =
47
45
  "Worktree ready. Open it in a separate terminal or tmux pane with this command";
48
46
 
49
- type PromptStatus = "completed" | "error";
50
-
51
- async function withPromptSignal<T>(pi: ExtensionAPI, run: () => Promise<T>): Promise<T> {
52
- pi.events.emit("ui:prompt_start", { source: "worktree" });
53
-
54
- let status: PromptStatus = "completed";
55
- try {
56
- return await run();
57
- } catch (error) {
58
- status = "error";
59
- throw error;
60
- } finally {
61
- pi.events.emit("ui:prompt_end", { source: "worktree", status });
62
- }
63
- }
64
-
65
47
  const FETCH_TIMEOUT_MS = 60_000;
66
48
  const STATUS_SPINNER_INTERVAL_MS = 80;
67
49
  const STATUS_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -620,7 +602,6 @@ function pathExistsAndIsNotEmptyDir(p: string): boolean {
620
602
  }
621
603
 
622
604
  async function resolveWorktreePath(
623
- pi: ExtensionAPI,
624
605
  ctx: ExtensionCommandContext,
625
606
  repo: RepoInfo,
626
607
  worktrees: WorktreeInfo[],
@@ -639,9 +620,7 @@ async function resolveWorktreePath(
639
620
  `Branch name "${branch}" normalizes to an empty directory name. Please choose a worktree directory.`,
640
621
  "warning",
641
622
  );
642
- const input = await withPromptSignal(pi, () =>
643
- ctx.ui.input("Enter worktree directory", suggested),
644
- );
623
+ const input = await ctx.ui.input("Enter worktree directory", suggested);
645
624
  if (!input) return null;
646
625
  candidate = path.isAbsolute(input) ? input : path.resolve(repo.currentRoot, input);
647
626
  } else {
@@ -663,9 +642,7 @@ async function resolveWorktreePath(
663
642
  }
664
643
 
665
644
  ctx.ui.notify(`${reason}: ${candidate}`, "warning");
666
- const input = await withPromptSignal(pi, () =>
667
- ctx.ui.input("Enter a different worktree directory", candidate),
668
- );
645
+ const input = await ctx.ui.input("Enter a different worktree directory", candidate);
669
646
  if (!input) return null;
670
647
 
671
648
  candidate = path.isAbsolute(input) ? input : path.resolve(repo.currentRoot, input);
@@ -848,13 +825,14 @@ function parseWorktreeIncludePatterns(content: string): Array<{ glob: string; ne
848
825
  function matchesWorktreeInclude(
849
826
  entry: string,
850
827
  patterns: Array<{ glob: string; negate: boolean }>,
828
+ included = false,
851
829
  ): boolean {
852
- // Normalize trailing slashes: git ls-files --directory appends / to directories,
853
- // and patterns may or may not have trailing /. Strip both for matching since
854
- // --directory already ensures we only get directory entries for directory patterns.
855
- const normalizedEntry = entry.replace(/\/$/, "");
856
- let matched = false;
830
+ // Both git ls-files --directory and our traversal mark directories with /.
831
+ const isDirectory = entry.endsWith("/");
832
+ const normalizedEntry = isDirectory ? entry.slice(0, -1) : entry;
833
+ let matched = included;
857
834
  for (const { glob, negate } of patterns) {
835
+ if (glob.endsWith("/") && !isDirectory) continue;
858
836
  const normalizedGlob = glob.replace(/\/$/, "");
859
837
  if (path.matchesGlob(normalizedEntry, normalizedGlob)) {
860
838
  matched = !negate;
@@ -873,10 +851,22 @@ function matchesWorktreeInclude(
873
851
  * clonefile on large directories can saturate disk I/O and block interactive
874
852
  * processes). Symlinks are preserved (re-created, not followed).
875
853
  */
876
- function copyDirRecursive(src: string, dest: string): void {
854
+ function copyDirRecursive(
855
+ src: string,
856
+ dest: string,
857
+ relativeDir: string,
858
+ patterns: Array<{ glob: string; negate: boolean }>,
859
+ ): void {
877
860
  fs.mkdirSync(dest, { recursive: true });
878
861
 
879
862
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
863
+ const relativePath = `${relativeDir}/${entry.name}`;
864
+ // Children inherit inclusion, but their own rules can override it. Like
865
+ // gitignore, excluded directories are pruned; reinclude the directory itself
866
+ // before trying to reinclude anything inside it. No preliminary tree scan.
867
+ if (!matchesWorktreeInclude(relativePath + (entry.isDirectory() ? "/" : ""), patterns, true)) {
868
+ continue;
869
+ }
880
870
  const srcPath = path.join(src, entry.name);
881
871
  const destPath = path.join(dest, entry.name);
882
872
 
@@ -888,7 +878,7 @@ function copyDirRecursive(src: string, dest: string): void {
888
878
  if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
889
879
  }
890
880
  } else if (entry.isDirectory()) {
891
- copyDirRecursive(srcPath, destPath);
881
+ copyDirRecursive(srcPath, destPath, relativePath, patterns);
892
882
  } else {
893
883
  try {
894
884
  fs.copyFileSync(srcPath, destPath, COPYFILE_COW_EXCL);
@@ -955,11 +945,9 @@ async function applyWorktreeInclude(
955
945
  if (entriesToCopy.length === 0) return;
956
946
 
957
947
  const listing = entriesToCopy.map((e) => ` ${e}`).join("\n");
958
- const ok = await withPromptSignal(pi, () =>
959
- ctx.ui.confirm(
960
- "Copy cached files from main worktree?",
961
- `Found .worktreeinclude. Copy these gitignored entries:\n\n${listing}`,
962
- ),
948
+ const ok = await ctx.ui.confirm(
949
+ "Copy cached files from main worktree?",
950
+ `Found .worktreeinclude. Copy these gitignored entries:\n\n${listing}`,
963
951
  );
964
952
  if (!ok) return;
965
953
 
@@ -977,7 +965,7 @@ async function applyWorktreeInclude(
977
965
  try {
978
966
  const srcStat = fs.lstatSync(src);
979
967
  if (srcStat.isDirectory()) {
980
- copyDirRecursive(src, dest);
968
+ copyDirRecursive(src, dest, entry.replace(/\/$/, ""), patterns);
981
969
  } else if (srcStat.isSymbolicLink()) {
982
970
  const target = fs.readlinkSync(src);
983
971
  const destParent = path.dirname(dest);
@@ -1023,27 +1011,24 @@ async function runProjectScripts(
1023
1011
 
1024
1012
  if (actions.length === 1) {
1025
1013
  const action = actions[0];
1026
- const ok = await withPromptSignal(pi, () =>
1027
- ctx.ui.confirm(`Run worktree ${phase}?`, `${action.label}\n\nCommand:\n${action.command}`),
1014
+ const ok = await ctx.ui.confirm(
1015
+ `Run worktree ${phase}?`,
1016
+ `${action.label}\n\nCommand:\n${action.command}`,
1028
1017
  );
1029
1018
  if (!ok) return;
1030
1019
  chosen = action;
1031
1020
  } else {
1032
1021
  const options = ["Skip", ...actions.map((a) => `${a.label} (${a.source})`)];
1033
- const choice = await withPromptSignal(pi, () =>
1034
- ctx.ui.select(`Choose ${phase} script to run`, options),
1035
- );
1022
+ const choice = await ctx.ui.select(`Choose ${phase} script to run`, options);
1036
1023
  if (!choice || choice === "Skip") return;
1037
1024
 
1038
1025
  const idx = options.indexOf(choice) - 1;
1039
1026
  const selectedAction = actions[idx];
1040
1027
  if (!selectedAction) return;
1041
1028
 
1042
- const ok = await withPromptSignal(pi, () =>
1043
- ctx.ui.confirm(
1044
- `Run worktree ${phase}?`,
1045
- `${selectedAction.label}\n\nCommand:\n${selectedAction.command}`,
1046
- ),
1029
+ const ok = await ctx.ui.confirm(
1030
+ `Run worktree ${phase}?`,
1031
+ `${selectedAction.label}\n\nCommand:\n${selectedAction.command}`,
1047
1032
  );
1048
1033
  if (!ok) return;
1049
1034
 
@@ -1092,7 +1077,6 @@ type ParsedWorktreeTargetArgs = {
1092
1077
  };
1093
1078
 
1094
1079
  async function parseWorktreeTargetArgs(
1095
- pi: ExtensionAPI,
1096
1080
  ctx: ExtensionCommandContext,
1097
1081
  args: string,
1098
1082
  usage: string,
@@ -1103,7 +1087,7 @@ async function parseWorktreeTargetArgs(
1103
1087
  if (!ctx.hasUI) {
1104
1088
  throw new Error(usage);
1105
1089
  }
1106
- const input = await withPromptSignal(pi, () => ctx.ui.input("Branch name"));
1090
+ const input = await ctx.ui.input("Branch name");
1107
1091
  if (!input) return null;
1108
1092
  branch = input.trim();
1109
1093
  }
@@ -1166,22 +1150,18 @@ async function maybeSwitchMainToDefaultBranch(
1166
1150
  );
1167
1151
  }
1168
1152
 
1169
- const ok = await withPromptSignal(pi, () =>
1170
- ctx.ui.confirm(
1171
- "Switch main worktree?",
1172
- `Main worktree is on ${current}. Checkout ${defaultBranch} to ${reason}?`,
1173
- ),
1153
+ const ok = await ctx.ui.confirm(
1154
+ "Switch main worktree?",
1155
+ `Main worktree is on ${current}. Checkout ${defaultBranch} to ${reason}?`,
1174
1156
  );
1175
1157
  if (!ok) return { proceed: false, switched: false };
1176
1158
 
1177
1159
  let stashSpec: string | undefined;
1178
1160
  if (await isDirty(pi, repo.mainRoot)) {
1179
- const choice = await withPromptSignal(pi, () =>
1180
- ctx.ui.select("Main worktree has uncommitted changes", [
1181
- "Stash changes (including untracked) and continue",
1182
- "Cancel",
1183
- ]),
1184
- );
1161
+ const choice = await ctx.ui.select("Main worktree has uncommitted changes", [
1162
+ "Stash changes (including untracked) and continue",
1163
+ "Cancel",
1164
+ ]);
1185
1165
  if (!choice || choice.startsWith("Cancel")) {
1186
1166
  return { proceed: false, switched: false };
1187
1167
  }
@@ -1217,17 +1197,15 @@ async function createWorktree(
1217
1197
  branch: string,
1218
1198
  fromRef?: string,
1219
1199
  ): Promise<string | null> {
1220
- const targetPath = await resolveWorktreePath(pi, ctx, repo, worktrees, branch);
1200
+ const targetPath = await resolveWorktreePath(ctx, repo, worktrees, branch);
1221
1201
  if (!targetPath) return null;
1222
1202
 
1223
1203
  const branchExists = await localBranchExists(pi, repo.mainRoot, branch);
1224
1204
  if (branchExists && fromRef) {
1225
1205
  if (ctx.hasUI) {
1226
- const ok = await withPromptSignal(pi, () =>
1227
- ctx.ui.confirm(
1228
- "Branch exists",
1229
- `Branch ${branch} already exists.\n\nContinuing will use the existing branch at its current state; --from (${fromRef}) will have no effect.\n\nContinue?`,
1230
- ),
1206
+ const ok = await ctx.ui.confirm(
1207
+ "Branch exists",
1208
+ `Branch ${branch} already exists.\n\nContinuing will use the existing branch at its current state; --from (${fromRef}) will have no effect.\n\nContinue?`,
1231
1209
  );
1232
1210
  if (!ok) return null;
1233
1211
  } else {
@@ -1281,22 +1259,12 @@ function hasValidSessionFile(sessionFile: string): boolean {
1281
1259
  }
1282
1260
  }
1283
1261
 
1284
- function createFreshSessionFile(targetCwd: string, sessionDir: string): string {
1285
- fs.mkdirSync(sessionDir, { recursive: true });
1286
-
1287
- const sessionId = randomUUID();
1288
- const timestamp = new Date().toISOString();
1289
- const fileTimestamp = timestamp.replace(/[:.]/g, "-");
1290
- const sessionFile = path.join(sessionDir, `${fileTimestamp}_${sessionId}.jsonl`);
1291
- const header: SessionHeader = {
1292
- type: "session",
1293
- version: CURRENT_SESSION_VERSION,
1294
- id: sessionId,
1295
- timestamp,
1296
- cwd: targetCwd,
1297
- };
1298
-
1299
- fs.writeFileSync(sessionFile, `${JSON.stringify(header)}\n`);
1262
+ function createFreshSessionFile(targetCwd: string, sessionDir?: string): string {
1263
+ const session = SessionManager.create(targetCwd, sessionDir);
1264
+ 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.
1267
+ fs.writeFileSync(sessionFile, `${JSON.stringify(session.getHeader())}\n`, { flag: "wx" });
1300
1268
  return sessionFile;
1301
1269
  }
1302
1270
 
@@ -1334,14 +1302,24 @@ async function switchToWorktree(
1334
1302
  return;
1335
1303
  }
1336
1304
 
1337
- const sessionDir = ctx.sessionManager.getSessionDir();
1305
+ const currentSessionDir = ctx.sessionManager.getSessionDir();
1306
+ // The read-only context omits usesDefaultSessionDir(). A new, unwritten manager
1307
+ // asks Pi's native storage policy without opening or modifying source history,
1308
+ // including when the source has not been persisted yet.
1309
+ const storage = SessionManager.create(ctx.sessionManager.getCwd(), currentSessionDir);
1310
+ const sessionDir = storage.usesDefaultSessionDir() ? undefined : currentSessionDir;
1338
1311
  const hasAssistantReply = ctx.sessionManager
1339
1312
  .getEntries()
1340
1313
  .some((entry) => entry.type === "message" && entry.message.role === "assistant");
1341
1314
 
1342
1315
  let sessionFile: string;
1343
1316
  if (hasValidSessionFile(currentSessionFile)) {
1317
+ const leafId = ctx.sessionManager.getLeafId();
1344
1318
  const nextSession = SessionManager.forkFrom(currentSessionFile, targetPath, sessionDir);
1319
+ if (leafId) nextSession.branch(leafId);
1320
+ else nextSession.resetLeaf();
1321
+ // Leaf navigation alone is not persisted when the session is reopened.
1322
+ nextSession.appendCustomEntry("worktree-branch", { sourceLeafId: leafId });
1345
1323
  const forkedSessionFile = nextSession.getSessionFile();
1346
1324
  if (!forkedSessionFile) {
1347
1325
  throw new Error(`Failed to create a session for worktree: ${targetPath}`);
@@ -1382,7 +1360,6 @@ async function handleNew(
1382
1360
  args: string,
1383
1361
  ): Promise<void> {
1384
1362
  const parsed = await parseWorktreeTargetArgs(
1385
- pi,
1386
1363
  ctx,
1387
1364
  args,
1388
1365
  "Usage: /worktree new <branch> [--from <ref>]",
@@ -1474,7 +1451,6 @@ async function handleSwitch(
1474
1451
  args: string,
1475
1452
  ): Promise<void> {
1476
1453
  const parsed = await parseWorktreeTargetArgs(
1477
- pi,
1478
1454
  ctx,
1479
1455
  args,
1480
1456
  "Usage: /worktree switch <branch> [--from <ref>]",
@@ -1562,13 +1538,11 @@ async function archiveWorktree(
1562
1538
  throw new Error(`Cannot archive dirty worktree without UI: ${wt.path}`);
1563
1539
  }
1564
1540
 
1565
- const choice = await withPromptSignal(pi, () =>
1566
- ctx.ui.select(`Worktree has uncommitted changes: ${wt.path}`, [
1567
- "Stash changes (including untracked) and archive",
1568
- "Force remove (lose changes)",
1569
- "Cancel",
1570
- ]),
1571
- );
1541
+ const choice = await ctx.ui.select(`Worktree has uncommitted changes: ${wt.path}`, [
1542
+ "Stash changes (including untracked) and archive",
1543
+ "Force remove (lose changes)",
1544
+ "Cancel",
1545
+ ]);
1572
1546
 
1573
1547
  if (!choice || choice === "Cancel") {
1574
1548
  return {
@@ -1634,11 +1608,9 @@ async function archiveWorktree(
1634
1608
 
1635
1609
  // Branch has commits not on upstream (not fully pushed)
1636
1610
  if (ctx.hasUI) {
1637
- const ok = await withPromptSignal(pi, () =>
1638
- ctx.ui.confirm(
1639
- "Delete local branch?",
1640
- `Branch ${branch} is ahead of ${upstream} by ${ahead} commit(s). Delete it anyway?`,
1641
- ),
1611
+ const ok = await ctx.ui.confirm(
1612
+ "Delete local branch?",
1613
+ `Branch ${branch} is ahead of ${upstream} by ${ahead} commit(s). Delete it anyway?`,
1642
1614
  );
1643
1615
  if (ok) {
1644
1616
  const del = await git(pi, repo.mainRoot, ["branch", "-D", branch]);
@@ -1648,11 +1620,9 @@ async function archiveWorktree(
1648
1620
  } else if (!aheadBehind) {
1649
1621
  // Couldn't determine ahead/behind; be conservative.
1650
1622
  if (ctx.hasUI) {
1651
- const ok = await withPromptSignal(pi, () =>
1652
- ctx.ui.confirm(
1653
- "Delete local branch?",
1654
- `Branch ${branch} has an upstream (${upstream}), but I couldn't determine if it's fully pushed. Delete it anyway?`,
1655
- ),
1623
+ const ok = await ctx.ui.confirm(
1624
+ "Delete local branch?",
1625
+ `Branch ${branch} has an upstream (${upstream}), but I couldn't determine if it's fully pushed. Delete it anyway?`,
1656
1626
  );
1657
1627
  if (ok) {
1658
1628
  const del = await git(pi, repo.mainRoot, ["branch", "-D", branch]);
@@ -1667,11 +1637,9 @@ async function archiveWorktree(
1667
1637
 
1668
1638
  if (!branchDeleted && ctx.hasUI) {
1669
1639
  const details = [del.stdout.trim(), del.stderr.trim()].filter(Boolean).join("\n");
1670
- const ok = await withPromptSignal(pi, () =>
1671
- ctx.ui.confirm(
1672
- "Force delete local branch?",
1673
- `git branch -d ${branch} failed.${details ? `\n\n${details}` : ""}\n\nThis usually means the branch isn't merged into the main worktree's current branch.\n\nThe branch appears fully pushed to ${upstream}. Force delete it with -D?`,
1674
- ),
1640
+ const ok = await ctx.ui.confirm(
1641
+ "Force delete local branch?",
1642
+ `git branch -d ${branch} failed.${details ? `\n\n${details}` : ""}\n\nThis usually means the branch isn't merged into the main worktree's current branch.\n\nThe branch appears fully pushed to ${upstream}. Force delete it with -D?`,
1675
1643
  );
1676
1644
  if (ok) {
1677
1645
  const forceDel = await git(pi, repo.mainRoot, ["branch", "-D", branch]);
@@ -1688,11 +1656,9 @@ async function archiveWorktree(
1688
1656
  }
1689
1657
  } else {
1690
1658
  if (ctx.hasUI) {
1691
- const ok = await withPromptSignal(pi, () =>
1692
- ctx.ui.confirm(
1693
- "Delete local branch?",
1694
- `Branch ${branch} has no upstream. Delete it anyway?`,
1695
- ),
1659
+ const ok = await ctx.ui.confirm(
1660
+ "Delete local branch?",
1661
+ `Branch ${branch} has no upstream. Delete it anyway?`,
1696
1662
  );
1697
1663
  if (ok) {
1698
1664
  const del = await git(pi, repo.mainRoot, ["branch", "-D", branch]);
@@ -1718,7 +1684,7 @@ async function handleArchive(
1718
1684
  let branch = tokens[0];
1719
1685
  if (!branch || branch.startsWith("-")) {
1720
1686
  if (!ctx.hasUI) return;
1721
- const input = await withPromptSignal(pi, () => ctx.ui.input("Branch name"));
1687
+ const input = await ctx.ui.input("Branch name");
1722
1688
  if (!input) return;
1723
1689
  branch = input.trim();
1724
1690
  }
@@ -1848,16 +1814,14 @@ async function handleClean(pi: ExtensionAPI, ctx: ExtensionCommandContext): Prom
1848
1814
  const dirtyCount = candidates.filter((c) => c.dirty).length;
1849
1815
 
1850
1816
  if (dirtyCount > 0) {
1851
- const choice = await withPromptSignal(pi, () =>
1852
- ctx.ui.select(
1853
- `Found ${candidates.length} pushed worktree(s): ${candidates.length - dirtyCount} clean, ${dirtyCount} dirty`,
1854
- [
1855
- "Archive clean only (skip dirty)",
1856
- "Stash dirty (including untracked) and archive all",
1857
- "Force remove dirty and archive all (lose changes)",
1858
- "Cancel",
1859
- ],
1860
- ),
1817
+ const choice = await ctx.ui.select(
1818
+ `Found ${candidates.length} pushed worktree(s): ${candidates.length - dirtyCount} clean, ${dirtyCount} dirty`,
1819
+ [
1820
+ "Archive clean only (skip dirty)",
1821
+ "Stash dirty (including untracked) and archive all",
1822
+ "Force remove dirty and archive all (lose changes)",
1823
+ "Cancel",
1824
+ ],
1861
1825
  );
1862
1826
 
1863
1827
  if (!choice || choice === "Cancel") {
@@ -1871,11 +1835,9 @@ async function handleClean(pi: ExtensionAPI, ctx: ExtensionCommandContext): Prom
1871
1835
  ? "force"
1872
1836
  : "skip";
1873
1837
  } else {
1874
- const ok = await withPromptSignal(pi, () =>
1875
- ctx.ui.confirm(
1876
- "Archive pushed worktrees?",
1877
- `Archive ${candidates.length} pushed worktree(s)?`,
1878
- ),
1838
+ const ok = await ctx.ui.confirm(
1839
+ "Archive pushed worktrees?",
1840
+ `Archive ${candidates.length} pushed worktree(s)?`,
1879
1841
  );
1880
1842
  if (!ok) {
1881
1843
  ctx.ui.notify("Cancelled", "warning");
@@ -2026,59 +1988,57 @@ async function handleList(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promi
2026
1988
  const theme = ctx.ui.theme;
2027
1989
 
2028
1990
  const selectItems: SelectItem[] = items.map((item) => ({
2029
- value: item.branch,
1991
+ value: item.wt.path,
2030
1992
  label: formatWorktreeLabel(item, theme),
2031
1993
  }));
2032
1994
 
2033
- const itemByValue = new Map(items.map((item) => [item.branch, item]));
1995
+ const itemByValue = new Map(items.map((item) => [item.wt.path, item]));
2034
1996
 
2035
1997
  type ListResult = { action: "switch" | "archive"; item: WorktreeDisplayItem } | null;
2036
1998
 
2037
- const result = await withPromptSignal(pi, () =>
2038
- ctx.ui.custom<ListResult>((tui, theme, _kb, done) => {
2039
- const container = new Container();
2040
- container.addChild(new DynamicBorder((s: string) => theme.fg("borderMuted", s)));
2041
-
2042
- const selectList = new SelectList(selectItems, Math.min(selectItems.length, 15), {
2043
- selectedPrefix: (t) => theme.fg("accent", t),
2044
- selectedText: (t) => theme.fg("accent", t),
2045
- description: (t) => t,
2046
- scrollInfo: (t) => theme.fg("dim", t),
2047
- noMatch: (t) => theme.fg("warning", t),
2048
- });
2049
- selectList.onSelect = (si) => {
2050
- const item = itemByValue.get(si.value);
2051
- if (item) done({ action: "switch", item });
2052
- else done(null);
2053
- };
2054
- selectList.onCancel = () => done(null);
2055
- container.addChild(selectList);
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)));
2056
2002
 
2057
- container.addChild(
2058
- new Text(theme.fg("dim", " ↑↓ navigate enter switch a archive esc close"), 0, 0),
2059
- );
2060
- container.addChild(new DynamicBorder((s: string) => theme.fg("borderMuted", s)));
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);
2061
2017
 
2062
- return {
2063
- render: (w) => container.render(w),
2064
- invalidate: () => container.invalidate(),
2065
- handleInput: (data) => {
2066
- if (matchesKey(data, "a")) {
2067
- const si = selectList.getSelectedItem();
2068
- if (si) {
2069
- const item = itemByValue.get(si.value);
2070
- if (item) {
2071
- done({ action: "archive", item });
2072
- return;
2073
- }
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;
2074
2034
  }
2075
2035
  }
2076
- selectList.handleInput(data);
2077
- tui.requestRender();
2078
- },
2079
- };
2080
- }),
2081
- );
2036
+ }
2037
+ selectList.handleInput(data);
2038
+ tui.requestRender();
2039
+ },
2040
+ };
2041
+ });
2082
2042
 
2083
2043
  if (!result) return;
2084
2044
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tau-coding-agent",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
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,12 +27,14 @@
27
27
  },
28
28
  "type": "module",
29
29
  "dependencies": {
30
- "@anthropic-ai/sandbox-runtime": "^0.0.49",
30
+ "@anthropic-ai/sandbox-runtime": "^0.0.73",
31
31
  "@mozilla/readability": "^0.6.0",
32
32
  "jsdom": "^29.0.2",
33
+ "proper-lockfile": "4.1.2",
33
34
  "puppeteer-core": "^24.40.0",
34
35
  "turndown": "^7.2.4",
35
- "turndown-plugin-gfm": "^1.0.2"
36
+ "turndown-plugin-gfm": "^1.0.2",
37
+ "undici": "8.9.0"
36
38
  },
37
39
  "peerDependencies": {
38
40
  "@earendil-works/pi-agent-core": "*",
@@ -47,8 +49,9 @@
47
49
  "pi": {
48
50
  "extensions": [
49
51
  "extensions/answer.ts",
50
- "extensions/branch-term.ts",
52
+ "extensions/branch-term/index.ts",
51
53
  "extensions/btw.ts",
54
+ "extensions/caffeinate/index.ts",
52
55
  "extensions/ghostty.ts",
53
56
  "extensions/git-diff-stats.ts",
54
57
  "extensions/git-pr-status.ts",
@@ -57,10 +60,11 @@
57
60
  "extensions/loop.ts",
58
61
  "extensions/memory.ts",
59
62
  "extensions/notify.ts",
60
- "extensions/openai-fast.ts",
63
+ "extensions/fast.ts",
61
64
  "extensions/openai-verbosity.ts",
62
65
  "extensions/review/index.ts",
63
66
  "extensions/sandbox/index.ts",
67
+ "extensions/subagent/index.ts",
64
68
  "extensions/tool-display-mode.ts",
65
69
  "extensions/usage/index.ts",
66
70
  "extensions/websearch/index.ts",
@@ -70,6 +74,7 @@
70
74
  "skills/browser-tools",
71
75
  "skills/git-clean-history",
72
76
  "skills/git-commit",
77
+ "skills/github-pull-request",
73
78
  "skills/oracle",
74
79
  "skills/sentry",
75
80
  "skills/update-changelog",