@mrclrchtr/supi-review 2.8.0 → 3.0.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 (44) 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 +306 -505
  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 +154 -290
  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 +101 -104
  17. package/src/tool/agent-review-tools.ts +268 -309
  18. package/src/tool/child-failure-diagnostics.ts +1 -1
  19. package/src/tool/child-resource-loader.ts +37 -0
  20. package/src/tool/child-session-runner.ts +83 -0
  21. package/src/tool/output-page.ts +47 -0
  22. package/src/tool/planner-runner.ts +69 -0
  23. package/src/tool/review-runner.ts +42 -257
  24. package/src/tool/review-system-prompt.ts +12 -110
  25. package/src/tool/review-tools.ts +119 -0
  26. package/src/tool/review-workflow.ts +309 -0
  27. package/src/tool/runner-helpers.ts +3 -8
  28. package/src/tool/schemas.ts +75 -62
  29. package/src/tui/common.ts +175 -0
  30. package/src/tui/prepare.ts +132 -0
  31. package/src/tui/run.ts +160 -0
  32. package/src/types.ts +153 -275
  33. package/src/history/synthesize.ts +0 -121
  34. package/src/tool/agent-review-workflow.ts +0 -398
  35. package/src/tool/brief-runner.ts +0 -180
  36. package/src/tool/guidance.ts +0 -21
  37. package/src/tool/review-handlers.ts +0 -314
  38. package/src/tool/snapshot-tools.ts +0 -117
  39. package/src/ui/flow.ts +0 -189
  40. package/src/ui/format-content.ts +0 -143
  41. package/src/ui/renderer.ts +0 -274
  42. package/src/ui/review-plan-inspector.ts +0 -391
  43. package/src/ui/review-tool-format.ts +0 -138
  44. package/src/ui/review-tool-renderer.ts +0 -234
@@ -0,0 +1,175 @@
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
+ FindingEffort,
13
+ FindingImpact,
14
+ ReviewFinding,
15
+ ReviewTaskResult,
16
+ TaskVerdict,
17
+ } from "../types.ts";
18
+
19
+ // ── Spinner state ────────────────────────────────────────────────
20
+
21
+ /** Progress indicator shown while the tool is still running. */
22
+ export function renderPartial(label: string, theme: Theme): Text {
23
+ return new Text(theme.fg("warning", `● ${label}`), 0, 0);
24
+ }
25
+
26
+ /** Error state when the tool fails. */
27
+ export function renderError(label: string, theme: Theme): Text {
28
+ return new Text(theme.fg("error", label), 0, 0);
29
+ }
30
+
31
+ // ── Verdict / status badges ──────────────────────────────────────
32
+
33
+ /** Colored verdict label for PASS or ISSUES. */
34
+ export function formatVerdictBadge(verdict: TaskVerdict, theme: Theme): string {
35
+ return verdict === "pass"
36
+ ? theme.fg("success", theme.bold("PASS"))
37
+ : theme.fg("error", theme.bold("ISSUES"));
38
+ }
39
+
40
+ /** Colored status label for non-completed task results. */
41
+ export function formatStatusLabel(
42
+ status: "failed" | "canceled" | "timeout",
43
+ failureCode?: string,
44
+ timeoutMs?: number,
45
+ ): string {
46
+ switch (status) {
47
+ case "failed":
48
+ return `FAILED${failureCode ? ` (${failureCode})` : ""}`;
49
+ case "canceled":
50
+ return "CANCELED";
51
+ case "timeout":
52
+ return `TIMEOUT (${timeoutMs} ms)`;
53
+ }
54
+ }
55
+
56
+ // ── Finding badges ───────────────────────────────────────────────
57
+
58
+ const IMPACT_LABEL: Record<FindingImpact, string> = {
59
+ low: "low",
60
+ medium: "medium",
61
+ high: "high",
62
+ };
63
+
64
+ const EFFORT_LABEL: Record<FindingEffort, string> = {
65
+ small: "small",
66
+ medium: "medium",
67
+ large: "large",
68
+ };
69
+
70
+ /** Build a compact finding summary line with impact, effort, and confidence badges. */
71
+ export function formatFindingLine(finding: ReviewFinding, theme: Theme): string {
72
+ const blocking = finding.blocksAcceptance
73
+ ? theme.fg("error", "BLOCKING")
74
+ : theme.fg("muted", "non-blocking");
75
+ const impact = theme.fg("dim", `impact: ${IMPACT_LABEL[finding.impact]}`);
76
+ const effort = theme.fg("dim", `effort: ${EFFORT_LABEL[finding.effort]}`);
77
+ const confidence = theme.fg("dim", `confidence: ${finding.confidence.toFixed(1)}`);
78
+ return `[${blocking}] ${theme.fg("accent", finding.title)} · ${impact} · ${effort} · ${confidence}`;
79
+ }
80
+
81
+ // ── Tool call rendering ──────────────────────────────────────────
82
+
83
+ /** Compact single-line renderCall shared by both review tools. */
84
+ export function renderReviewToolCall(
85
+ name: string,
86
+ primary: string,
87
+ theme: Theme,
88
+ secondary?: string,
89
+ ): Text {
90
+ let content = theme.fg("toolTitle", name);
91
+ if (primary) {
92
+ content += ` ${theme.fg("accent", primary)}`;
93
+ }
94
+ if (secondary) {
95
+ content += theme.fg("dim", ` — ${secondary}`);
96
+ }
97
+ return new Text(content, 0, 0);
98
+ }
99
+
100
+ // ── Task section (expanded) ──────────────────────────────────────
101
+
102
+ /** Render a single finding's details into the container. */
103
+ function renderFinding(container: Container, finding: ReviewFinding, theme: Theme): void {
104
+ container.addChild(new Text(formatFindingLine(finding, theme), 1, 0));
105
+ if (finding.location) {
106
+ container.addChild(
107
+ new Text(
108
+ theme.fg(
109
+ "dim",
110
+ ` ${finding.location.path}:${finding.location.startLine}-${finding.location.endLine}`,
111
+ ),
112
+ 1,
113
+ 0,
114
+ ),
115
+ );
116
+ }
117
+ container.addChild(new Text(theme.fg("text", ` ${finding.description}`), 1, 0));
118
+ container.addChild(new Spacer(1));
119
+ }
120
+
121
+ /** Build an expanded-view section for a single review task result. */
122
+ export function buildTaskSection(
123
+ container: Container,
124
+ result: ReviewTaskResult,
125
+ theme: Theme,
126
+ ): void {
127
+ // Header
128
+ const verdictOrStatus =
129
+ result.status === "completed"
130
+ ? formatVerdictBadge(result.verdict, theme)
131
+ : theme.fg(
132
+ "warning",
133
+ formatStatusLabel(
134
+ result.status,
135
+ result.status === "failed" ? result.failureCode : undefined,
136
+ result.status === "timeout" ? result.timeoutMs : undefined,
137
+ ),
138
+ );
139
+
140
+ const findingCount =
141
+ result.status === "completed"
142
+ ? formatEvidenceBadge({
143
+ shownCount: result.findings.length,
144
+ totalCount: result.findings.length,
145
+ omittedCount: 0,
146
+ partialReason: null,
147
+ label: result.findings.length === 1 ? "finding" : "findings",
148
+ })
149
+ : "";
150
+
151
+ const headerParts = [verdictOrStatus];
152
+ if (findingCount) headerParts.push(theme.fg("muted", findingCount));
153
+ container.addChild(new Text(headerParts.join(" "), 1, 0));
154
+
155
+ const metaParts = [
156
+ theme.fg("dim", `model: ${result.modelId}`),
157
+ theme.fg("dim", `hash: ${result.packetHash.slice(0, 12)}…`),
158
+ ];
159
+ container.addChild(new Text(metaParts.join(" "), 1, 0));
160
+
161
+ // Completed task — show summary and findings
162
+ if (result.status !== "completed") return;
163
+
164
+ if (result.summary) {
165
+ container.addChild(new Spacer(1));
166
+ container.addChild(new Text(theme.fg("muted", result.summary), 1, 0));
167
+ }
168
+
169
+ if (result.findings.length === 0) return;
170
+
171
+ container.addChild(new Spacer(1));
172
+ for (const finding of result.findings) {
173
+ renderFinding(container, finding, theme);
174
+ }
175
+ }
@@ -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
+ }