@shanepadgett/tau-agent 0.29.0 → 0.31.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/docs/context.md +14 -9
- package/docs/subagents.md +7 -5
- package/extensions/cache-diagnostics/index.ts +3 -4
- package/extensions/context/README.md +8 -5
- package/extensions/context/definitions.ts +28 -17
- package/extensions/context/evidence.ts +4 -3
- package/extensions/context/index.ts +57 -35
- package/extensions/context/panel.ts +10 -9
- package/extensions/context/projection.ts +141 -0
- package/extensions/context/state.ts +30 -0
- package/extensions/explore/ast/read/hook.ts +3 -1
- package/extensions/ideas/browser.ts +27 -16
- package/extensions/review/README.md +11 -0
- package/extensions/review/index.ts +135 -0
- package/extensions/review/model.ts +144 -0
- package/extensions/review/panel.ts +128 -0
- package/extensions/review/session.ts +106 -0
- package/extensions/script-runner/README.md +7 -0
- package/extensions/script-runner/index.ts +275 -0
- package/extensions/stash/browser.ts +30 -18
- package/extensions/subagent/README.md +17 -4
- package/extensions/subagent/agents/context-sync.md +2 -2
- package/extensions/subagent/agents/scout.md +48 -61
- package/extensions/subagent/agents.ts +0 -1
- package/extensions/subagent/cmux-dashboard.ts +7 -3
- package/extensions/subagent/index.ts +105 -4
- package/extensions/subagent/panel.ts +124 -0
- package/extensions/subagent/render.ts +2 -1
- package/extensions/subagent/run.ts +9 -9
- package/extensions/subagent/runtime.ts +5 -5
- package/extensions/subagent/session-resource.ts +19 -127
- package/extensions/subagent/settings.ts +18 -0
- package/extensions/tau-help/help.md +11 -7
- package/extensions/working-memory/README.md +1 -1
- package/extensions/working-memory/checkpoint.ts +3 -4
- package/extensions/working-memory/index.ts +25 -7
- package/extensions/working-memory/memory.ts +53 -82
- package/package.json +2 -2
- package/schemas/tau.schema.json +9 -23
- package/shared/context-messages.ts +19 -0
- package/shared/injected-context.ts +2 -2
- package/shared/isolated-session.ts +151 -0
- package/extensions/subagent/agents/review.md +0 -75
- package/extensions/turn-budget/README.md +0 -14
- package/extensions/turn-budget/index.ts +0 -116
- package/extensions/turn-budget/settings.ts +0 -35
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ReadToolDetails } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { loadTauExtensionSettings } from "../../../../shared/settings/load.ts";
|
|
3
3
|
import exploreSettings from "../../settings.ts";
|
|
4
4
|
import { resolveExplorePath } from "../../traverse.ts";
|
|
@@ -9,6 +9,7 @@ import { formatLargeReadOutline, readCallKind, shouldOutlineFullRead, type Explo
|
|
|
9
9
|
|
|
10
10
|
type ReadOverlayResult = {
|
|
11
11
|
content: Array<{ type: "text"; text: string }>;
|
|
12
|
+
details: ReadToolDetails;
|
|
12
13
|
isError: boolean;
|
|
13
14
|
};
|
|
14
15
|
|
|
@@ -80,6 +81,7 @@ async function substituteLargeFullRead(options: {
|
|
|
80
81
|
result.file.rows.length === 0 ? "No declarations" : formatOutlineFile(result.file, options.engine.cwd, false);
|
|
81
82
|
return {
|
|
82
83
|
content: [{ type: "text", text: formatLargeReadOutline(outlineText) }],
|
|
84
|
+
details: {},
|
|
83
85
|
isError: false,
|
|
84
86
|
};
|
|
85
87
|
} catch {
|
|
@@ -6,17 +6,15 @@ import {
|
|
|
6
6
|
type TextRecordSelectPanelConfig,
|
|
7
7
|
type TextRecordSelectResult,
|
|
8
8
|
} from "@shanepadgett/tau-tui";
|
|
9
|
+
import { errorText } from "../../shared/text.ts";
|
|
9
10
|
import { deleteIdea, type Idea, ideasFilePath, loadIdeas, updateIdea } from "./store.ts";
|
|
10
11
|
|
|
11
|
-
const CONFIG: Omit<TextRecordSelectPanelConfig
|
|
12
|
+
const CONFIG: Omit<TextRecordSelectPanelConfig<Idea>, "path" | "destructiveAction"> = {
|
|
12
13
|
title: "Ideas",
|
|
13
14
|
emptyMessage: "No ideas yet. Use /ideas <text> to log one.",
|
|
14
15
|
primaryLabel: "insert",
|
|
15
16
|
expandActiveItem: true,
|
|
16
|
-
actions: [
|
|
17
|
-
{ id: "edit", key: Key.ctrl("e"), hint: rawHint("ctrl+e", "edit") },
|
|
18
|
-
{ id: "delete", key: Key.ctrl("d"), hint: rawHint("ctrl+d", "delete") },
|
|
19
|
-
],
|
|
17
|
+
actions: [{ id: "edit", key: Key.ctrl("e"), hint: rawHint("ctrl+e", "edit") }],
|
|
20
18
|
};
|
|
21
19
|
|
|
22
20
|
export async function browseIdeas(ctx: ExtensionCommandContext): Promise<Idea | undefined> {
|
|
@@ -34,15 +32,6 @@ export async function browseIdeas(ctx: ExtensionCommandContext): Promise<Idea |
|
|
|
34
32
|
if (result.kind === "cancel") return undefined;
|
|
35
33
|
if (result.kind === "primary") return result.item;
|
|
36
34
|
|
|
37
|
-
if (result.actionId === "delete") {
|
|
38
|
-
const ok = await ctx.ui.confirm("Delete idea?", result.item.text);
|
|
39
|
-
if (ok) {
|
|
40
|
-
await deleteIdea(ctx.cwd, result.item.id);
|
|
41
|
-
ctx.ui.notify("Idea deleted.", "info");
|
|
42
|
-
}
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
35
|
// edit: native multiline editor, prefilled with the current text.
|
|
47
36
|
const edited = await ctx.ui.editor("Edit idea", result.item.text);
|
|
48
37
|
if (edited == null) continue;
|
|
@@ -60,7 +49,29 @@ async function show(
|
|
|
60
49
|
ideas: readonly Idea[],
|
|
61
50
|
path: string,
|
|
62
51
|
): Promise<TextRecordSelectResult<Idea>> {
|
|
63
|
-
return ctx.ui.custom<TextRecordSelectResult<Idea>>((
|
|
64
|
-
createTextRecordSelectPanel(
|
|
52
|
+
return ctx.ui.custom<TextRecordSelectResult<Idea>>((tui, theme, _keybindings, done) =>
|
|
53
|
+
createTextRecordSelectPanel(
|
|
54
|
+
tui,
|
|
55
|
+
theme,
|
|
56
|
+
ideas,
|
|
57
|
+
{
|
|
58
|
+
...CONFIG,
|
|
59
|
+
path,
|
|
60
|
+
destructiveAction: {
|
|
61
|
+
id: "delete",
|
|
62
|
+
key: Key.ctrl("d"),
|
|
63
|
+
hint: rawHint("ctrl+d", "delete"),
|
|
64
|
+
confirmLabel: () => "Delete idea?",
|
|
65
|
+
runningLabel: "Deleting idea…",
|
|
66
|
+
onConfirm: async (item) => {
|
|
67
|
+
const next = await deleteIdea(ctx.cwd, item.id);
|
|
68
|
+
ctx.ui.notify("Idea deleted.", "info");
|
|
69
|
+
return next;
|
|
70
|
+
},
|
|
71
|
+
onError: (error) => ctx.ui.notify(`Idea delete failed: ${errorText(error)}`, "error"),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
done,
|
|
75
|
+
),
|
|
65
76
|
);
|
|
66
77
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Review
|
|
2
|
+
|
|
3
|
+
Run `/review` to inspect current repository's staged, unstaged, and untracked work in a fresh isolated session. Choose one focused pass:
|
|
4
|
+
|
|
5
|
+
- `simplify` looks for code and concepts that can disappear or reuse what already exists.
|
|
6
|
+
- `architecture` performs a nuclear maintainability review and accepts substantial redesign when ownership, reuse, or structure is poor.
|
|
7
|
+
- `correctness` checks runtime bugs and failures after architecture is accepted.
|
|
8
|
+
|
|
9
|
+
Run a mode directly with `/review simplify`, `/review architecture`, or `/review correctness`. Run `/review show` to reopen latest result on current session branch.
|
|
10
|
+
|
|
11
|
+
Results stay outside parent agent context. In result view, press `a` to send review to agent's next turn or `e` to export Markdown under `.pi/tau/reviews/`.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { createGitRunner, loadRepoStatus } from "../../shared/git.ts";
|
|
5
|
+
import { createInjectedContext } from "../../shared/injected-context.ts";
|
|
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";
|
|
16
|
+
import { runReview } from "./session.ts";
|
|
17
|
+
|
|
18
|
+
export default function reviewExtension(pi: ExtensionAPI): void {
|
|
19
|
+
pi.registerCommand("review", {
|
|
20
|
+
description: "Run or show an isolated simplify, architecture, or correctness review",
|
|
21
|
+
handler: async (args, ctx) => {
|
|
22
|
+
if (ctx.mode !== "tui" || !ctx.isProjectTrusted()) {
|
|
23
|
+
ctx.ui.notify("/review requires a trusted TUI project", "warning");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
await ctx.waitForIdle();
|
|
27
|
+
const requested = args.trim().toLowerCase();
|
|
28
|
+
if (requested === "show") {
|
|
29
|
+
const latest = latestReview(ctx.sessionManager.getBranch());
|
|
30
|
+
if (!latest) {
|
|
31
|
+
ctx.ui.notify("No review exists on this session branch", "warning");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await showReview(pi, ctx, latest);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let mode: ReviewMode | undefined;
|
|
39
|
+
if (requested) {
|
|
40
|
+
if (!isReviewMode(requested)) {
|
|
41
|
+
ctx.ui.notify("Usage: /review [simplify|architecture|correctness|show]", "warning");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
mode = requested;
|
|
45
|
+
} else {
|
|
46
|
+
const selected = await ctx.ui.select("Review mode", ["Simplify", "Architecture", "Correctness"]);
|
|
47
|
+
if (!selected) return;
|
|
48
|
+
const normalized = selected.toLowerCase();
|
|
49
|
+
if (!isReviewMode(normalized)) return;
|
|
50
|
+
mode = normalized;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const status = await loadRepoStatus(createGitRunner(pi, ctx));
|
|
54
|
+
if (!status) {
|
|
55
|
+
ctx.ui.notify("/review requires a Git repository", "warning");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (status.fileCount === 0) {
|
|
59
|
+
ctx.ui.notify("Nothing to review: working tree is clean", "info");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const signal = ctx.signal ? AbortSignal.any([controller.signal, ctx.signal]) : controller.signal;
|
|
65
|
+
let failure: string | undefined;
|
|
66
|
+
const output = await ctx.ui.custom<ReviewRecord | undefined>((tui, theme, keys, done) => {
|
|
67
|
+
const panel = new ReviewProgressPanel(tui, theme, mode, keys, () => controller.abort());
|
|
68
|
+
void runReview({
|
|
69
|
+
ctx,
|
|
70
|
+
root: status.root,
|
|
71
|
+
mode,
|
|
72
|
+
parentThinkingLevel: pi.getThinkingLevel(),
|
|
73
|
+
signal,
|
|
74
|
+
onProgress: (line) => panel.update(line),
|
|
75
|
+
})
|
|
76
|
+
.then((result) => done({ ...result, mode, root: status.root, createdAt: new Date().toISOString() }))
|
|
77
|
+
.catch((error: unknown) => {
|
|
78
|
+
failure = errorText(error);
|
|
79
|
+
done(undefined);
|
|
80
|
+
});
|
|
81
|
+
return panel;
|
|
82
|
+
});
|
|
83
|
+
if (!output) {
|
|
84
|
+
ctx.ui.notify(
|
|
85
|
+
signal.aborted ? "Review cancelled" : `Review failed: ${failure ?? "unknown error"}`,
|
|
86
|
+
signal.aborted ? "info" : "error",
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
pi.appendEntry(REVIEW_ENTRY_TYPE, output);
|
|
91
|
+
await showReview(pi, ctx, output);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function latestReview(entries: readonly SessionEntry[]): ReviewRecord | undefined {
|
|
97
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
98
|
+
const entry = entries[index];
|
|
99
|
+
if (entry?.type !== "custom" || entry.customType !== REVIEW_ENTRY_TYPE) continue;
|
|
100
|
+
if (isReviewRecord(entry.data)) return entry.data;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function showReview(pi: ExtensionAPI, ctx: ExtensionContext, review: ReviewRecord): Promise<void> {
|
|
106
|
+
const action = await ctx.ui.custom<ReviewResultAction>(
|
|
107
|
+
(tui, theme, keys, done) => new ReviewResultPanel(tui, theme, keys, review, done),
|
|
108
|
+
{
|
|
109
|
+
overlay: true,
|
|
110
|
+
overlayOptions: { anchor: "top-center", width: "80%", minWidth: 68, maxHeight: "90%", margin: 1 },
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
if (action === "send") {
|
|
114
|
+
try {
|
|
115
|
+
await pi.sendMessage(
|
|
116
|
+
createInjectedContext(formatReviewMarkdown(review), { source: "review", title: `${review.mode} review` }),
|
|
117
|
+
{ triggerTurn: false, deliverAs: "nextTurn" },
|
|
118
|
+
);
|
|
119
|
+
ctx.ui.notify("Review queued for agent's next turn", "info");
|
|
120
|
+
} catch (error) {
|
|
121
|
+
ctx.ui.notify(`Failed to send review: ${errorText(error)}`, "error");
|
|
122
|
+
}
|
|
123
|
+
} else if (action === "export") {
|
|
124
|
+
try {
|
|
125
|
+
const directory = join(review.root, ".pi", "tau", "reviews");
|
|
126
|
+
await mkdir(directory, { recursive: true });
|
|
127
|
+
const timestamp = review.createdAt.replace(/[^0-9A-Za-z-]/g, "-");
|
|
128
|
+
const path = join(directory, `${timestamp}-${review.mode}.md`);
|
|
129
|
+
await writeFile(path, formatReviewMarkdown(review), { encoding: "utf8", mode: 0o600 });
|
|
130
|
+
ctx.ui.notify(`Review exported to ${path}`, "info");
|
|
131
|
+
} catch (error) {
|
|
132
|
+
ctx.ui.notify(`Failed to export review: ${errorText(error)}`, "error");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { Tool } from "@earendil-works/pi-ai";
|
|
2
|
+
import { Type, type Static } from "typebox";
|
|
3
|
+
import { Value } from "typebox/value";
|
|
4
|
+
|
|
5
|
+
const MAX_SUMMARY_LENGTH = 2_000;
|
|
6
|
+
const MAX_FINDINGS = 12;
|
|
7
|
+
const MAX_PATH_LENGTH = 400;
|
|
8
|
+
const MAX_LINES_LENGTH = 80;
|
|
9
|
+
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
|
+
const REVIEW_FINDING_SCHEMA = Type.Object(
|
|
16
|
+
{
|
|
17
|
+
severity: Type.Union([
|
|
18
|
+
Type.Literal("critical"),
|
|
19
|
+
Type.Literal("high"),
|
|
20
|
+
Type.Literal("medium"),
|
|
21
|
+
Type.Literal("low"),
|
|
22
|
+
]),
|
|
23
|
+
path: Type.String({
|
|
24
|
+
minLength: 1,
|
|
25
|
+
maxLength: MAX_PATH_LENGTH,
|
|
26
|
+
description: "Repository-relative file path",
|
|
27
|
+
}),
|
|
28
|
+
lines: Type.String({
|
|
29
|
+
minLength: 1,
|
|
30
|
+
maxLength: MAX_LINES_LENGTH,
|
|
31
|
+
description: "Exact line or line range, or N/A when no source line exists",
|
|
32
|
+
}),
|
|
33
|
+
finding: Type.String({
|
|
34
|
+
minLength: 1,
|
|
35
|
+
maxLength: MAX_FINDING_LENGTH,
|
|
36
|
+
description: "Failure mechanism or concrete maintenance cost",
|
|
37
|
+
}),
|
|
38
|
+
fix: Type.String({
|
|
39
|
+
minLength: 1,
|
|
40
|
+
maxLength: MAX_FINDING_LENGTH,
|
|
41
|
+
description: "Smallest credible fix direction",
|
|
42
|
+
}),
|
|
43
|
+
},
|
|
44
|
+
{ additionalProperties: false },
|
|
45
|
+
);
|
|
46
|
+
const REVIEW_OUTPUT_SCHEMA = Type.Object(
|
|
47
|
+
{
|
|
48
|
+
verdict: Type.Union([Type.Literal("pass"), Type.Literal("changes-required")]),
|
|
49
|
+
summary: Type.String({
|
|
50
|
+
minLength: 1,
|
|
51
|
+
maxLength: MAX_SUMMARY_LENGTH,
|
|
52
|
+
description: "Short, blunt prose verdict",
|
|
53
|
+
}),
|
|
54
|
+
findings: Type.Array(REVIEW_FINDING_SCHEMA, { maxItems: MAX_FINDINGS }),
|
|
55
|
+
},
|
|
56
|
+
{ additionalProperties: false },
|
|
57
|
+
);
|
|
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
|
+
|
|
68
|
+
export type ReviewMode = Static<typeof REVIEW_MODE_SCHEMA>;
|
|
69
|
+
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";
|
|
73
|
+
|
|
74
|
+
export const REVIEW_RESULT_TOOL = {
|
|
75
|
+
name: "review_result",
|
|
76
|
+
description: "Submit final review. This is the only allowed final response.",
|
|
77
|
+
parameters: REVIEW_OUTPUT_SCHEMA,
|
|
78
|
+
} satisfies Tool;
|
|
79
|
+
|
|
80
|
+
const MODE_INSTRUCTIONS: Record<ReviewMode, string> = {
|
|
81
|
+
simplify: [
|
|
82
|
+
"Delete concepts before improving them.",
|
|
83
|
+
"Find behavior that was not requested, code the repository or platform already provides, duplicate concepts, needless files, wrappers, helpers, types, options, and branches.",
|
|
84
|
+
"Judge against the smallest credible implementation. Runtime correctness is out of scope unless a problem directly proves needless complexity.",
|
|
85
|
+
].join(" "),
|
|
86
|
+
architecture: [
|
|
87
|
+
"Assume the current architecture is poor and likely needs substantial rework. Make existing structure prove it should remain.",
|
|
88
|
+
"Inspect ownership, boundaries, shared roots, reuse, sources of truth, coupling, cohesion, and clean coding patterns.",
|
|
89
|
+
"Prefer a coherent redesign over preserving a bad local structure. Runtime correctness is secondary and should appear only when it supports an architectural finding.",
|
|
90
|
+
].join(" "),
|
|
91
|
+
correctness: [
|
|
92
|
+
"Architecture is accepted for this pass.",
|
|
93
|
+
"Look only for concrete runtime bugs, broken state transitions, unsafe boundaries, data loss, error-handling failures, and affected callers.",
|
|
94
|
+
"Do not report style, simplification, or architecture preferences.",
|
|
95
|
+
].join(" "),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export function buildReviewPrompt(root: string, mode: ReviewMode): string {
|
|
99
|
+
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.",
|
|
102
|
+
"Do not modify files. Do not report theoretical concerns or personal preferences. Use the cheapest evidence that settles each point.",
|
|
103
|
+
MODE_INSTRUCTIONS[mode],
|
|
104
|
+
`Call ${REVIEW_RESULT_TOOL.name} exactly once as the final action. Write no final prose outside that tool call.`,
|
|
105
|
+
"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.",
|
|
106
|
+
].join("\n\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function formatReviewMarkdown(review: ReviewRecord): string {
|
|
110
|
+
const findings = review.findings.length
|
|
111
|
+
? review.findings.flatMap((finding, index) => [
|
|
112
|
+
`### ${index + 1}. ${finding.severity.toUpperCase()} — ${finding.path}:${finding.lines}`,
|
|
113
|
+
"",
|
|
114
|
+
finding.finding,
|
|
115
|
+
"",
|
|
116
|
+
`**Fix:** ${finding.fix}`,
|
|
117
|
+
"",
|
|
118
|
+
])
|
|
119
|
+
: ["No actionable findings.", ""];
|
|
120
|
+
return [
|
|
121
|
+
`# ${reviewModeLabel(review.mode)} review`,
|
|
122
|
+
"",
|
|
123
|
+
`**Verdict:** ${review.verdict}`,
|
|
124
|
+
`**Created:** ${review.createdAt}`,
|
|
125
|
+
"",
|
|
126
|
+
review.summary,
|
|
127
|
+
"",
|
|
128
|
+
"## Findings",
|
|
129
|
+
"",
|
|
130
|
+
...findings,
|
|
131
|
+
].join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
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 {
|
|
139
|
+
return `${mode.charAt(0).toUpperCase()}${mode.slice(1)}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function isReviewRecord(value: unknown): value is ReviewRecord {
|
|
143
|
+
return Value.Check(REVIEW_RECORD_SCHEMA, value);
|
|
144
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { bindingHint, bindingsHint, rawHint, ToolPanel } from "@shanepadgett/tau-tui";
|
|
2
|
+
import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Markdown, type Component, type KeybindingsManager, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
|
|
4
|
+
import { formatReviewMarkdown, reviewModeLabel, type ReviewMode, type ReviewRecord } from "./model.ts";
|
|
5
|
+
|
|
6
|
+
export class ReviewProgressPanel implements Component {
|
|
7
|
+
private readonly tui: TUI;
|
|
8
|
+
private readonly panel: ToolPanel;
|
|
9
|
+
private readonly lines: string[] = [];
|
|
10
|
+
|
|
11
|
+
constructor(tui: TUI, theme: Theme, mode: ReviewMode, keys: KeybindingsManager, cancel: () => void) {
|
|
12
|
+
this.tui = tui;
|
|
13
|
+
this.panel = new ToolPanel(theme, {
|
|
14
|
+
title: `${reviewModeLabel(mode)} review`,
|
|
15
|
+
secondary: "Fresh isolated review · hidden from parent context",
|
|
16
|
+
body: {
|
|
17
|
+
render: (width) =>
|
|
18
|
+
(this.lines.length ? this.lines.slice(-5) : ["Starting review"]).map((line) =>
|
|
19
|
+
truncateToWidth(theme.fg("muted", line), width, "…"),
|
|
20
|
+
),
|
|
21
|
+
invalidate() {},
|
|
22
|
+
},
|
|
23
|
+
footer: { kind: "hints", hints: [bindingHint("tui.select.cancel", "cancel")] },
|
|
24
|
+
border: "box",
|
|
25
|
+
});
|
|
26
|
+
this.handleInput = (data) => {
|
|
27
|
+
if (!keys.matches(data, "tui.select.cancel")) return;
|
|
28
|
+
cancel();
|
|
29
|
+
this.update("Cancelling review");
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
handleInput: (data: string) => void;
|
|
34
|
+
|
|
35
|
+
update(status: string): void {
|
|
36
|
+
if (this.lines.at(-1) !== status) {
|
|
37
|
+
this.lines.push(status);
|
|
38
|
+
if (this.lines.length > 5) this.lines.shift();
|
|
39
|
+
}
|
|
40
|
+
this.tui.requestRender();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
render(width: number): string[] {
|
|
44
|
+
return this.panel.render(width);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
invalidate(): void {
|
|
48
|
+
this.panel.invalidate();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ReviewResultAction = "send" | "export" | "close";
|
|
53
|
+
|
|
54
|
+
export class ReviewResultPanel implements Component {
|
|
55
|
+
private readonly panel: ToolPanel;
|
|
56
|
+
private readonly body: ReviewMarkdownBody;
|
|
57
|
+
|
|
58
|
+
constructor(
|
|
59
|
+
tui: TUI,
|
|
60
|
+
theme: Theme,
|
|
61
|
+
keys: KeybindingsManager,
|
|
62
|
+
review: ReviewRecord,
|
|
63
|
+
done: (action: ReviewResultAction) => void,
|
|
64
|
+
) {
|
|
65
|
+
this.body = new ReviewMarkdownBody(tui, formatReviewMarkdown(review));
|
|
66
|
+
this.panel = new ToolPanel(theme, {
|
|
67
|
+
title: `${reviewModeLabel(review.mode)} review · ${review.verdict}`,
|
|
68
|
+
secondary: `${review.findings.length} finding${review.findings.length === 1 ? "" : "s"} · not in parent context`,
|
|
69
|
+
body: this.body,
|
|
70
|
+
footer: {
|
|
71
|
+
kind: "hints",
|
|
72
|
+
hints: [
|
|
73
|
+
bindingsHint(["tui.select.up", "tui.select.down"], "scroll"),
|
|
74
|
+
rawHint("a", "send to agent"),
|
|
75
|
+
rawHint("e", "export"),
|
|
76
|
+
bindingHint("tui.select.cancel", "close"),
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
border: "box",
|
|
80
|
+
});
|
|
81
|
+
this.handleInput = (data) => {
|
|
82
|
+
if (keys.matches(data, "tui.select.up")) this.body.scroll(-1);
|
|
83
|
+
else if (keys.matches(data, "tui.select.down")) this.body.scroll(1);
|
|
84
|
+
else if (data === "a") done("send");
|
|
85
|
+
else if (data === "e") done("export");
|
|
86
|
+
else if (keys.matches(data, "tui.select.cancel")) done("close");
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
handleInput: (data: string) => void;
|
|
91
|
+
|
|
92
|
+
render(width: number): string[] {
|
|
93
|
+
return this.panel.render(width);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
invalidate(): void {
|
|
97
|
+
this.panel.invalidate();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class ReviewMarkdownBody implements Component {
|
|
102
|
+
private readonly tui: TUI;
|
|
103
|
+
private readonly markdown: Markdown;
|
|
104
|
+
private offset = 0;
|
|
105
|
+
private maxOffset = 0;
|
|
106
|
+
|
|
107
|
+
constructor(tui: TUI, content: string) {
|
|
108
|
+
this.tui = tui;
|
|
109
|
+
this.markdown = new Markdown(content, 0, 0, getMarkdownTheme());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
scroll(delta: number): void {
|
|
113
|
+
this.offset = Math.max(0, Math.min(this.maxOffset, this.offset + delta));
|
|
114
|
+
this.tui.requestRender();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
render(width: number): string[] {
|
|
118
|
+
const lines = this.markdown.render(width);
|
|
119
|
+
const height = Math.max(4, Math.floor(this.tui.terminal.rows * 0.9) - 9);
|
|
120
|
+
this.maxOffset = Math.max(0, lines.length - height);
|
|
121
|
+
this.offset = Math.min(this.offset, this.maxOffset);
|
|
122
|
+
return lines.slice(this.offset, this.offset + height);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
invalidate(): void {
|
|
126
|
+
this.markdown.invalidate();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { defineTool, type AgentSessionEvent, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import {
|
|
5
|
+
createIsolatedSessionResource,
|
|
6
|
+
resolveIsolatedSessionModel,
|
|
7
|
+
type IsolatedSessionResource,
|
|
8
|
+
} from "../../shared/isolated-session.ts";
|
|
9
|
+
import { buildReviewPrompt, REVIEW_RESULT_TOOL, type ReviewMode, type ReviewOutput } from "./model.ts";
|
|
10
|
+
|
|
11
|
+
const REVIEW_MODEL = "openai-codex/gpt-5.6-sol";
|
|
12
|
+
const REVIEW_TOOLS = [
|
|
13
|
+
"read",
|
|
14
|
+
"bash",
|
|
15
|
+
"outline",
|
|
16
|
+
"show",
|
|
17
|
+
"discover",
|
|
18
|
+
"ast_search",
|
|
19
|
+
"deps",
|
|
20
|
+
"reverse_deps",
|
|
21
|
+
"callers",
|
|
22
|
+
"callees",
|
|
23
|
+
"references",
|
|
24
|
+
"implementations",
|
|
25
|
+
"impact",
|
|
26
|
+
"context",
|
|
27
|
+
REVIEW_RESULT_TOOL.name,
|
|
28
|
+
];
|
|
29
|
+
const EXPLORE_EXTENSION = join(dirname(fileURLToPath(import.meta.url)), "..", "explore", "index.ts");
|
|
30
|
+
|
|
31
|
+
export async function runReview(options: {
|
|
32
|
+
ctx: ExtensionContext;
|
|
33
|
+
root: string;
|
|
34
|
+
mode: ReviewMode;
|
|
35
|
+
parentThinkingLevel: NonNullable<ExtensionContext["thinkingLevel"]>;
|
|
36
|
+
signal: AbortSignal;
|
|
37
|
+
onProgress: (status: string) => void;
|
|
38
|
+
}): Promise<ReviewOutput> {
|
|
39
|
+
const { ctx, root, mode, signal, onProgress } = options;
|
|
40
|
+
let output: ReviewOutput | undefined;
|
|
41
|
+
const outputTool = defineTool({
|
|
42
|
+
...REVIEW_RESULT_TOOL,
|
|
43
|
+
label: "Review Result",
|
|
44
|
+
async execute(_toolCallId, params) {
|
|
45
|
+
output = params;
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text" as const, text: "Review recorded" }],
|
|
48
|
+
details: params,
|
|
49
|
+
terminate: true,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
let resource: IsolatedSessionResource | undefined;
|
|
54
|
+
let unsubscribe: (() => void) | undefined;
|
|
55
|
+
try {
|
|
56
|
+
const selected = await resolveIsolatedSessionModel({
|
|
57
|
+
label: "Review",
|
|
58
|
+
preferredModel: REVIEW_MODEL,
|
|
59
|
+
preferredThinkingLevel: "high",
|
|
60
|
+
usePreferredThinkingAfterModelFallback: false,
|
|
61
|
+
ctx,
|
|
62
|
+
parentThinkingLevel: options.parentThinkingLevel,
|
|
63
|
+
signal,
|
|
64
|
+
onWarning: onProgress,
|
|
65
|
+
});
|
|
66
|
+
resource = await createIsolatedSessionResource(
|
|
67
|
+
{
|
|
68
|
+
...selected,
|
|
69
|
+
label: "Review",
|
|
70
|
+
extensionPaths: [EXPLORE_EXTENSION],
|
|
71
|
+
cwd: root,
|
|
72
|
+
tools: REVIEW_TOOLS,
|
|
73
|
+
customTools: [outputTool],
|
|
74
|
+
bindTarget: { mode: "print" },
|
|
75
|
+
},
|
|
76
|
+
signal,
|
|
77
|
+
);
|
|
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
|
+
const abort = () => void session.abort().catch(() => undefined);
|
|
87
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
88
|
+
try {
|
|
89
|
+
onProgress(`Running ${mode} review`);
|
|
90
|
+
await session.prompt(buildReviewPrompt(root, mode), { expandPromptTemplates: false });
|
|
91
|
+
} finally {
|
|
92
|
+
signal.removeEventListener("abort", abort);
|
|
93
|
+
}
|
|
94
|
+
if (signal.aborted) throw new Error("Review cancelled");
|
|
95
|
+
if (!output) throw new Error("Review ended without structured output");
|
|
96
|
+
return output;
|
|
97
|
+
} finally {
|
|
98
|
+
unsubscribe?.();
|
|
99
|
+
await resource?.dispose();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Script Runner
|
|
2
|
+
|
|
3
|
+
Gives the agent a first-class `script_runner` tool for running Python and TypeScript instead of falling back to bash. The agent picks whichever language is more efficient for the task.
|
|
4
|
+
|
|
5
|
+
When a run fails, the tool keeps the script and returns a `scriptId`. The agent retries with targeted `{oldText, newText}` edits against what it just wrote instead of resending the whole script, saving output tokens and keeping duplicate scripts out of context. Only the source the agent already sent is referenced; no file path is exposed.
|
|
6
|
+
|
|
7
|
+
Languages are detected from the environment: Python via `python3`/`python`, TypeScript via Node with `--experimental-strip-types` (Node 22.6 or newer). The tool registers only the languages actually available and is hidden from the prompt entirely when neither is present.
|