@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.
- package/extensions/bash-approval/README.md +20 -0
- package/extensions/bash-approval/index.ts +235 -0
- package/extensions/bash-approval/settings.ts +24 -0
- package/extensions/checkpoint/checkpoint-budget.ts +1 -14
- package/extensions/checkpoint/checkpoint.ts +2 -0
- package/extensions/checkpoint/index.ts +1 -1
- package/extensions/checkpoint/messages.ts +1 -0
- package/extensions/commit/commit-plan.ts +2 -2
- package/extensions/reference/index.ts +2 -0
- package/extensions/reference/panel.ts +66 -15
- package/extensions/review/README.md +14 -6
- package/extensions/review/index.ts +67 -120
- package/extensions/review/model.ts +13 -33
- package/extensions/review/session.ts +9 -23
- package/extensions/soul/README.md +2 -2
- package/extensions/soul/index.ts +4 -4
- package/extensions/soul/prompt.ts +6 -10
- package/extensions/soul/settings.ts +6 -3
- package/extensions/subagent/agents/web-research.md +2 -2
- package/extensions/subagent/run.ts +6 -2
- package/extensions/tau-help/help.md +6 -2
- package/package.json +2 -2
- package/schemas/tau.schema.json +18 -2
- package/shared/isolated-session.ts +1 -1
- package/shared/model-effort.ts +14 -3
- package/extensions/review/panel.ts +0 -99
|
@@ -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
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { createGitRunner, loadRepoStatus } from "../../shared/git.ts";
|
|
5
|
-
import {
|
|
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: "
|
|
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
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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,
|
|
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:
|
|
59
|
-
preferredThinkingLevel:
|
|
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:
|
|
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
|
-
|
|
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
|
-
- **
|
|
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, "
|
|
15
|
+
"soul": { "ponytail": true, "simplified": false }
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
```
|
package/extensions/soul/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
29
|
+
export const SIMPLIFIED_TECHNICAL_ENGLISH = `## Communication style
|
|
30
30
|
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
13
|
-
Type.Boolean({
|
|
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-
|
|
15
|
-
thinking:
|
|
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 === "
|
|
310
|
-
details.response =
|
|
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
|
|
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 `
|
|
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.
|
|
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.
|
|
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",
|
package/schemas/tau.schema.json
CHANGED
|
@@ -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
|
-
"
|
|
279
|
+
"simplified": {
|
|
264
280
|
"type": "boolean",
|
|
265
281
|
"default": true,
|
|
266
|
-
"description": "Add
|
|
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, {
|
|
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,
|
package/shared/model-effort.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|