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
package/README.md CHANGED
@@ -23,6 +23,7 @@ pi install -l npm:tau-coding-agent
23
23
  | `answer` | `/answer` | Extract and interactively answer agent questions. |
24
24
  | `branch-term` | `/branch` | Open a new terminal on the current session's git branch. |
25
25
  | `btw` | `/btw` | Run a one-off side request with read-only tools and no context persistence. |
26
+ | `caffeinate` | — | Prevent system sleep while the agent runs. |
26
27
  | `ghostty` | — | Ghostty tab title enhancements while the agent is working, waiting, or idle. |
27
28
  | `git-diff-stats` | — | Status bar diff stats for local changes in the current repo. |
28
29
  | `git-pr-status` | — | Status bar PR number and link for the current branch. |
@@ -31,26 +32,28 @@ pi install -l npm:tau-coding-agent
31
32
  | `loop` | `/loop` | Repeat a prompt until the agent signals success. |
32
33
  | `memory` | `/memory` | Opt-in project-local memory for learning and continuity across sessions. |
33
34
  | `notify` | — | Terminal notification when the agent is waiting for input. |
34
- | `openai-fast` | `/fast` | Toggle priority service tier for supported OpenAI models. |
35
- | `openai-verbosity` | `/verbosity` | Set verbosity for supported OpenAI models. |
35
+ | `fast` | `/fast` | Toggle fast processing for compatible models. |
36
+ | `openai-verbosity` | `/verbosity` | Set verbosity for supported GPT models. |
36
37
  | `review` | `/review`, `/triage` | Multi-focus review and PR feedback triage for PRs, branches, commits, and local changes, with integrated follow-up fixes. |
37
38
  | `sandbox` | `/sandbox` | OS-level sandboxing for bash commands with runtime overrides. |
38
- | `tool-display-mode` | `ctrl+o` | Cycle tool output between Pi's default rendering, expanded output, and compact summaries. |
39
+ | `subagent` | — | Delegate work to background Pi agents and follow up with them. |
40
+ | `tool-display-mode` | `ctrl+o` | Cycle tool output modes and combine background activity in the composer. |
39
41
  | `usage` | `/usage` | Historical provider usage breakdown with all-provider history and live quota snapshots. |
40
42
  | `websearch` | — | Web search via Gemini, OpenAI, or Claude, leveraging Pi or browser session credentials. |
41
43
  | `worktree` | `/worktree` | Create, list, and archive git worktrees, optionally opening them in a new terminal or tmux pane. |
42
44
 
43
45
  ## Skills
44
46
 
45
- | Skill | Description |
46
- | ------------------- | ---------------------------------------------------------------------------------- |
47
- | `browser-tools` | Interactive browser automation via Chrome DevTools Protocol. |
48
- | `git-clean-history` | Reimplement a branch on a fresh branch off `main` with a clean commit history. |
49
- | `git-commit` | Tidy, focused commits with clear rationale in messages. |
50
- | `oracle` | Second opinion from another LLM for debugging, refactors, design, or code reviews. |
51
- | `sentry` | Fetch and analyze Sentry issues, events, transactions, and logs. |
52
- | `update-changelog` | Update CHANGELOG.md following Keep a Changelog. |
53
- | `web-design` | Distinctive, production-ready web interfaces. |
47
+ | Skill | Description |
48
+ | --------------------- | ---------------------------------------------------------------------------------- |
49
+ | `browser-tools` | Interactive browser automation via Chrome DevTools Protocol. |
50
+ | `git-clean-history` | Reimplement a branch on a fresh branch off `main` with a clean commit history. |
51
+ | `git-commit` | Tidy, focused commits with clear rationale in messages. |
52
+ | `github-pull-request` | Create and steward pull requests through CI, review, and merge. |
53
+ | `oracle` | Second opinion from another LLM for debugging, refactors, design, or code reviews. |
54
+ | `sentry` | Fetch and analyze Sentry issues, events, transactions, and logs. |
55
+ | `update-changelog` | Update CHANGELOG.md following Keep a Changelog. |
56
+ | `web-design` | Distinctive, production-ready web interfaces. |
54
57
 
55
58
  ## Themes
56
59
 
@@ -10,9 +10,13 @@
10
10
  * 4. Submits the compiled answers when done
11
11
  */
12
12
 
13
- import { complete } from "@earendil-works/pi-ai/compat";
14
13
  import type { Model, Api, UserMessage } from "@earendil-works/pi-ai";
15
- import { BorderedLoader, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent";
14
+ import {
15
+ BorderedLoader,
16
+ type ExtensionAPI,
17
+ type ModelRegistry,
18
+ type Theme,
19
+ } from "@earendil-works/pi-coding-agent";
16
20
  import {
17
21
  type Component,
18
22
  Editor,
@@ -26,6 +30,8 @@ import {
26
30
  wrapTextWithAnsi,
27
31
  } from "@earendil-works/pi-tui";
28
32
 
33
+ const MIN_QUESTIONNAIRE_WIDTH = 40;
34
+
29
35
  const SYSTEM_PROMPT = `You extract items that need user input from assistant text:
30
36
 
31
37
  - Direct questions
@@ -78,7 +84,7 @@ Example output:
78
84
  "questions": []
79
85
  }`;
80
86
 
81
- const OPENAI_FAST_MODEL_ID = "gpt-5.3-codex-spark";
87
+ const OPENAI_FAST_MODEL_ID = "gpt-5.6-luna";
82
88
  const ANTHROPIC_FAST_MODEL_ID = "claude-haiku-4-5";
83
89
 
84
90
  // Structured output format for question extraction
@@ -91,20 +97,16 @@ interface ExtractionResult {
91
97
  questions: ExtractedQuestion[];
92
98
  }
93
99
 
94
- interface ModelRequestAuth {
100
+ type ExtractionOutcome =
101
+ | { status: "success"; result: ExtractionResult }
102
+ | { status: "cancelled" }
103
+ | { status: "error"; message: string };
104
+
105
+ interface ModelSelection {
95
106
  model: Model<Api>;
96
- apiKey?: string;
97
- headers?: Record<string, string>;
98
107
  }
99
108
 
100
- type ModelAuthLookup = {
101
- find: (provider: string, modelId: string) => Model<Api> | undefined;
102
- getApiKeyAndHeaders: (
103
- model: Model<Api>,
104
- ) => Promise<
105
- { ok: true; apiKey?: string; headers?: Record<string, string> } | { ok: false; error: string }
106
- >;
107
- };
109
+ type ModelLookup = Pick<ModelRegistry, "find" | "getApiKeyAndHeaders">;
108
110
 
109
111
  type ModelFamily = "openai" | "anthropic";
110
112
 
@@ -120,10 +122,10 @@ function detectModelFamily(provider: string): ModelFamily | null {
120
122
  */
121
123
  async function selectExtractionModel(
122
124
  currentModel: Model<Api>,
123
- modelRegistry: ModelAuthLookup,
124
- ): Promise<ModelRequestAuth | null> {
125
+ modelRegistry: ModelLookup,
126
+ ): Promise<ModelSelection | null> {
125
127
  const family = detectModelFamily(currentModel.provider);
126
- if (!family) return resolveModelRequestAuth(modelRegistry, currentModel);
128
+ if (!family) return resolveModelSelection(modelRegistry, currentModel);
127
129
 
128
130
  const modelId = family === "openai" ? OPENAI_FAST_MODEL_ID : ANTHROPIC_FAST_MODEL_ID;
129
131
  const providerCandidates =
@@ -135,21 +137,21 @@ async function selectExtractionModel(
135
137
  const model = modelRegistry.find(provider, modelId);
136
138
  if (!model) continue;
137
139
 
138
- const auth = await resolveModelRequestAuth(modelRegistry, model);
139
- if (auth) {
140
- return auth;
140
+ const selection = await resolveModelSelection(modelRegistry, model);
141
+ if (selection) {
142
+ return selection;
141
143
  }
142
144
  }
143
145
 
144
- return resolveModelRequestAuth(modelRegistry, currentModel);
146
+ return resolveModelSelection(modelRegistry, currentModel);
145
147
  }
146
148
 
147
- async function resolveModelRequestAuth(
148
- modelRegistry: ModelAuthLookup,
149
+ async function resolveModelSelection(
150
+ modelRegistry: ModelLookup,
149
151
  model: Model<Api>,
150
- ): Promise<ModelRequestAuth | null> {
152
+ ): Promise<ModelSelection | null> {
151
153
  const auth = await modelRegistry.getApiKeyAndHeaders(model);
152
- return auth.ok ? { model, apiKey: auth.apiKey, headers: auth.headers } : null;
154
+ return auth.ok ? { model } : null;
153
155
  }
154
156
 
155
157
  /**
@@ -166,11 +168,25 @@ function parseExtractionResult(text: string): ExtractionResult | null {
166
168
  jsonStr = jsonMatch[1].trim();
167
169
  }
168
170
 
169
- const parsed = JSON.parse(jsonStr);
170
- if (parsed && Array.isArray(parsed.questions)) {
171
- return parsed as ExtractionResult;
171
+ const parsed: unknown = JSON.parse(jsonStr);
172
+ if (
173
+ !parsed ||
174
+ typeof parsed !== "object" ||
175
+ !("questions" in parsed) ||
176
+ !Array.isArray(parsed.questions) ||
177
+ !parsed.questions.every(
178
+ (question: unknown): question is ExtractedQuestion =>
179
+ question !== null &&
180
+ typeof question === "object" &&
181
+ "question" in question &&
182
+ typeof question.question === "string" &&
183
+ question.question.trim().length > 0 &&
184
+ (!("context" in question) || typeof question.context === "string"),
185
+ )
186
+ ) {
187
+ return null;
172
188
  }
173
- return null;
189
+ return { questions: parsed.questions };
174
190
  } catch {
175
191
  return null;
176
192
  }
@@ -190,6 +206,7 @@ class QnAComponent implements Component, Focusable {
190
206
  private showingConfirmation: boolean = false;
191
207
 
192
208
  // Cache
209
+ private renderWidth?: number;
193
210
  private cachedWidth?: number;
194
211
  private cachedLines?: string[];
195
212
 
@@ -239,7 +256,7 @@ class QnAComponent implements Component, Focusable {
239
256
  }
240
257
 
241
258
  private saveCurrentAnswer(): void {
242
- this.answers[this.currentIndex] = this.editor.getText();
259
+ this.answers[this.currentIndex] = this.editor.getExpandedText();
243
260
  }
244
261
 
245
262
  private navigateTo(index: number): void {
@@ -282,6 +299,11 @@ class QnAComponent implements Component, Focusable {
282
299
  }
283
300
 
284
301
  handleInput(data: string): void {
302
+ if ((this.renderWidth ?? this.tui.terminal.columns) < MIN_QUESTIONNAIRE_WIDTH) {
303
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) this.cancel();
304
+ return;
305
+ }
306
+
285
307
  // Handle confirmation dialog
286
308
  if (this.showingConfirmation) {
287
309
  if (matchesKey(data, Key.enter) || data.toLowerCase() === "y") {
@@ -363,6 +385,19 @@ class QnAComponent implements Component, Focusable {
363
385
  }
364
386
 
365
387
  render(width: number): string[] {
388
+ this.renderWidth = width;
389
+ if (width < MIN_QUESTIONNAIRE_WIDTH) {
390
+ return [
391
+ truncateToWidth(
392
+ this.theme.fg(
393
+ "muted",
394
+ `Resize to ${MIN_QUESTIONNAIRE_WIDTH}+ columns to answer. Esc cancels.`,
395
+ ),
396
+ width,
397
+ ),
398
+ ];
399
+ }
400
+
366
401
  if (this.cachedLines && this.cachedWidth === width) {
367
402
  return this.cachedLines;
368
403
  }
@@ -411,7 +446,9 @@ class QnAComponent implements Component, Focusable {
411
446
  progressParts.push(theme.fg("dim", "○"));
412
447
  }
413
448
  }
414
- lines.push(padToWidth(boxLine(progressParts.join(" "))));
449
+ for (const line of wrapTextWithAnsi(progressParts.join(" "), contentWidth)) {
450
+ lines.push(padToWidth(boxLine(line)));
451
+ }
415
452
  lines.push(padToWidth(emptyBoxLine()));
416
453
 
417
454
  // Current question
@@ -520,65 +557,84 @@ export default function (pi: ExtensionAPI) {
520
557
  }
521
558
 
522
559
  // Run extraction with loader UI
523
- const extractionResult = await ctx.ui.custom<ExtractionResult | null>(
524
- (tui, theme, _kb, done) => {
525
- const loader = new BorderedLoader(
526
- tui,
527
- theme,
528
- `Extracting questions using ${extractionSelection.model.id}...`,
560
+ const extraction = await ctx.ui.custom<ExtractionOutcome>((tui, theme, _kb, done) => {
561
+ const loader = new BorderedLoader(
562
+ tui,
563
+ theme,
564
+ `Extracting questions using ${extractionSelection.model.id}...`,
565
+ );
566
+ loader.onAbort = () => done({ status: "cancelled" });
567
+
568
+ const doExtract = async (): Promise<ExtractionOutcome> => {
569
+ const userMessage: UserMessage = {
570
+ role: "user",
571
+ content: [{ type: "text", text: lastAssistantText! }],
572
+ timestamp: Date.now(),
573
+ };
574
+
575
+ const response = await ctx.modelRegistry.complete(
576
+ extractionSelection.model,
577
+ { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
578
+ { signal: loader.signal },
529
579
  );
530
- loader.onAbort = () => done(null);
531
580
 
532
- const doExtract = async () => {
533
- const userMessage: UserMessage = {
534
- role: "user",
535
- content: [{ type: "text", text: lastAssistantText! }],
536
- timestamp: Date.now(),
581
+ if (loader.signal.aborted || response.stopReason === "aborted") {
582
+ return { status: "cancelled" };
583
+ }
584
+ if (response.stopReason !== "stop") {
585
+ return {
586
+ status: "error",
587
+ message: response.errorMessage
588
+ ? `Question extraction failed: ${response.errorMessage}`
589
+ : `Question extraction did not complete (${response.stopReason})`,
537
590
  };
591
+ }
538
592
 
539
- const response = await complete(
540
- extractionSelection.model,
541
- { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
542
- {
543
- apiKey: extractionSelection.apiKey,
544
- headers: extractionSelection.headers,
545
- signal: loader.signal,
546
- },
593
+ const responseText = response.content
594
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
595
+ .map((c) => c.text)
596
+ .join("\n");
597
+
598
+ const result = parseExtractionResult(responseText);
599
+ return result
600
+ ? { status: "success", result }
601
+ : { status: "error", message: "Question extraction returned an invalid response" };
602
+ };
603
+
604
+ doExtract()
605
+ .then(done)
606
+ .catch((error: unknown) => {
607
+ done(
608
+ loader.signal.aborted
609
+ ? { status: "cancelled" }
610
+ : {
611
+ status: "error",
612
+ message: `Question extraction failed: ${error instanceof Error ? error.message : String(error)}`,
613
+ },
547
614
  );
615
+ });
548
616
 
549
- if (response.stopReason === "aborted") {
550
- return null;
551
- }
552
-
553
- const responseText = response.content
554
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
555
- .map((c) => c.text)
556
- .join("\n");
557
-
558
- return parseExtractionResult(responseText);
559
- };
560
-
561
- doExtract()
562
- .then(done)
563
- .catch(() => done(null));
564
-
565
- return loader;
566
- },
567
- );
617
+ return loader;
618
+ });
568
619
 
569
- if (extractionResult === null) {
620
+ if (extraction.status === "cancelled") {
570
621
  ctx.ui.notify("Cancelled", "info");
571
622
  return;
572
623
  }
624
+ if (extraction.status === "error") {
625
+ ctx.ui.notify(extraction.message, "error");
626
+ return;
627
+ }
573
628
 
574
- if (extractionResult.questions.length === 0) {
629
+ const { questions } = extraction.result;
630
+ if (questions.length === 0) {
575
631
  ctx.ui.notify("No questions found in the last message", "info");
576
632
  return;
577
633
  }
578
634
 
579
635
  // Show the Q&A component
580
636
  const answersResult = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
581
- return new QnAComponent(extractionResult.questions, tui, theme, done);
637
+ return new QnAComponent(questions, tui, theme, done);
582
638
  });
583
639
 
584
640
  if (answersResult === null) {
@@ -0,0 +1,7 @@
1
+ # Branch terminals
2
+
3
+ `/branch` forks the selected conversation without switching the current session. Inside tmux, `--branch-tmux-layout` chooses `window`, `split-right`, or `split-down`; a `/branch` argument overrides that setting.
4
+
5
+ `--branch-term` takes precedence over tmux and accepts a Bash launcher template. Use unquoted `{cwd}` and `{session}` placeholders for path arguments. `{command}` supplies a single command-string argument, suitable for a launcher's command option or `bash -c`. Do not add shell quotes around placeholders; they are already quoted. If neither `{session}` nor `{command}` appears, the fork path is appended. The launcher runs in the session's working directory with the current environment.
6
+
7
+ A failed launcher produces a recovery command. Reloading or closing the original Pi session stops observing launcher failures but does not close user-owned terminals.