tau-coding-agent 0.1.6 → 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 (80) hide show
  1. package/README.md +15 -12
  2. package/extensions/answer.ts +129 -79
  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 +17 -2
  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 +109 -191
  13. package/extensions/loop.ts +149 -145
  14. package/extensions/memory.ts +138 -86
  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 +41 -38
  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/triage.ts +41 -44
  28. package/extensions/sandbox/bash.ts +775 -0
  29. package/extensions/sandbox/command.ts +605 -0
  30. package/extensions/sandbox/config.ts +764 -0
  31. package/extensions/sandbox/index.ts +132 -3338
  32. package/extensions/sandbox/permissions/dialog.ts +118 -0
  33. package/extensions/sandbox/permissions/filesystem.ts +559 -0
  34. package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
  35. package/extensions/sandbox/permissions/network.ts +164 -0
  36. package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
  37. package/extensions/sandbox/runtime.ts +616 -0
  38. package/extensions/stash.ts +28 -15
  39. package/extensions/subagent/README.md +73 -0
  40. package/extensions/subagent/index.ts +822 -0
  41. package/extensions/subagent/interrupt.ts +117 -0
  42. package/extensions/subagent/permissions.ts +101 -0
  43. package/extensions/subagent/rpc.ts +177 -0
  44. package/extensions/tool-display-mode.ts +267 -64
  45. package/extensions/usage/index.ts +249 -356
  46. package/extensions/usage/openrouter.ts +10 -2
  47. package/extensions/websearch/README.md +12 -47
  48. package/extensions/websearch/config.ts +5 -2
  49. package/extensions/websearch/index.ts +94 -109
  50. package/extensions/websearch/output.ts +51 -0
  51. package/extensions/websearch/providers/anthropic.pi.ts +27 -44
  52. package/extensions/websearch/providers/gemini.browser.ts +68 -77
  53. package/extensions/websearch/providers/gemini.pi.ts +17 -17
  54. package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
  55. package/extensions/websearch/providers/pi-model.shared.ts +33 -39
  56. package/extensions/websearch/providers/shared.ts +87 -2
  57. package/extensions/websearch/types.ts +0 -3
  58. package/extensions/worktree.ts +132 -172
  59. package/package.json +10 -5
  60. package/skills/browser-tools/SKILL.md +29 -234
  61. package/skills/browser-tools/references/cookies.md +36 -0
  62. package/skills/browser-tools/references/interaction.md +90 -0
  63. package/skills/browser-tools/references/logging.md +34 -0
  64. package/skills/git-clean-history/SKILL.md +6 -6
  65. package/skills/git-commit/SKILL.md +5 -3
  66. package/skills/github-pull-request/SKILL.md +60 -0
  67. package/skills/github-pull-request/references/create.md +40 -0
  68. package/skills/github-pull-request/references/stewardship.md +60 -0
  69. package/skills/oracle/SKILL.md +3 -3
  70. package/skills/oracle/scripts/oracle +10 -1
  71. package/skills/sentry/SKILL.md +15 -185
  72. package/skills/sentry/references/events.md +79 -0
  73. package/skills/sentry/references/issues.md +62 -0
  74. package/skills/sentry/references/logs.md +46 -0
  75. package/skills/update-changelog/SKILL.md +27 -121
  76. package/skills/web-design/SKILL.md +16 -105
  77. package/themes/tau-dark.json +4 -0
  78. package/extensions/openai-fast.ts +0 -229
  79. package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
  80. 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,25 +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
- env?: Record<string, string>;
99
107
  }
100
108
 
101
- type ModelAuthLookup = {
102
- find: (provider: string, modelId: string) => Model<Api> | undefined;
103
- getApiKeyAndHeaders: (model: Model<Api>) => Promise<
104
- | {
105
- ok: true;
106
- apiKey?: string;
107
- headers?: Record<string, string>;
108
- env?: Record<string, string>;
109
- }
110
- | { ok: false; error: string }
111
- >;
112
- };
109
+ type ModelLookup = Pick<ModelRegistry, "find" | "getApiKeyAndHeaders">;
113
110
 
114
111
  type ModelFamily = "openai" | "anthropic";
115
112
 
@@ -125,10 +122,10 @@ function detectModelFamily(provider: string): ModelFamily | null {
125
122
  */
126
123
  async function selectExtractionModel(
127
124
  currentModel: Model<Api>,
128
- modelRegistry: ModelAuthLookup,
129
- ): Promise<ModelRequestAuth | null> {
125
+ modelRegistry: ModelLookup,
126
+ ): Promise<ModelSelection | null> {
130
127
  const family = detectModelFamily(currentModel.provider);
131
- if (!family) return resolveModelRequestAuth(modelRegistry, currentModel);
128
+ if (!family) return resolveModelSelection(modelRegistry, currentModel);
132
129
 
133
130
  const modelId = family === "openai" ? OPENAI_FAST_MODEL_ID : ANTHROPIC_FAST_MODEL_ID;
134
131
  const providerCandidates =
@@ -140,21 +137,21 @@ async function selectExtractionModel(
140
137
  const model = modelRegistry.find(provider, modelId);
141
138
  if (!model) continue;
142
139
 
143
- const auth = await resolveModelRequestAuth(modelRegistry, model);
144
- if (auth) {
145
- return auth;
140
+ const selection = await resolveModelSelection(modelRegistry, model);
141
+ if (selection) {
142
+ return selection;
146
143
  }
147
144
  }
148
145
 
149
- return resolveModelRequestAuth(modelRegistry, currentModel);
146
+ return resolveModelSelection(modelRegistry, currentModel);
150
147
  }
151
148
 
152
- async function resolveModelRequestAuth(
153
- modelRegistry: ModelAuthLookup,
149
+ async function resolveModelSelection(
150
+ modelRegistry: ModelLookup,
154
151
  model: Model<Api>,
155
- ): Promise<ModelRequestAuth | null> {
152
+ ): Promise<ModelSelection | null> {
156
153
  const auth = await modelRegistry.getApiKeyAndHeaders(model);
157
- return auth.ok ? { model, apiKey: auth.apiKey, headers: auth.headers, env: auth.env } : null;
154
+ return auth.ok ? { model } : null;
158
155
  }
159
156
 
160
157
  /**
@@ -171,11 +168,25 @@ function parseExtractionResult(text: string): ExtractionResult | null {
171
168
  jsonStr = jsonMatch[1].trim();
172
169
  }
173
170
 
174
- const parsed = JSON.parse(jsonStr);
175
- if (parsed && Array.isArray(parsed.questions)) {
176
- 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;
177
188
  }
178
- return null;
189
+ return { questions: parsed.questions };
179
190
  } catch {
180
191
  return null;
181
192
  }
@@ -195,6 +206,7 @@ class QnAComponent implements Component, Focusable {
195
206
  private showingConfirmation: boolean = false;
196
207
 
197
208
  // Cache
209
+ private renderWidth?: number;
198
210
  private cachedWidth?: number;
199
211
  private cachedLines?: string[];
200
212
 
@@ -244,7 +256,7 @@ class QnAComponent implements Component, Focusable {
244
256
  }
245
257
 
246
258
  private saveCurrentAnswer(): void {
247
- this.answers[this.currentIndex] = this.editor.getText();
259
+ this.answers[this.currentIndex] = this.editor.getExpandedText();
248
260
  }
249
261
 
250
262
  private navigateTo(index: number): void {
@@ -287,6 +299,11 @@ class QnAComponent implements Component, Focusable {
287
299
  }
288
300
 
289
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
+
290
307
  // Handle confirmation dialog
291
308
  if (this.showingConfirmation) {
292
309
  if (matchesKey(data, Key.enter) || data.toLowerCase() === "y") {
@@ -368,6 +385,19 @@ class QnAComponent implements Component, Focusable {
368
385
  }
369
386
 
370
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
+
371
401
  if (this.cachedLines && this.cachedWidth === width) {
372
402
  return this.cachedLines;
373
403
  }
@@ -416,7 +446,9 @@ class QnAComponent implements Component, Focusable {
416
446
  progressParts.push(theme.fg("dim", "○"));
417
447
  }
418
448
  }
419
- lines.push(padToWidth(boxLine(progressParts.join(" "))));
449
+ for (const line of wrapTextWithAnsi(progressParts.join(" "), contentWidth)) {
450
+ lines.push(padToWidth(boxLine(line)));
451
+ }
420
452
  lines.push(padToWidth(emptyBoxLine()));
421
453
 
422
454
  // Current question
@@ -525,66 +557,84 @@ export default function (pi: ExtensionAPI) {
525
557
  }
526
558
 
527
559
  // Run extraction with loader UI
528
- const extractionResult = await ctx.ui.custom<ExtractionResult | null>(
529
- (tui, theme, _kb, done) => {
530
- const loader = new BorderedLoader(
531
- tui,
532
- theme,
533
- `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 },
534
579
  );
535
- loader.onAbort = () => done(null);
536
580
 
537
- const doExtract = async () => {
538
- const userMessage: UserMessage = {
539
- role: "user",
540
- content: [{ type: "text", text: lastAssistantText! }],
541
- 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})`,
542
590
  };
591
+ }
543
592
 
544
- const response = await complete(
545
- extractionSelection.model,
546
- { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
547
- {
548
- apiKey: extractionSelection.apiKey,
549
- headers: extractionSelection.headers,
550
- env: extractionSelection.env,
551
- signal: loader.signal,
552
- },
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
+ },
553
614
  );
615
+ });
554
616
 
555
- if (response.stopReason === "aborted") {
556
- return null;
557
- }
558
-
559
- const responseText = response.content
560
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
561
- .map((c) => c.text)
562
- .join("\n");
563
-
564
- return parseExtractionResult(responseText);
565
- };
566
-
567
- doExtract()
568
- .then(done)
569
- .catch(() => done(null));
570
-
571
- return loader;
572
- },
573
- );
617
+ return loader;
618
+ });
574
619
 
575
- if (extractionResult === null) {
620
+ if (extraction.status === "cancelled") {
576
621
  ctx.ui.notify("Cancelled", "info");
577
622
  return;
578
623
  }
624
+ if (extraction.status === "error") {
625
+ ctx.ui.notify(extraction.message, "error");
626
+ return;
627
+ }
579
628
 
580
- if (extractionResult.questions.length === 0) {
629
+ const { questions } = extraction.result;
630
+ if (questions.length === 0) {
581
631
  ctx.ui.notify("No questions found in the last message", "info");
582
632
  return;
583
633
  }
584
634
 
585
635
  // Show the Q&A component
586
636
  const answersResult = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
587
- return new QnAComponent(extractionResult.questions, tui, theme, done);
637
+ return new QnAComponent(questions, tui, theme, done);
588
638
  });
589
639
 
590
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.