@mrclrchtr/supi-review 2.8.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +71 -180
  2. package/node_modules/@mrclrchtr/supi-core/package.json +2 -1
  3. package/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +40 -0
  5. package/package.json +3 -3
  6. package/src/config.ts +30 -21
  7. package/src/git-command.ts +78 -0
  8. package/src/git.ts +311 -507
  9. package/src/history/collect.ts +43 -120
  10. package/src/model.ts +3 -1
  11. package/src/review-path.ts +85 -0
  12. package/src/review-result.ts +43 -92
  13. package/src/review.ts +187 -286
  14. package/src/session/review-plan-store.ts +20 -51
  15. package/src/target/packet.ts +46 -369
  16. package/src/tool/agent-review-schemas.ts +108 -104
  17. package/src/tool/agent-review-tools.ts +283 -307
  18. package/src/tool/child-failure-diagnostics.ts +59 -1
  19. package/src/tool/child-lifecycle-trace.ts +32 -3
  20. package/src/tool/child-resource-loader.ts +37 -0
  21. package/src/tool/child-session-runner.ts +83 -0
  22. package/src/tool/output-page.ts +47 -0
  23. package/src/tool/planner-runner.ts +69 -0
  24. package/src/tool/review-runner.ts +42 -257
  25. package/src/tool/review-system-prompt.ts +12 -110
  26. package/src/tool/review-tools.ts +119 -0
  27. package/src/tool/review-workflow.ts +325 -0
  28. package/src/tool/runner-helpers.ts +3 -8
  29. package/src/tool/schemas.ts +75 -62
  30. package/src/tui/common.ts +237 -0
  31. package/src/tui/prepare.ts +132 -0
  32. package/src/tui/run.ts +160 -0
  33. package/src/types.ts +157 -275
  34. package/src/history/synthesize.ts +0 -121
  35. package/src/tool/agent-review-workflow.ts +0 -398
  36. package/src/tool/brief-runner.ts +0 -180
  37. package/src/tool/guidance.ts +0 -21
  38. package/src/tool/review-handlers.ts +0 -314
  39. package/src/tool/snapshot-tools.ts +0 -117
  40. package/src/ui/flow.ts +0 -189
  41. package/src/ui/format-content.ts +0 -143
  42. package/src/ui/renderer.ts +0 -274
  43. package/src/ui/review-plan-inspector.ts +0 -391
  44. package/src/ui/review-tool-format.ts +0 -138
  45. package/src/ui/review-tool-renderer.ts +0 -234
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Shared TUI rendering helpers for supi-review tool output.
3
+ *
4
+ * Dual-surface rendering: chrome built from details, markdown body excluded.
5
+ * Follows the tool-rendering convention in docs/tool-rendering.md.
6
+ */
7
+
8
+ import type { Theme } from "@earendil-works/pi-coding-agent";
9
+ import { type Container, Spacer, Text } from "@earendil-works/pi-tui";
10
+ import { formatEvidenceBadge } from "@mrclrchtr/supi-core/evidence-badge";
11
+ import type {
12
+ ChildFailureDiagnostics,
13
+ FindingEffort,
14
+ FindingImpact,
15
+ ReviewFinding,
16
+ ReviewTaskResult,
17
+ TaskVerdict,
18
+ } from "../types.ts";
19
+
20
+ // ── Spinner state ────────────────────────────────────────────────
21
+
22
+ /** Progress indicator shown while the tool is still running. */
23
+ export function renderPartial(label: string, theme: Theme): Text {
24
+ return new Text(theme.fg("warning", `● ${label}`), 0, 0);
25
+ }
26
+
27
+ /** Error state when the tool fails. */
28
+ export function renderError(label: string, theme: Theme): Text {
29
+ return new Text(theme.fg("error", label), 0, 0);
30
+ }
31
+
32
+ // ── Verdict / status badges ──────────────────────────────────────
33
+
34
+ /** Colored verdict label for PASS or ISSUES. */
35
+ export function formatVerdictBadge(verdict: TaskVerdict, theme: Theme): string {
36
+ return verdict === "pass"
37
+ ? theme.fg("success", theme.bold("PASS"))
38
+ : theme.fg("error", theme.bold("ISSUES"));
39
+ }
40
+
41
+ /** Colored status label for non-completed task results. */
42
+ export function formatStatusLabel(
43
+ status: "failed" | "canceled" | "timeout",
44
+ failureCode?: string,
45
+ timeoutMs?: number,
46
+ ): string {
47
+ switch (status) {
48
+ case "failed":
49
+ return `FAILED${failureCode ? ` (${failureCode})` : ""}`;
50
+ case "canceled":
51
+ return "CANCELED";
52
+ case "timeout":
53
+ return `TIMEOUT (${timeoutMs} ms)`;
54
+ }
55
+ }
56
+
57
+ // ── Finding badges ───────────────────────────────────────────────
58
+
59
+ const IMPACT_LABEL: Record<FindingImpact, string> = {
60
+ low: "low",
61
+ medium: "medium",
62
+ high: "high",
63
+ };
64
+
65
+ const EFFORT_LABEL: Record<FindingEffort, string> = {
66
+ small: "small",
67
+ medium: "medium",
68
+ large: "large",
69
+ };
70
+
71
+ /** Build a compact finding summary line with impact, effort, and confidence badges. */
72
+ export function formatFindingLine(finding: ReviewFinding, theme: Theme): string {
73
+ const blocking = finding.blocksAcceptance
74
+ ? theme.fg("error", "BLOCKING")
75
+ : theme.fg("muted", "non-blocking");
76
+ const impact = theme.fg("dim", `impact: ${IMPACT_LABEL[finding.impact]}`);
77
+ const effort = theme.fg("dim", `effort: ${EFFORT_LABEL[finding.effort]}`);
78
+ const confidence = theme.fg("dim", `confidence: ${finding.confidence.toFixed(1)}`);
79
+ return `[${blocking}] ${theme.fg("accent", finding.title)} · ${impact} · ${effort} · ${confidence}`;
80
+ }
81
+
82
+ // ── Tool call rendering ──────────────────────────────────────────
83
+
84
+ /** Compact single-line renderCall shared by both review tools. */
85
+ export function renderReviewToolCall(
86
+ name: string,
87
+ primary: string,
88
+ theme: Theme,
89
+ secondary?: string,
90
+ ): Text {
91
+ let content = theme.fg("toolTitle", name);
92
+ if (primary) {
93
+ content += ` ${theme.fg("accent", primary)}`;
94
+ }
95
+ if (secondary) {
96
+ content += theme.fg("dim", ` — ${secondary}`);
97
+ }
98
+ return new Text(content, 0, 0);
99
+ }
100
+
101
+ // ── Task section (expanded) ──────────────────────────────────────
102
+
103
+ /** Render a single finding's details into the container. */
104
+ function renderFinding(container: Container, finding: ReviewFinding, theme: Theme): void {
105
+ container.addChild(new Text(formatFindingLine(finding, theme), 1, 0));
106
+ if (finding.location) {
107
+ container.addChild(
108
+ new Text(
109
+ theme.fg(
110
+ "dim",
111
+ ` ${finding.location.path}:${finding.location.startLine}-${finding.location.endLine}`,
112
+ ),
113
+ 1,
114
+ 0,
115
+ ),
116
+ );
117
+ }
118
+ container.addChild(new Text(theme.fg("text", ` ${finding.description}`), 1, 0));
119
+ container.addChild(new Spacer(1));
120
+ }
121
+
122
+ /** Build a compact diagnostics section for non-completed (failed/canceled/timeout) task results. */
123
+ function buildNonCompletedSection(
124
+ container: Container,
125
+ result: ReviewTaskResult & { status: "failed" | "canceled" | "timeout" },
126
+ theme: Theme,
127
+ ): void {
128
+ const diagnostics: ChildFailureDiagnostics | undefined =
129
+ "diagnostics" in result ? result.diagnostics : undefined;
130
+ if (!diagnostics) return;
131
+
132
+ container.addChild(new Spacer(1));
133
+ container.addChild(
134
+ new Text(
135
+ theme.fg("dim", `${diagnostics.turns} turns · ${diagnostics.toolUses} tool uses`),
136
+ 1,
137
+ 0,
138
+ ),
139
+ );
140
+
141
+ if (diagnostics.lastAssistantStopReason) {
142
+ container.addChild(
143
+ new Text(
144
+ theme.fg(
145
+ diagnostics.lastAssistantStopReason === "error" ? "error" : "dim",
146
+ `Last assistant stop: ${diagnostics.lastAssistantStopReason}`,
147
+ ),
148
+ 1,
149
+ 0,
150
+ ),
151
+ );
152
+ }
153
+
154
+ if (diagnostics.lastAssistantErrorText) {
155
+ const truncated =
156
+ diagnostics.lastAssistantErrorText.length > 200
157
+ ? `${diagnostics.lastAssistantErrorText.slice(0, 200)}…`
158
+ : diagnostics.lastAssistantErrorText;
159
+ container.addChild(new Text(theme.fg("error", truncated), 1, 0));
160
+ } else if (diagnostics.lastLifecycleErrorText) {
161
+ const truncated =
162
+ diagnostics.lastLifecycleErrorText.length > 200
163
+ ? `${diagnostics.lastLifecycleErrorText.slice(0, 200)}…`
164
+ : diagnostics.lastLifecycleErrorText;
165
+ container.addChild(new Text(theme.fg("error", truncated), 1, 0));
166
+ } else if (diagnostics.lastAssistantStopReason === "error") {
167
+ container.addChild(
168
+ new Text(
169
+ theme.fg(
170
+ "dim",
171
+ "The model produced an error with no further details — check provider quota, auth, or configuration.",
172
+ ),
173
+ 1,
174
+ 0,
175
+ ),
176
+ );
177
+ }
178
+ }
179
+
180
+ /** Build an expanded-view section for a single review task result. */
181
+ export function buildTaskSection(
182
+ container: Container,
183
+ result: ReviewTaskResult,
184
+ theme: Theme,
185
+ ): void {
186
+ // Header
187
+ const verdictOrStatus =
188
+ result.status === "completed"
189
+ ? formatVerdictBadge(result.verdict, theme)
190
+ : theme.fg(
191
+ "warning",
192
+ formatStatusLabel(
193
+ result.status,
194
+ result.status === "failed" ? result.failureCode : undefined,
195
+ result.status === "timeout" ? result.timeoutMs : undefined,
196
+ ),
197
+ );
198
+
199
+ const findingCount =
200
+ result.status === "completed"
201
+ ? formatEvidenceBadge({
202
+ shownCount: result.findings.length,
203
+ totalCount: result.findings.length,
204
+ omittedCount: 0,
205
+ partialReason: null,
206
+ label: result.findings.length === 1 ? "finding" : "findings",
207
+ })
208
+ : "";
209
+
210
+ const headerParts = [verdictOrStatus];
211
+ if (findingCount) headerParts.push(theme.fg("muted", findingCount));
212
+ container.addChild(new Text(headerParts.join(" "), 1, 0));
213
+
214
+ const metaParts = [
215
+ theme.fg("dim", `model: ${result.modelId}`),
216
+ theme.fg("dim", `hash: ${result.packetHash.slice(0, 12)}…`),
217
+ ];
218
+ container.addChild(new Text(metaParts.join(" "), 1, 0));
219
+
220
+ // Completed task — show summary and findings
221
+ if (result.status !== "completed") {
222
+ buildNonCompletedSection(container, result, theme);
223
+ return;
224
+ }
225
+
226
+ if (result.summary) {
227
+ container.addChild(new Spacer(1));
228
+ container.addChild(new Text(theme.fg("muted", result.summary), 1, 0));
229
+ }
230
+
231
+ if (result.findings.length === 0) return;
232
+
233
+ container.addChild(new Spacer(1));
234
+ for (const finding of result.findings) {
235
+ renderFinding(container, finding, theme);
236
+ }
237
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * TUI renderer for supi_review_prepare — renderCall + renderResult.
3
+ *
4
+ * Dual-surface rendering: chrome built from details, markdown body excluded.
5
+ */
6
+
7
+ import type { Theme } from "@earendil-works/pi-coding-agent";
8
+ import { Container, Spacer, Text } from "@earendil-works/pi-tui";
9
+ import type { PreparedReviewDetails } from "../types.ts";
10
+ import { renderError, renderPartial, renderReviewToolCall } from "./common.ts";
11
+
12
+ /** Plan details shape extracted from execute's details return. */
13
+ interface PrepareDetails extends PreparedReviewDetails {
14
+ plannerDraft?: {
15
+ sharedContext?: string;
16
+ tasks: Array<{ id: string; instructions: string }>;
17
+ };
18
+ }
19
+
20
+ // ── renderCall ───────────────────────────────────────────────────
21
+
22
+ export function renderPrepareCall(args: unknown, theme: Theme): Text {
23
+ const params = (args ?? {}) as {
24
+ planning?: string;
25
+ target?: { kind?: string };
26
+ };
27
+ const planning = params.planning ?? "none";
28
+ const targetKind = params.target?.kind ?? "working-tree";
29
+
30
+ return renderReviewToolCall("supi_review_prepare", planning, theme, targetKind);
31
+ }
32
+
33
+ // ── renderResult ─────────────────────────────────────────────────
34
+
35
+ export function renderPrepareResult(
36
+ result: {
37
+ content?: Array<{ type: string; text?: string }>;
38
+ details?: unknown;
39
+ isError?: boolean;
40
+ },
41
+ options: { expanded: boolean; isPartial: boolean },
42
+ theme: Theme,
43
+ ): Container | Text {
44
+ if (options.isPartial) return renderPartial("Preparing review…", theme);
45
+
46
+ const details = result.details as PrepareDetails | undefined;
47
+
48
+ if (result.isError || !details) {
49
+ return renderError("supi_review_prepare failed", theme);
50
+ }
51
+
52
+ if (!options.expanded) {
53
+ return buildCollapsed(details, theme);
54
+ }
55
+
56
+ return buildExpanded(details, theme);
57
+ }
58
+
59
+ // ── Collapsed ────────────────────────────────────────────────────
60
+
61
+ function buildCollapsed(details: PrepareDetails, theme: Theme): Text {
62
+ const fileCount = details.snapshot.changedFiles.length;
63
+ const targetKind = details.snapshot.requestedTarget.kind;
64
+ const hasPlanner = details.plannerDraft !== undefined;
65
+
66
+ const segments: string[] = [];
67
+ segments.push(theme.fg("accent", "Plan ready"));
68
+ segments.push(theme.fg("muted", targetKind));
69
+ segments.push(theme.fg("dim", `${fileCount} file${fileCount !== 1 ? "s" : ""} changed`));
70
+ if (hasPlanner) {
71
+ segments.push(theme.fg("muted", "with draft"));
72
+ }
73
+
74
+ return new Text(segments.join(` ${theme.fg("dim", "·")} `), 0, 0);
75
+ }
76
+
77
+ // ── Expanded ─────────────────────────────────────────────────────
78
+
79
+ function buildExpanded(details: PrepareDetails, theme: Theme): Container {
80
+ const container = new Container();
81
+
82
+ // Header
83
+ container.addChild(new Text(theme.fg("accent", theme.bold("Review Plan Prepared")), 1, 0));
84
+ container.addChild(new Spacer(1));
85
+
86
+ // Plan identity
87
+ container.addChild(new Text(theme.fg("muted", `Plan ID: ${details.planId}`), 1, 0));
88
+
89
+ // Target info
90
+ const snapshot = details.snapshot;
91
+ container.addChild(new Spacer(1));
92
+ container.addChild(new Text(theme.fg("accent", theme.bold("Target")), 1, 0));
93
+ container.addChild(new Text(theme.fg("muted", snapshot.title), 1, 0));
94
+ container.addChild(
95
+ new Text(
96
+ theme.fg(
97
+ "dim",
98
+ `${snapshot.changedFiles.length} file${snapshot.changedFiles.length !== 1 ? "s" : ""} changed · +${snapshot.stats.additions} / -${snapshot.stats.deletions}`,
99
+ ),
100
+ 1,
101
+ 0,
102
+ ),
103
+ );
104
+
105
+ // Model info
106
+ container.addChild(new Spacer(1));
107
+ container.addChild(new Text(theme.fg("dim", `Reviewer model: ${details.reviewerModelId}`), 1, 0));
108
+ if (details.plannerModelId) {
109
+ container.addChild(new Text(theme.fg("dim", `Planner model: ${details.plannerModelId}`), 1, 0));
110
+ }
111
+
112
+ // Planner draft
113
+ if (details.plannerDraft) {
114
+ container.addChild(new Spacer(1));
115
+ container.addChild(new Text(theme.fg("accent", theme.bold("Planner Draft")), 1, 0));
116
+ if (details.plannerDraft.sharedContext) {
117
+ container.addChild(new Spacer(1));
118
+ container.addChild(new Text(theme.fg("muted", details.plannerDraft.sharedContext), 1, 0));
119
+ }
120
+ if (details.plannerDraft.tasks.length > 0) {
121
+ container.addChild(new Spacer(1));
122
+ for (const task of details.plannerDraft.tasks) {
123
+ container.addChild(
124
+ new Text(`${theme.fg("accent", task.id)}: ${theme.fg("muted", task.instructions)}`, 1, 0),
125
+ );
126
+ container.addChild(new Text(theme.fg("dim", "─".repeat(40)), 1, 0));
127
+ }
128
+ }
129
+ }
130
+
131
+ return container;
132
+ }
package/src/tui/run.ts ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * TUI renderer for supi_review_run — renderCall + renderResult.
3
+ *
4
+ * Dual-surface rendering: chrome built from details, markdown body excluded.
5
+ */
6
+
7
+ import type { Theme } from "@earendil-works/pi-coding-agent";
8
+ import { Container, Spacer, Text } from "@earendil-works/pi-tui";
9
+ import type { ReviewBatchDetails, ReviewTaskResult } from "../types.ts";
10
+ import {
11
+ buildTaskSection,
12
+ formatStatusLabel,
13
+ formatVerdictBadge,
14
+ renderError,
15
+ renderPartial,
16
+ renderReviewToolCall,
17
+ } from "./common.ts";
18
+
19
+ // ── Helpers ──────────────────────────────────────────────────────
20
+
21
+ /** Format a compact per-task collapsed line. */
22
+ function formatTaskCollapsed(result: ReviewTaskResult, theme: Theme): string {
23
+ if (result.status === "completed") {
24
+ const findingCount = result.findings.length;
25
+ const findingLabel =
26
+ findingCount > 0 ? ` (${findingCount} finding${findingCount !== 1 ? "s" : ""})` : "";
27
+ return `${result.taskId}: ${formatVerdictBadge(result.verdict, theme)}${theme.fg("dim", findingLabel)}`;
28
+ }
29
+ // Non-completed task — show status
30
+ const label = formatStatusLabel(
31
+ result.status,
32
+ result.status === "failed" ? result.failureCode : undefined,
33
+ result.status === "timeout" ? result.timeoutMs : undefined,
34
+ );
35
+ return `${result.taskId}: ${theme.fg("warning", label)}`;
36
+ }
37
+
38
+ // ── renderCall ───────────────────────────────────────────────────
39
+
40
+ export function renderRunCall(args: unknown, theme: Theme): Text {
41
+ const params = (args ?? {}) as {
42
+ mode?: string;
43
+ target?: { kind?: string };
44
+ planId?: string;
45
+ };
46
+ const mode = params.mode ?? "direct";
47
+ const primary = mode;
48
+ const secondary = mode === "prepared" ? "from plan" : (params.target?.kind ?? undefined);
49
+
50
+ return renderReviewToolCall("supi_review_run", primary, theme, secondary);
51
+ }
52
+
53
+ // ── renderResult ─────────────────────────────────────────────────
54
+
55
+ export function renderRunResult(
56
+ result: {
57
+ content?: Array<{ type: string; text?: string }>;
58
+ details?: unknown;
59
+ isError?: boolean;
60
+ },
61
+ options: { expanded: boolean; isPartial: boolean },
62
+ theme: Theme,
63
+ ): Container | Text {
64
+ if (options.isPartial) {
65
+ const details = result.details as { completedCount?: number; totalCount?: number } | undefined;
66
+ const completed = details?.completedCount ?? 0;
67
+ const total = details?.totalCount ?? 0;
68
+ const label = total > 0 ? `Reviewing… (${completed} of ${total} tasks complete)` : "Reviewing…";
69
+ return renderPartial(label, theme);
70
+ }
71
+
72
+ const details = result.details as ReviewBatchDetails | undefined;
73
+
74
+ if (result.isError || !details) {
75
+ return renderError("supi_review_run failed", theme);
76
+ }
77
+
78
+ if (!options.expanded) {
79
+ return buildCollapsed(details, theme);
80
+ }
81
+
82
+ return buildExpanded(details, theme);
83
+ }
84
+
85
+ // ── Collapsed ────────────────────────────────────────────────────
86
+
87
+ function buildCollapsed(details: ReviewBatchDetails, theme: Theme): Container {
88
+ const taskLines = details.results.map((r) => formatTaskCollapsed(r, theme));
89
+ const fileCount = details.snapshot.changedFiles.length;
90
+
91
+ const container = new Container();
92
+ // Per-task verdicts
93
+ container.addChild(new Text(taskLines.join(` ${theme.fg("dim", "·")} `), 0, 0));
94
+ // Target summary
95
+ container.addChild(
96
+ new Text(
97
+ theme.fg("dim", `${details.snapshot.title} (${fileCount} file${fileCount !== 1 ? "s" : ""})`),
98
+ 0,
99
+ 0,
100
+ ),
101
+ );
102
+ return container;
103
+ }
104
+
105
+ // ── Expanded ─────────────────────────────────────────────────────
106
+
107
+ function buildExpanded(details: ReviewBatchDetails, theme: Theme): Container {
108
+ const container = new Container();
109
+
110
+ // Header
111
+ const provenanceLabel =
112
+ details.provenance === "planner-assisted"
113
+ ? theme.fg("muted", "(planner-assisted)")
114
+ : theme.fg("dim", "(caller-supplied)");
115
+ container.addChild(
116
+ new Text(`${theme.fg("accent", theme.bold("Review Complete"))} ${provenanceLabel}`, 1, 0),
117
+ );
118
+ container.addChild(new Spacer(1));
119
+
120
+ // Preamble: mode + target
121
+ container.addChild(
122
+ new Text(`${theme.fg("dim", "mode:")} ${theme.fg("muted", details.mode)}`, 1, 0),
123
+ );
124
+ container.addChild(
125
+ new Text(`${theme.fg("dim", "target:")} ${theme.fg("muted", details.snapshot.title)}`, 1, 0),
126
+ );
127
+ const fileCount = details.snapshot.changedFiles.length;
128
+ container.addChild(
129
+ new Text(
130
+ theme.fg(
131
+ "dim",
132
+ `${fileCount} file${fileCount !== 1 ? "s" : ""} changed · +${details.snapshot.stats.additions} / -${details.snapshot.stats.deletions}`,
133
+ ),
134
+ 1,
135
+ 0,
136
+ ),
137
+ );
138
+
139
+ // Planning info
140
+ if (details.planning) {
141
+ container.addChild(new Spacer(1));
142
+ container.addChild(
143
+ new Text(
144
+ `${theme.fg("dim", "planner:")} ${theme.fg("muted", details.planning.modelId)} · ${theme.fg("dim", "decision:")} ${theme.fg("muted", details.planning.decision)}`,
145
+ 1,
146
+ 0,
147
+ ),
148
+ );
149
+ }
150
+
151
+ // Per-task sections
152
+ for (const result of details.results) {
153
+ container.addChild(new Spacer(1));
154
+ container.addChild(new Text(theme.fg("accent", theme.bold(result.taskId)), 1, 0));
155
+ container.addChild(new Text(theme.fg("dim", "─".repeat(40)), 1, 0));
156
+ buildTaskSection(container, result, theme);
157
+ }
158
+
159
+ return container;
160
+ }