@mrclrchtr/supi-review 2.7.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 (49) hide show
  1. package/README.md +75 -163
  2. package/node_modules/@mrclrchtr/supi-core/README.md +1 -1
  3. package/node_modules/@mrclrchtr/supi-core/package.json +2 -1
  4. package/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +40 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +1 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +14 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +31 -14
  9. package/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
  10. package/package.json +3 -3
  11. package/src/config.ts +64 -0
  12. package/src/git-command.ts +78 -0
  13. package/src/git.ts +306 -505
  14. package/src/history/collect.ts +43 -120
  15. package/src/model.ts +35 -0
  16. package/src/review-path.ts +85 -0
  17. package/src/review-result.ts +43 -92
  18. package/src/review.ts +154 -288
  19. package/src/session/review-plan-store.ts +20 -51
  20. package/src/target/packet.ts +46 -369
  21. package/src/tool/agent-review-schemas.ts +101 -104
  22. package/src/tool/agent-review-tools.ts +269 -301
  23. package/src/tool/child-failure-diagnostics.ts +1 -1
  24. package/src/tool/child-resource-loader.ts +37 -0
  25. package/src/tool/child-session-runner.ts +83 -0
  26. package/src/tool/output-page.ts +47 -0
  27. package/src/tool/planner-runner.ts +69 -0
  28. package/src/tool/review-runner.ts +42 -257
  29. package/src/tool/review-system-prompt.ts +12 -110
  30. package/src/tool/review-tools.ts +119 -0
  31. package/src/tool/review-workflow.ts +309 -0
  32. package/src/tool/runner-helpers.ts +3 -8
  33. package/src/tool/schemas.ts +75 -62
  34. package/src/tui/common.ts +175 -0
  35. package/src/tui/prepare.ts +132 -0
  36. package/src/tui/run.ts +160 -0
  37. package/src/types.ts +153 -275
  38. package/src/history/synthesize.ts +0 -121
  39. package/src/tool/agent-review-workflow.ts +0 -398
  40. package/src/tool/brief-runner.ts +0 -180
  41. package/src/tool/guidance.ts +0 -21
  42. package/src/tool/review-handlers.ts +0 -314
  43. package/src/tool/snapshot-tools.ts +0 -117
  44. package/src/ui/flow.ts +0 -189
  45. package/src/ui/format-content.ts +0 -143
  46. package/src/ui/renderer.ts +0 -274
  47. package/src/ui/review-plan-inspector.ts +0 -391
  48. package/src/ui/review-tool-format.ts +0 -138
  49. 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
+ }