@shanepadgett/tau-agent 0.34.0 → 0.35.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.
@@ -1,147 +1,94 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
3
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import { createGitRunner, loadRepoStatus } from "../../shared/git.ts";
5
- import { createInjectedContext } from "../../shared/injected-context.ts";
5
+ import { resolveEffortProviders } from "../../shared/model-effort.ts";
6
6
  import { errorText } from "../../shared/text.ts";
7
- import {
8
- formatReviewMarkdown,
9
- isReviewRecord,
10
- isReviewMode,
11
- REVIEW_ENTRY_TYPE,
12
- type ReviewMode,
13
- type ReviewRecord,
14
- } from "./model.ts";
15
- import { ReviewProgressPanel, ReviewResultPanel, type ReviewResultAction } from "./panel.ts";
7
+ import { formatReviewMarkdown, type ReviewMode } from "./model.ts";
16
8
  import { runReview } from "./session.ts";
17
9
 
10
+ const REVIEW_TYPES = [
11
+ { label: "Simplify", mode: "simplify" },
12
+ { label: "Architecture", mode: "architecture" },
13
+ { label: "Correctness", mode: "correctness" },
14
+ ] as const satisfies ReadonlyArray<{ label: string; mode: ReviewMode }>;
15
+
18
16
  export default function reviewExtension(pi: ExtensionAPI): void {
19
17
  pi.registerCommand("review", {
20
- description: "Run or show an isolated simplify, architecture, or correctness review",
18
+ description: "Write an isolated requested or Git-change review to Markdown",
21
19
  handler: async (args, ctx) => {
22
20
  if (ctx.mode !== "tui" || !ctx.isProjectTrusted()) {
23
21
  ctx.ui.notify("/review requires a trusted TUI project", "warning");
24
22
  return;
25
23
  }
26
24
  await ctx.waitForIdle();
27
- const requested = args.trim().toLowerCase();
28
- if (requested === "show") {
29
- await showLatestReview(pi, ctx);
30
- return;
31
- }
32
- const mode = await resolveReviewMode(requested, ctx);
33
- if (!mode) return;
25
+ const direction = args.trim();
34
26
  const status = await loadRepoStatus(createGitRunner(pi, ctx));
35
27
  if (!status) {
36
28
  ctx.ui.notify("/review requires a Git repository", "warning");
37
29
  return;
38
30
  }
39
- if (status.fileCount === 0) {
31
+ if (!direction && status.fileCount === 0) {
40
32
  ctx.ui.notify("Nothing to review: working tree is clean", "info");
41
33
  return;
42
34
  }
43
- const output = await runReviewWithPanel(pi, ctx, mode, status.root);
44
- if (!output) return;
45
- pi.appendEntry(REVIEW_ENTRY_TYPE, output);
46
- await showReview(pi, ctx, output);
35
+ const selectedType = await ctx.ui.select(
36
+ "Review type",
37
+ REVIEW_TYPES.map(({ label }) => label),
38
+ );
39
+ if (!selectedType) return;
40
+ const mode = REVIEW_TYPES.find(({ label }) => label === selectedType)?.mode;
41
+ if (!mode) return;
42
+ const providers = resolveEffortProviders(ctx, "deep");
43
+ let preferred: ReviewModelChoice | undefined;
44
+ if (providers.length > 0) {
45
+ const labels = providers.map((provider) => `${provider.label} · ${provider.candidates[0]?.model.id ?? ""}`);
46
+ const selected = await ctx.ui.select("Review provider", labels);
47
+ if (!selected) return;
48
+ const provider = providers[labels.indexOf(selected)];
49
+ const candidate = provider?.candidates[0];
50
+ if (provider && candidate) {
51
+ preferred = { model: `${provider.provider}/${candidate.model.id}`, thinkingLevel: candidate.reasoning };
52
+ }
53
+ } else {
54
+ ctx.ui.notify("No logged-in review provider. Using current model.", "warning");
55
+ }
56
+ const signal = ctx.signal ?? new AbortController().signal;
57
+ ctx.ui.setStatus("review", `running ${mode} review`);
58
+ try {
59
+ const output = await runReview({
60
+ ctx,
61
+ root: status.root,
62
+ mode,
63
+ direction,
64
+ preferredModel: preferred?.model,
65
+ preferredThinkingLevel: preferred?.thinkingLevel,
66
+ parentThinkingLevel: pi.getThinkingLevel(),
67
+ signal,
68
+ });
69
+ const createdAt = new Date().toISOString();
70
+ const directory = join(status.root, ".pi", "tau", "reviews");
71
+ const timestamp = createdAt.replace(/[^0-9A-Za-z-]/g, "-");
72
+ const path = join(directory, `${timestamp}-${mode}-review.md`);
73
+ await mkdir(directory, { recursive: true });
74
+ await writeFile(path, formatReviewMarkdown({ ...output, mode, direction, createdAt }), {
75
+ encoding: "utf8",
76
+ mode: 0o600,
77
+ });
78
+ ctx.ui.notify(`Review written to ${path}`, "info");
79
+ } catch (error) {
80
+ ctx.ui.notify(
81
+ signal.aborted ? "Review cancelled" : `Review failed: ${errorText(error)}`,
82
+ signal.aborted ? "info" : "error",
83
+ );
84
+ } finally {
85
+ ctx.ui.setStatus("review", undefined);
86
+ }
47
87
  },
48
88
  });
49
89
  }
50
90
 
51
- function latestReview(entries: readonly SessionEntry[]): ReviewRecord | undefined {
52
- for (let index = entries.length - 1; index >= 0; index -= 1) {
53
- const entry = entries[index];
54
- if (entry?.type !== "custom" || entry.customType !== REVIEW_ENTRY_TYPE) continue;
55
- if (isReviewRecord(entry.data)) return entry.data;
56
- }
57
- return undefined;
58
- }
59
-
60
- async function showLatestReview(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
61
- const latest = latestReview(ctx.sessionManager.getBranch());
62
- if (!latest) {
63
- ctx.ui.notify("No review exists on this session branch", "warning");
64
- return;
65
- }
66
- await showReview(pi, ctx, latest);
67
- }
68
-
69
- async function resolveReviewMode(requested: string, ctx: ExtensionContext): Promise<ReviewMode | undefined> {
70
- if (requested) {
71
- if (!isReviewMode(requested)) {
72
- ctx.ui.notify("Usage: /review [simplify|architecture|correctness|show]", "warning");
73
- return undefined;
74
- }
75
- return requested;
76
- }
77
- const selected = await ctx.ui.select("Review mode", ["Simplify", "Architecture", "Correctness"]);
78
- if (!selected) return undefined;
79
- const normalized = selected.toLowerCase();
80
- return isReviewMode(normalized) ? normalized : undefined;
81
- }
82
-
83
- async function runReviewWithPanel(
84
- pi: ExtensionAPI,
85
- ctx: ExtensionContext,
86
- mode: ReviewMode,
87
- root: string,
88
- ): Promise<ReviewRecord | undefined> {
89
- const controller = new AbortController();
90
- const signal = ctx.signal ? AbortSignal.any([controller.signal, ctx.signal]) : controller.signal;
91
- let failure: string | undefined;
92
- const output = await ctx.ui.custom<ReviewRecord | undefined>((tui, theme, keys, done) => {
93
- const panel = new ReviewProgressPanel(tui, theme, mode, keys, () => controller.abort());
94
- void runReview({
95
- ctx,
96
- root,
97
- mode,
98
- parentThinkingLevel: pi.getThinkingLevel(),
99
- signal,
100
- onProgress: (line) => panel.update(line),
101
- })
102
- .then((result) => done({ ...result, mode, root, createdAt: new Date().toISOString() }))
103
- .catch((error: unknown) => {
104
- failure = errorText(error);
105
- done(undefined);
106
- });
107
- return panel;
108
- });
109
- if (output) return output;
110
- ctx.ui.notify(
111
- signal.aborted ? "Review cancelled" : `Review failed: ${failure ?? "unknown error"}`,
112
- signal.aborted ? "info" : "error",
113
- );
114
- return undefined;
115
- }
116
-
117
- async function showReview(pi: ExtensionAPI, ctx: ExtensionContext, review: ReviewRecord): Promise<void> {
118
- const action = await ctx.ui.custom<ReviewResultAction>(
119
- (tui, theme, keys, done) => new ReviewResultPanel(tui, theme, keys, review, done),
120
- {
121
- overlay: true,
122
- overlayOptions: { anchor: "top-center", width: "80%", minWidth: 68, maxHeight: "90%", margin: 1 },
123
- },
124
- );
125
- if (action === "send") {
126
- try {
127
- await pi.sendMessage(
128
- createInjectedContext(formatReviewMarkdown(review), { source: "review", title: `${review.mode} review` }),
129
- { triggerTurn: false, deliverAs: "nextTurn" },
130
- );
131
- ctx.ui.notify("Review queued for agent's next turn", "info");
132
- } catch (error) {
133
- ctx.ui.notify(`Failed to send review: ${errorText(error)}`, "error");
134
- }
135
- } else if (action === "export") {
136
- try {
137
- const directory = join(review.root, ".pi", "tau", "reviews");
138
- await mkdir(directory, { recursive: true });
139
- const timestamp = review.createdAt.replace(/[^0-9A-Za-z-]/g, "-");
140
- const path = join(directory, `${timestamp}-${review.mode}.md`);
141
- await writeFile(path, formatReviewMarkdown(review), { encoding: "utf8", mode: 0o600 });
142
- ctx.ui.notify(`Review exported to ${path}`, "info");
143
- } catch (error) {
144
- ctx.ui.notify(`Failed to export review: ${errorText(error)}`, "error");
145
- }
146
- }
91
+ interface ReviewModelChoice {
92
+ model: string;
93
+ thinkingLevel: NonNullable<ExtensionContext["thinkingLevel"]>;
147
94
  }
@@ -1,17 +1,11 @@
1
1
  import type { Tool } from "@earendil-works/pi-ai";
2
2
  import { Type, type Static } from "typebox";
3
- import { Value } from "typebox/value";
4
3
 
5
4
  const MAX_SUMMARY_LENGTH = 2_000;
6
5
  const MAX_FINDINGS = 12;
7
6
  const MAX_PATH_LENGTH = 400;
8
7
  const MAX_LINES_LENGTH = 80;
9
8
  const MAX_FINDING_LENGTH = 1_500;
10
- const REVIEW_MODE_SCHEMA = Type.Union([
11
- Type.Literal("simplify"),
12
- Type.Literal("architecture"),
13
- Type.Literal("correctness"),
14
- ]);
15
9
  const REVIEW_FINDING_SCHEMA = Type.Object(
16
10
  {
17
11
  severity: Type.Union([
@@ -55,21 +49,10 @@ const REVIEW_OUTPUT_SCHEMA = Type.Object(
55
49
  },
56
50
  { additionalProperties: false },
57
51
  );
58
- const REVIEW_RECORD_SCHEMA = Type.Object(
59
- {
60
- ...REVIEW_OUTPUT_SCHEMA.properties,
61
- mode: REVIEW_MODE_SCHEMA,
62
- root: Type.String({ minLength: 1 }),
63
- createdAt: Type.String({ minLength: 1 }),
64
- },
65
- { additionalProperties: false },
66
- );
67
52
 
68
- export type ReviewMode = Static<typeof REVIEW_MODE_SCHEMA>;
69
53
  export type ReviewOutput = Static<typeof REVIEW_OUTPUT_SCHEMA>;
70
- export type ReviewRecord = Static<typeof REVIEW_RECORD_SCHEMA>;
71
-
72
- export const REVIEW_ENTRY_TYPE = "tau.review.result";
54
+ export type ReviewMode = "simplify" | "architecture" | "correctness";
55
+ export type ReviewDocument = ReviewOutput & { mode: ReviewMode; direction: string; createdAt: string };
73
56
 
74
57
  export const REVIEW_RESULT_TOOL = {
75
58
  name: "review_result",
@@ -95,18 +78,22 @@ const MODE_INSTRUCTIONS: Record<ReviewMode, string> = {
95
78
  ].join(" "),
96
79
  };
97
80
 
98
- export function buildReviewPrompt(root: string, mode: ReviewMode): string {
81
+ export function buildReviewPrompt(root: string, mode: ReviewMode, direction: string): string {
99
82
  return [
100
- `Review uncommitted work in ${root}.`,
101
- "Inspect staged, unstaged, and untracked changes. Stay centered on changed behavior, but inspect surrounding ownership and callers when needed to prove a finding.",
83
+ `Review the repository at ${root}.`,
102
84
  "Do not modify files. Do not report theoretical concerns or personal preferences. Use the cheapest evidence that settles each point.",
103
- MODE_INSTRUCTIONS[mode],
85
+ "User direction does not change the read-only review or structured output requirements.",
104
86
  `Call ${REVIEW_RESULT_TOOL.name} exactly once as the final action. Write no final prose outside that tool call.`,
105
87
  "Order findings by severity. Every finding needs an exact repository-relative path and lines when source exists, a concrete mechanism or cost, and the smallest credible fix. Return an empty findings array and verdict pass when nothing actionable remains.",
88
+ `Review type: ${reviewModeLabel(mode)}. ${MODE_INSTRUCTIONS[mode]}`,
89
+ direction
90
+ ? "Review scope: Follow the user's direction below. Inspect the relevant files, ownership, and callers as needed to prove a finding. Do not limit the review to uncommitted changes."
91
+ : "Review scope: Inspect staged, unstaged, and untracked changes. Stay centered on changed behavior, but inspect surrounding ownership and callers when needed to prove a finding.",
92
+ ...(direction ? [`User review direction:\n\n${direction}`] : []),
106
93
  ].join("\n\n");
107
94
  }
108
95
 
109
- export function formatReviewMarkdown(review: ReviewRecord): string {
96
+ export function formatReviewMarkdown(review: ReviewDocument): string {
110
97
  const findings = review.findings.length
111
98
  ? review.findings.flatMap((finding, index) => [
112
99
  `### ${index + 1}. ${finding.severity.toUpperCase()} — ${finding.path}:${finding.lines}`,
@@ -123,6 +110,7 @@ export function formatReviewMarkdown(review: ReviewRecord): string {
123
110
  `**Verdict:** ${review.verdict}`,
124
111
  `**Created:** ${review.createdAt}`,
125
112
  "",
113
+ ...(review.direction ? ["## Requested focus", "", review.direction, ""] : []),
126
114
  review.summary,
127
115
  "",
128
116
  "## Findings",
@@ -131,14 +119,6 @@ export function formatReviewMarkdown(review: ReviewRecord): string {
131
119
  ].join("\n");
132
120
  }
133
121
 
134
- export function isReviewMode(value: string): value is ReviewMode {
135
- return Value.Check(REVIEW_MODE_SCHEMA, value);
136
- }
137
-
138
- export function reviewModeLabel(mode: ReviewMode): string {
122
+ function reviewModeLabel(mode: ReviewMode): string {
139
123
  return `${mode.charAt(0).toUpperCase()}${mode.slice(1)}`;
140
124
  }
141
-
142
- export function isReviewRecord(value: unknown): value is ReviewRecord {
143
- return Value.Check(REVIEW_RECORD_SCHEMA, value);
144
- }
@@ -1,6 +1,6 @@
1
1
  import { dirname, join } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
- import { defineTool, type AgentSessionEvent, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { defineTool, type ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import {
5
5
  createIsolatedSessionResource,
6
6
  resolveIsolatedSessionModel,
@@ -8,7 +8,6 @@ import {
8
8
  } from "../../shared/isolated-session.ts";
9
9
  import { buildReviewPrompt, REVIEW_RESULT_TOOL, type ReviewMode, type ReviewOutput } from "./model.ts";
10
10
 
11
- const REVIEW_MODEL = "openai-codex/gpt-5.6-sol";
12
11
  const REVIEW_TOOLS = [
13
12
  "read",
14
13
  "bash",
@@ -32,11 +31,13 @@ export async function runReview(options: {
32
31
  ctx: ExtensionContext;
33
32
  root: string;
34
33
  mode: ReviewMode;
34
+ direction: string;
35
+ preferredModel: string | undefined;
36
+ preferredThinkingLevel: NonNullable<ExtensionContext["thinkingLevel"]> | undefined;
35
37
  parentThinkingLevel: NonNullable<ExtensionContext["thinkingLevel"]>;
36
38
  signal: AbortSignal;
37
- onProgress: (status: string) => void;
38
39
  }): Promise<ReviewOutput> {
39
- const { ctx, root, mode, signal, onProgress } = options;
40
+ const { ctx, root, mode, direction, signal } = options;
40
41
  let output: ReviewOutput | undefined;
41
42
  const outputTool = defineTool({
42
43
  ...REVIEW_RESULT_TOOL,
@@ -51,17 +52,16 @@ export async function runReview(options: {
51
52
  },
52
53
  });
53
54
  let resource: IsolatedSessionResource | undefined;
54
- let unsubscribe: (() => void) | undefined;
55
55
  try {
56
56
  const selected = await resolveIsolatedSessionModel({
57
57
  label: "Review",
58
- preferredModel: REVIEW_MODEL,
59
- preferredThinkingLevel: "high",
58
+ preferredModel: options.preferredModel,
59
+ preferredThinkingLevel: options.preferredThinkingLevel,
60
60
  usePreferredThinkingAfterModelFallback: false,
61
61
  ctx,
62
62
  parentThinkingLevel: options.parentThinkingLevel,
63
63
  signal,
64
- onWarning: onProgress,
64
+ onWarning: (warning) => ctx.ui.notify(warning, "warning"),
65
65
  });
66
66
  resource = await createIsolatedSessionResource(
67
67
  {
@@ -76,18 +76,10 @@ export async function runReview(options: {
76
76
  signal,
77
77
  );
78
78
  const { session } = resource;
79
- unsubscribe = session.subscribe((event: AgentSessionEvent) => {
80
- if (event.type === "tool_execution_start") {
81
- onProgress(`${event.toolName} ${summarizeArgs(event.args)}`.trim());
82
- } else if (event.type === "tool_execution_end") {
83
- onProgress(event.isError ? `${event.toolName} failed` : `${event.toolName} complete`);
84
- }
85
- });
86
79
  const abort = () => void session.abort().catch(() => undefined);
87
80
  signal.addEventListener("abort", abort, { once: true });
88
81
  try {
89
- onProgress(`Running ${mode} review`);
90
- await session.prompt(buildReviewPrompt(root, mode), { expandPromptTemplates: false });
82
+ await session.prompt(buildReviewPrompt(root, mode, direction), { expandPromptTemplates: false });
91
83
  } finally {
92
84
  signal.removeEventListener("abort", abort);
93
85
  }
@@ -95,12 +87,6 @@ export async function runReview(options: {
95
87
  if (!output) throw new Error("Review ended without structured output");
96
88
  return output;
97
89
  } finally {
98
- unsubscribe?.();
99
90
  await resource?.dispose();
100
91
  }
101
92
  }
102
-
103
- function summarizeArgs(args: unknown): string {
104
- const text = typeof args === "string" ? args : (JSON.stringify(args) ?? String(args));
105
- return text.length > 140 ? `${text.slice(0, 139)}…` : text;
106
- }
@@ -3,7 +3,7 @@
3
3
  Soul shapes how Tau works and talks by adding two independent sections to Pi's native assistant prompt:
4
4
 
5
5
  - **ponytail** — a lazy-senior-dev build ethos: do the smallest correct thing, reuse before writing, YAGNI, fix bugs at the root, never cut validation, security, or accessibility.
6
- - **caveman** — a terse communication style: drop filler, keep technical substance exact, quote the shortest decisive line, spell out anything where brevity would risk safety.
6
+ - **simplified** — Simplified Technical English (ASD-STE100): use short sentences and paragraphs, explain jargon, and shape plans and conversations in small chunks.
7
7
 
8
8
  Pi continues to own tool guidance, project instructions, skills, documentation paths, custom prompts, and working-directory context.
9
9
 
@@ -12,7 +12,7 @@ Toggle each section in Tau settings (both on by default):
12
12
  ```json
13
13
  {
14
14
  "extensions": {
15
- "soul": { "ponytail": true, "caveman": false }
15
+ "soul": { "ponytail": true, "simplified": false }
16
16
  }
17
17
  }
18
18
  ```
@@ -1,22 +1,22 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
3
- import { CAVEMAN_STYLE, PONYTAIL_ETHOS } from "./prompt.ts";
3
+ import { PONYTAIL_ETHOS, SIMPLIFIED_TECHNICAL_ENGLISH } from "./prompt.ts";
4
4
  import soulSettings from "./settings.ts";
5
5
 
6
6
  export default function soulExtension(pi: ExtensionAPI): void {
7
7
  let ponytail = true;
8
- let caveman = true;
8
+ let simplified = true;
9
9
 
10
10
  pi.on("session_start", async (_event, ctx) => {
11
11
  const settings = await loadTauExtensionSettings(ctx, soulSettings);
12
12
  ponytail = settings.ponytail;
13
- caveman = settings.caveman;
13
+ simplified = settings.simplified;
14
14
  });
15
15
 
16
16
  pi.on("before_agent_start", (event) => {
17
17
  const sections: string[] = [];
18
18
  if (ponytail) sections.push(PONYTAIL_ETHOS);
19
- if (caveman) sections.push(CAVEMAN_STYLE);
19
+ if (simplified) sections.push(SIMPLIFIED_TECHNICAL_ENGLISH);
20
20
  if (sections.length === 0) return undefined;
21
21
  return { systemPrompt: [event.systemPrompt, ...sections].join("\n\n") };
22
22
  });
@@ -26,18 +26,14 @@ Non-trivial logic leaves one runnable check behind: the smallest thing that fail
26
26
 
27
27
  Mark a deliberate simplification that cuts a real corner with a named ceiling and its upgrade path.`;
28
28
 
29
- export const CAVEMAN_STYLE = `## Communication style
29
+ export const SIMPLIFIED_TECHNICAL_ENGLISH = `## Communication style
30
30
 
31
- Primary directive: simple, terse, human-like conversation. Talk to the human the way a person does, not the way a document reads. A wall of text does not get read. Outside a plan or a file you were asked to write, keep replies short. All technical substance stays. Only fluff dies.
31
+ Use Simplified Technical English (ASD-STE100) when you communicate with the user. Assume the user is tired and has limited capacity for jargon.
32
32
 
33
- Drop articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), and hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). No emoji, no decorative tables, no tool-call narration. Do not dump long raw logs. Quote the shortest decisive line.
33
+ Use short sentences and short paragraphs. Explain one idea at a time. Prefer common words, active voice, and concrete explanations. Avoid idioms, vague language, filler, and unexplained abbreviations. Explain unavoidable jargon in plain words when you first use it.
34
34
 
35
- Standard well-known acronyms OK (DB, API, HTTP). Never invent abbreviations (cfg, impl, req, fn). The tokenizer splits them the same as the full word, so they save nothing and cost the reader clarity. No causal arrows. Technical terms, code blocks, and error strings stay exact and verbatim.
35
+ Keep technical content exact. Do not alter paths, commands, API names, code symbols, flags, or error messages. Explain what they mean around the exact text.
36
36
 
37
- Preserve the user's language. Compress the style, not the language.
37
+ Work in small chunks. Answer the immediate question first. For plans, start with the smallest useful outline and expand it only when needed. Expect plans to change after each decision. Do not write a novel before the plan has been checked.
38
38
 
39
- No self-reference. Never name or announce the style.
40
-
41
- No filler emphasis or manufactured insight. Skip "here's the thing", "that's the part that really matters", "and that's the key". Skip "not just X, it's Y" and "it's not X, it's Y" framing. Skip grand closers that restate the point as a lesson. State the fact, the mechanism, or the next step, then stop.
42
-
43
- Full clear sentences, terse style dropped, for security warnings, irreversible-action confirmations, multi-step sequences where order matters, and anywhere compression would create ambiguity. Resume terse after.`;
39
+ Keep responses short enough to scan, while giving enough explanation for the user to understand the reason and next step. Do not replace explanations with fragments just to be brief. Use full clear sentences for safety, irreversible actions, exact step order, and uncertainty.`;
@@ -3,14 +3,17 @@ import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
3
3
 
4
4
  export default defineTauExtensionSettings({
5
5
  key: "soul",
6
- defaults: { ponytail: true as boolean, caveman: true as boolean },
6
+ defaults: { ponytail: true as boolean, simplified: true as boolean },
7
7
  schema: Type.Object(
8
8
  {
9
9
  ponytail: Type.Optional(
10
10
  Type.Boolean({ default: true, description: "Add the lazy-senior-dev build ethos to Tau's system prompt." }),
11
11
  ),
12
- caveman: Type.Optional(
13
- Type.Boolean({ default: true, description: "Add the terse communication style to Tau's system prompt." }),
12
+ simplified: Type.Optional(
13
+ Type.Boolean({
14
+ default: true,
15
+ description: "Add Simplified Technical English and small-chunk explanations to Tau's system prompt.",
16
+ }),
14
17
  ),
15
18
  },
16
19
  { additionalProperties: false },
@@ -11,8 +11,8 @@ names:
11
11
  - Crawler
12
12
  - Netscout
13
13
  - Wayfinder
14
- model: openai-codex/gpt-5.6-sol
15
- thinking: medium
14
+ model: openai-codex/gpt-5.6-luna
15
+ thinking: xhigh
16
16
  ---
17
17
 
18
18
  Stay inside delegated task. Answer exactly what was asked. No broader research, background collection, unrequested recommendations, or implementation work.
@@ -306,8 +306,12 @@ function subscribeTurnEvents(options: {
306
306
  const { session, details, usage, toolUsage, turnMessages, assistantMessageEndAt, publish } = options;
307
307
  const actionById = new Map<string, string>();
308
308
  return session.subscribe((event: AgentSessionEvent) => {
309
- if (event.type === "message_update" && event.message.role === "assistant") {
310
- details.response = cappedTail(textOf(event.message), PREVIEW_LIMIT);
309
+ if (event.type === "message_start" && event.message.role === "assistant") {
310
+ details.response = "";
311
+ return;
312
+ }
313
+ if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
314
+ details.response = cappedTail(details.response + event.assistantMessageEvent.delta, PREVIEW_LIMIT);
311
315
  publish();
312
316
  return;
313
317
  }
@@ -22,6 +22,10 @@ Names sessions from their first request so saved sessions remain findable.
22
22
 
23
23
  Adds `/branch` to create and switch Git branches from the TUI.
24
24
 
25
+ ## bash-approval
26
+
27
+ Reviews every agent `bash` call with a quick-effort model before execution. Set `extensions.bashApproval.autoApprove` to run every reviewer-approved command without another confirmation. The reviewer approves routine local development work. Concrete destructive, system, production, privileged, or security-sensitive effects require human approval with one explanatory paragraph. Reviewer failures fall back to human approval and send an attention notification.
28
+
25
29
  ## cache-diagnostics
26
30
 
27
31
  Records private prompt-cache fingerprints without storing prompt content. Run `/cache-debug` after suspicious cache misses to write a bounded investigation report under `~/.pi/agent/cache-diagnostics/reports/`.
@@ -84,7 +88,7 @@ Adds `/ready` to scan agent-readiness rails (cold start, toolchain, verify, lint
84
88
 
85
89
  ## review
86
90
 
87
- Adds `/review` for explicit isolated review of current Git changes. Choose `simplify`, `architecture`, or `correctness`, or run a mode directly. Results stay outside agent context until you send them from result view, and can be exported under `.pi/tau/reviews/`. `/review show` reopens latest result on current session branch.
91
+ Adds `/review [direction]` for an isolated simplify, architecture, or correctness review. With no direction, it reviews current Git changes. Free-form direction reviews the requested part of the repository even when the working tree is clean. Choose a review type and logged-in provider, then Tau writes the result as Markdown under `.pi/tau/reviews/` without adding it to the parent agent context.
88
92
 
89
93
  ## reference
90
94
 
@@ -108,7 +112,7 @@ Runs configured commands while keeping their output out of agent context when th
108
112
 
109
113
  ## soul
110
114
 
111
- Adds two independently toggleable sections to Pi’s native assistant prompt: `ponytail` (a lazy-senior-dev build ethos) and `caveman` (a terse communication style). Both on by default.
115
+ Adds two independently toggleable sections to Pi’s native assistant prompt: `ponytail` (a lazy-senior-dev build ethos) and `simplified` (Simplified Technical English with short, explanatory responses and small planning chunks). Both on by default.
112
116
 
113
117
  ## stash
114
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-agent",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "Tau is a custom agentic harness built with pi extensions",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -35,7 +35,7 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@ast-grep/wasm": "0.45.0",
38
- "@shanepadgett/tau-tui": "0.34.0",
38
+ "@shanepadgett/tau-tui": "0.35.0",
39
39
  "@vscode/tree-sitter-wasm": "0.3.1",
40
40
  "image-size": "2.0.2",
41
41
  "smol-toml": "1.7.1",
@@ -12,6 +12,22 @@
12
12
  "extensions": {
13
13
  "type": "object",
14
14
  "properties": {
15
+ "bashApproval": {
16
+ "type": "object",
17
+ "properties": {
18
+ "enabled": {
19
+ "type": "boolean",
20
+ "default": true,
21
+ "description": "Enable bash command review and approval."
22
+ },
23
+ "autoApprove": {
24
+ "type": "boolean",
25
+ "default": true,
26
+ "description": "Run reviewer-approved commands without human confirmation."
27
+ }
28
+ },
29
+ "additionalProperties": false
30
+ },
15
31
  "checkpoint": {
16
32
  "type": "object",
17
33
  "required": [
@@ -260,10 +276,10 @@
260
276
  "default": true,
261
277
  "description": "Add the lazy-senior-dev build ethos to Tau's system prompt."
262
278
  },
263
- "caveman": {
279
+ "simplified": {
264
280
  "type": "boolean",
265
281
  "default": true,
266
- "description": "Add the terse communication style to Tau's system prompt."
282
+ "description": "Add Simplified Technical English and small-chunk explanations to Tau's system prompt."
267
283
  }
268
284
  },
269
285
  "additionalProperties": false
@@ -142,7 +142,7 @@ export async function createIsolatedSessionResource(
142
142
  const modelRuntime = await ModelRuntime.create();
143
143
  modelRuntime.registerNativeProvider(inputs.provider);
144
144
  if (inputs.runtimeApiKey !== undefined)
145
- await modelRuntime.setRuntimeApiKey(inputs.model.provider, inputs.runtimeApiKey, { allowNetwork: false });
145
+ await modelRuntime.setRuntimeApiKey(inputs.model.provider, inputs.runtimeApiKey, { signal });
146
146
  if (signal.aborted) throw new Error(`${inputs.label} startup aborted`);
147
147
  const resourceLoader = new DefaultResourceLoader({
148
148
  cwd: inputs.cwd,
@@ -24,6 +24,11 @@ export interface EffortProviderCandidates {
24
24
  }>;
25
25
  }
26
26
 
27
+ export interface EffortCandidateOptions {
28
+ includeParentModel: boolean;
29
+ preferredProvider?: string;
30
+ }
31
+
27
32
  const MODEL_PREFERENCES: Record<ModelEffort, readonly ProviderPreference[]> = {
28
33
  quick: [
29
34
  {
@@ -94,13 +99,19 @@ export function resolveEffortProviders(
94
99
  export function resolveEffortCandidates(
95
100
  ctx: Pick<ExtensionContext, "modelRegistry" | "model" | "cwd" | "isProjectTrusted">,
96
101
  effort: ModelEffort,
97
- includeParentModel: boolean,
102
+ options: EffortCandidateOptions,
98
103
  ): Promise<ModelCandidate[]> {
104
+ const preferences = options.preferredProvider
105
+ ? [
106
+ ...MODEL_PREFERENCES[effort].filter(({ provider }) => provider === options.preferredProvider),
107
+ ...MODEL_PREFERENCES[effort].filter(({ provider }) => provider !== options.preferredProvider),
108
+ ]
109
+ : MODEL_PREFERENCES[effort];
99
110
  return resolveCandidates(
100
111
  ctx,
101
- MODEL_PREFERENCES[effort].flatMap((preference) =>
112
+ preferences.flatMap((preference) =>
102
113
  preference.models.map((model) => ({ provider: preference.provider, ...model })),
103
114
  ),
104
- includeParentModel,
115
+ options.includeParentModel,
105
116
  );
106
117
  }