supipowers 1.2.6 → 1.5.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/README.md +118 -56
- package/bin/install.ts +48 -128
- package/package.json +11 -3
- package/skills/code-review/SKILL.md +137 -40
- package/skills/context-mode/SKILL.md +67 -56
- package/skills/creating-supi-agents/SKILL.md +204 -0
- package/skills/debugging/SKILL.md +86 -40
- package/skills/fix-pr/SKILL.md +96 -65
- package/skills/planning/SKILL.md +103 -46
- package/skills/qa-strategy/SKILL.md +68 -46
- package/skills/receiving-code-review/SKILL.md +60 -53
- package/skills/release/SKILL.md +111 -39
- package/skills/tdd/SKILL.md +118 -67
- package/skills/verification/SKILL.md +71 -37
- package/src/bootstrap.ts +27 -5
- package/src/commands/agents.ts +249 -0
- package/src/commands/ai-review.ts +1113 -0
- package/src/commands/config.ts +224 -95
- package/src/commands/doctor.ts +19 -13
- package/src/commands/fix-pr.ts +8 -11
- package/src/commands/generate.ts +200 -0
- package/src/commands/model-picker.ts +5 -15
- package/src/commands/model.ts +4 -5
- package/src/commands/optimize-context.ts +202 -0
- package/src/commands/plan.ts +148 -92
- package/src/commands/qa.ts +14 -23
- package/src/commands/release.ts +504 -275
- package/src/commands/review.ts +643 -86
- package/src/commands/status.ts +44 -17
- package/src/commands/supi.ts +69 -41
- package/src/commands/update.ts +57 -2
- package/src/config/defaults.ts +6 -39
- package/src/config/loader.ts +388 -40
- package/src/config/model-resolver.ts +26 -22
- package/src/config/schema.ts +113 -48
- package/src/context/analyzer.ts +61 -2
- package/src/context/optimizer.ts +199 -0
- package/src/context-mode/compressor.ts +14 -11
- package/src/context-mode/detector.ts +16 -54
- package/src/context-mode/event-extractor.ts +45 -16
- package/src/context-mode/event-store.ts +225 -16
- package/src/context-mode/hooks.ts +195 -22
- package/src/context-mode/knowledge/chunker.ts +235 -0
- package/src/context-mode/knowledge/store.ts +187 -0
- package/src/context-mode/routing.ts +12 -23
- package/src/context-mode/sandbox/executor.ts +183 -0
- package/src/context-mode/sandbox/runners.ts +40 -0
- package/src/context-mode/snapshot-builder.ts +243 -7
- package/src/context-mode/tools.ts +440 -0
- package/src/context-mode/web/fetcher.ts +117 -0
- package/src/context-mode/web/html-to-md.ts +293 -0
- package/src/debug/logger.ts +107 -0
- package/src/deps/registry.ts +0 -20
- package/src/docs/drift.ts +454 -0
- package/src/fix-pr/fetch-comments.ts +66 -0
- package/src/git/commit-msg.ts +2 -1
- package/src/git/commit.ts +123 -141
- package/src/git/conventions.ts +2 -2
- package/src/git/status.ts +4 -1
- package/src/lsp/bridge.ts +138 -12
- package/src/planning/approval-flow.ts +125 -19
- package/src/planning/plan-writer-prompt.ts +4 -11
- package/src/planning/planning-ask-tool.ts +81 -0
- package/src/planning/prompt-builder.ts +9 -169
- package/src/planning/system-prompt.ts +290 -0
- package/src/platform/omp.ts +50 -4
- package/src/platform/progress.ts +182 -0
- package/src/platform/test-utils.ts +4 -1
- package/src/platform/tui-colors.ts +30 -0
- package/src/platform/types.ts +1 -0
- package/src/qa/detect-app-type.ts +102 -0
- package/src/qa/discover-routes.ts +353 -0
- package/src/quality/ai-session.ts +96 -0
- package/src/quality/ai-setup.ts +86 -0
- package/src/quality/gates/ai-review.ts +129 -0
- package/src/quality/gates/build.ts +8 -0
- package/src/quality/gates/command.ts +150 -0
- package/src/quality/gates/format.ts +28 -0
- package/src/quality/gates/lint.ts +22 -0
- package/src/quality/gates/lsp-diagnostics.ts +84 -0
- package/src/quality/gates/test-suite.ts +8 -0
- package/src/quality/gates/typecheck.ts +22 -0
- package/src/quality/registry.ts +25 -0
- package/src/quality/review-gates.ts +33 -0
- package/src/quality/runner.ts +268 -0
- package/src/quality/schemas.ts +48 -0
- package/src/quality/setup.ts +227 -0
- package/src/release/changelog.ts +7 -3
- package/src/release/channels/custom.ts +43 -0
- package/src/release/channels/gitea.ts +35 -0
- package/src/release/channels/github.ts +35 -0
- package/src/release/channels/gitlab.ts +35 -0
- package/src/release/channels/registry.ts +52 -0
- package/src/release/channels/types.ts +27 -0
- package/src/release/detector.ts +10 -63
- package/src/release/executor.ts +61 -51
- package/src/release/prompt.ts +38 -38
- package/src/release/version.ts +129 -10
- package/src/review/agent-loader.ts +331 -0
- package/src/review/consolidator.ts +180 -0
- package/src/review/default-agents/correctness.md +72 -0
- package/src/review/default-agents/maintainability.md +64 -0
- package/src/review/default-agents/security.md +67 -0
- package/src/review/fixer.ts +219 -0
- package/src/review/multi-agent-runner.ts +135 -0
- package/src/review/output.ts +147 -0
- package/src/review/prompts/agent-review-wrapper.md +36 -0
- package/src/review/prompts/fix-findings.md +32 -0
- package/src/review/prompts/fix-output-schema.md +18 -0
- package/src/review/prompts/invalid-output-retry.md +22 -0
- package/src/review/prompts/output-instructions.md +14 -0
- package/src/review/prompts/review-output-schema.md +38 -0
- package/src/review/prompts/single-review.md +53 -0
- package/src/review/prompts/validation-review.md +30 -0
- package/src/review/runner.ts +128 -0
- package/src/review/scope.ts +353 -0
- package/src/review/template.ts +15 -0
- package/src/review/types.ts +296 -0
- package/src/review/validator.ts +160 -0
- package/src/storage/plans.ts +5 -3
- package/src/storage/reports.ts +50 -7
- package/src/storage/review-sessions.ts +117 -0
- package/src/text.ts +19 -0
- package/src/types.ts +336 -26
- package/src/utils/paths.ts +39 -0
- package/src/visual/companion.ts +5 -3
- package/src/visual/start-server.ts +101 -0
- package/src/visual/stop-server.ts +39 -0
- package/bin/ctx-mode-wrapper.mjs +0 -66
- package/src/config/profiles.ts +0 -64
- package/src/context-mode/installer.ts +0 -38
- package/src/quality/ai-review-gate.ts +0 -43
- package/src/quality/gate-runner.ts +0 -67
- package/src/quality/lsp-gate.ts +0 -24
- package/src/quality/test-gate.ts +0 -39
- package/src/visual/scripts/start-server.sh +0 -98
- package/src/visual/scripts/stop-server.sh +0 -21
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
import { matchesKey, truncateToWidth, wrapTextWithAnsi, type Component, type Focusable } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import type { Platform } from "../platform/types.js";
|
|
3
|
+
import { createWorkflowProgress } from "../platform/progress.js";
|
|
4
|
+
import { notifyInfo } from "../notifications/renderer.js";
|
|
5
|
+
import { modelRegistry } from "../config/model-registry-instance.js";
|
|
6
|
+
import { createModelBridge, resolveModelForAction } from "../config/model-resolver.js";
|
|
7
|
+
import { loadModelConfig } from "../config/model-config.js";
|
|
8
|
+
import { selectReviewScope } from "../review/scope.js";
|
|
9
|
+
import { loadMergedReviewAgents } from "../review/agent-loader.js";
|
|
10
|
+
import { runQuickReview, runDeepReview } from "../review/runner.js";
|
|
11
|
+
import { runMultiAgentReview, type MultiAgentAgentResult } from "../review/multi-agent-runner.js";
|
|
12
|
+
import { validateReviewFindings } from "../review/validator.js";
|
|
13
|
+
import { consolidateReviewOutputs } from "../review/consolidator.js";
|
|
14
|
+
import { compareReviewOutputs, runAutoFix } from "../review/fixer.js";
|
|
15
|
+
import {
|
|
16
|
+
createReviewSession,
|
|
17
|
+
generateReviewSessionId,
|
|
18
|
+
updateReviewSession,
|
|
19
|
+
writeReviewArtifact,
|
|
20
|
+
} from "../storage/review-sessions.js";
|
|
21
|
+
import { accent, bright, muted } from "../platform/tui-colors.js";
|
|
22
|
+
import type {
|
|
23
|
+
ConfiguredReviewAgent,
|
|
24
|
+
ReviewFinding,
|
|
25
|
+
ReviewLevel,
|
|
26
|
+
ReviewOutput,
|
|
27
|
+
ReviewPostConsolidationAction,
|
|
28
|
+
ReviewScope,
|
|
29
|
+
ReviewSession,
|
|
30
|
+
} from "../types.js";
|
|
31
|
+
|
|
32
|
+
modelRegistry.register({
|
|
33
|
+
id: "ai-review",
|
|
34
|
+
category: "command",
|
|
35
|
+
label: "AI Review",
|
|
36
|
+
harnessRoleHint: "slow",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const ITERATIONS_DIR = "iterations";
|
|
40
|
+
const AGENTS_DIR = "agents";
|
|
41
|
+
const RAW_FINDINGS_FILE = "findings-raw.json";
|
|
42
|
+
const VALIDATED_FINDINGS_FILE = "findings-validated.json";
|
|
43
|
+
const CONSOLIDATED_FINDINGS_FILE = "findings-consolidated.json";
|
|
44
|
+
const FINDINGS_REPORT_FILE = "findings.md";
|
|
45
|
+
|
|
46
|
+
const REVIEW_RESULTS_ACTIONS: Array<{
|
|
47
|
+
value: ReviewPostConsolidationAction;
|
|
48
|
+
label: string;
|
|
49
|
+
description: string;
|
|
50
|
+
}> = [
|
|
51
|
+
{
|
|
52
|
+
value: "fix-now",
|
|
53
|
+
label: "Fix now",
|
|
54
|
+
description: "Apply safe automatic fixes now, then offer a review loop that re-runs the review and refreshes findings.md.",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
value: "document-only",
|
|
58
|
+
label: "Document only",
|
|
59
|
+
description: "Keep the validated findings as findings.md, save the session, and finish without changing code.",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
value: "discuss-before-fixing",
|
|
63
|
+
label: "Discuss before fixing",
|
|
64
|
+
description: "Save findings.md and the session first, then hand off to the active conversation to plan the fixes.",
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const REVIEW_PRIORITY_ORDER = ["P0", "P1", "P2", "P3"] as const;
|
|
69
|
+
const REVIEW_SEVERITY_ORDER = ["error", "warning", "info"] as const;
|
|
70
|
+
|
|
71
|
+
interface ReviewResultsSummary {
|
|
72
|
+
statusLine: string;
|
|
73
|
+
summaryLine: string;
|
|
74
|
+
severityLine: string;
|
|
75
|
+
priorityLine: string;
|
|
76
|
+
processingLine: string;
|
|
77
|
+
reportPathLine: string;
|
|
78
|
+
topFindings: string[];
|
|
79
|
+
lines: string[];
|
|
80
|
+
helpText: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatFindingLocation(finding: ReviewFinding): string {
|
|
84
|
+
if (!finding.file) {
|
|
85
|
+
return "unknown location";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!finding.lineStart) {
|
|
89
|
+
return finding.file;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return finding.lineEnd && finding.lineEnd !== finding.lineStart
|
|
93
|
+
? `${finding.file}:${finding.lineStart}-${finding.lineEnd}`
|
|
94
|
+
: `${finding.file}:${finding.lineStart}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sortFindingsForDisplay(findings: ReviewFinding[]): ReviewFinding[] {
|
|
98
|
+
return [...findings].sort((left, right) => {
|
|
99
|
+
const priorityDelta = REVIEW_PRIORITY_ORDER.indexOf(left.priority) - REVIEW_PRIORITY_ORDER.indexOf(right.priority);
|
|
100
|
+
if (priorityDelta !== 0) {
|
|
101
|
+
return priorityDelta;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const severityDelta = REVIEW_SEVERITY_ORDER.indexOf(left.severity) - REVIEW_SEVERITY_ORDER.indexOf(right.severity);
|
|
105
|
+
if (severityDelta !== 0) {
|
|
106
|
+
return severityDelta;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const fileDelta = (left.file ?? "").localeCompare(right.file ?? "");
|
|
110
|
+
if (fileDelta !== 0) {
|
|
111
|
+
return fileDelta;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (left.lineStart ?? Number.MAX_SAFE_INTEGER) - (right.lineStart ?? Number.MAX_SAFE_INTEGER);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatConfidence(confidence: number): string {
|
|
119
|
+
return `${Math.round(confidence * 100)}%`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildCountLine<T extends string>(label: string, values: readonly T[], counts: Map<T, number>): string {
|
|
123
|
+
return `${label}: ${values.map((value) => `${value} ${counts.get(value) ?? 0}`).join(", ")}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function preserveBlockedReviewStatus(
|
|
127
|
+
output: ReviewOutput,
|
|
128
|
+
upstreamStatus: ReviewOutput["status"],
|
|
129
|
+
): ReviewOutput {
|
|
130
|
+
if (upstreamStatus !== "blocked" || output.status === "blocked") {
|
|
131
|
+
return output;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const summary = output.summary.includes("Some specialist agents were blocked during review.")
|
|
135
|
+
? output.summary
|
|
136
|
+
: `${output.summary} Some specialist agents were blocked during review.`;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
...output,
|
|
140
|
+
status: "blocked",
|
|
141
|
+
summary,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function prepareReviewOutputForFollowUp(
|
|
146
|
+
output: ReviewOutput,
|
|
147
|
+
upstreamStatus: ReviewOutput["status"],
|
|
148
|
+
): ReviewOutput {
|
|
149
|
+
const normalizedOutput = preserveBlockedReviewStatus(output, upstreamStatus);
|
|
150
|
+
const findings = normalizedOutput.findings.filter((finding) => finding.validation?.verdict !== "rejected");
|
|
151
|
+
if (findings.length === normalizedOutput.findings.length) {
|
|
152
|
+
return normalizedOutput;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const removedCount = normalizedOutput.findings.length - findings.length;
|
|
156
|
+
const summary = `${normalizedOutput.summary} Removed ${removedCount} rejected finding${removedCount === 1 ? "" : "s"} from follow-up.`;
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
...normalizedOutput,
|
|
160
|
+
findings,
|
|
161
|
+
summary,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
function buildReviewFindingsMarkdown(
|
|
167
|
+
output: ReviewOutput,
|
|
168
|
+
session: ReviewSession,
|
|
169
|
+
options: { preFixSnapshot?: boolean } = {},
|
|
170
|
+
): string {
|
|
171
|
+
const sortedFindings = sortFindingsForDisplay(output.findings);
|
|
172
|
+
|
|
173
|
+
const sections = [
|
|
174
|
+
"# supi:review findings",
|
|
175
|
+
"",
|
|
176
|
+
`- Session: \`${session.id}\``,
|
|
177
|
+
`- Scope: ${session.scope.description}`,
|
|
178
|
+
`- Status: ${output.status}`,
|
|
179
|
+
`- Findings: ${output.findings.length}`,
|
|
180
|
+
`- Validation: ${session.validateFindings ? "done" : "skipped"}`,
|
|
181
|
+
`- Consolidation: ${session.consolidate ? "done" : "skipped"}`,
|
|
182
|
+
...(options.preFixSnapshot
|
|
183
|
+
? [
|
|
184
|
+
`- Snapshot: pre-fix review output`,
|
|
185
|
+
"",
|
|
186
|
+
"## Snapshot",
|
|
187
|
+
"",
|
|
188
|
+
"This report captures the last review pass before automatic fixes changed the working tree.",
|
|
189
|
+
"Run the review loop to verify the current findings after those fixes.",
|
|
190
|
+
]
|
|
191
|
+
: []),
|
|
192
|
+
"",
|
|
193
|
+
"## Summary",
|
|
194
|
+
"",
|
|
195
|
+
output.summary,
|
|
196
|
+
"",
|
|
197
|
+
"## Findings",
|
|
198
|
+
"",
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
if (sortedFindings.length === 0) {
|
|
202
|
+
sections.push("No findings.");
|
|
203
|
+
return `${sections.join("\n")}\n`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
sortedFindings.forEach((finding, index) => {
|
|
207
|
+
sections.push(`### ${index + 1}. [${finding.priority}/${finding.severity}] ${finding.title}`);
|
|
208
|
+
sections.push("");
|
|
209
|
+
sections.push(`- Location: \`${formatFindingLocation(finding)}\``);
|
|
210
|
+
sections.push(`- Confidence: ${formatConfidence(finding.confidence)}`);
|
|
211
|
+
if (finding.agent) {
|
|
212
|
+
sections.push(`- Agent: ${finding.agent}`);
|
|
213
|
+
}
|
|
214
|
+
if (finding.validation) {
|
|
215
|
+
sections.push(`- Validation: ${finding.validation.verdict} — ${finding.validation.reasoning}`);
|
|
216
|
+
}
|
|
217
|
+
sections.push("");
|
|
218
|
+
sections.push(finding.body);
|
|
219
|
+
if (finding.suggestion) {
|
|
220
|
+
sections.push("");
|
|
221
|
+
sections.push(`Suggested fix: ${finding.suggestion}`);
|
|
222
|
+
}
|
|
223
|
+
sections.push("");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return `${sections.join("\n")}\n`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function buildReviewResultsSummary(
|
|
230
|
+
output: ReviewOutput,
|
|
231
|
+
session: Pick<ReviewSession, "validateFindings" | "consolidate">,
|
|
232
|
+
findingsReportPath: string,
|
|
233
|
+
maxFindings = 3,
|
|
234
|
+
): ReviewResultsSummary {
|
|
235
|
+
const severityCounts = new Map<(typeof REVIEW_SEVERITY_ORDER)[number], number>(
|
|
236
|
+
REVIEW_SEVERITY_ORDER.map((value) => [value, 0]),
|
|
237
|
+
);
|
|
238
|
+
const priorityCounts = new Map<(typeof REVIEW_PRIORITY_ORDER)[number], number>(
|
|
239
|
+
REVIEW_PRIORITY_ORDER.map((value) => [value, 0]),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
for (const finding of output.findings) {
|
|
243
|
+
severityCounts.set(finding.severity, (severityCounts.get(finding.severity) ?? 0) + 1);
|
|
244
|
+
priorityCounts.set(finding.priority, (priorityCounts.get(finding.priority) ?? 0) + 1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const topFindings = sortFindingsForDisplay(output.findings)
|
|
248
|
+
.slice(0, maxFindings)
|
|
249
|
+
.map((finding) => `[${finding.priority}/${finding.severity}] ${finding.title} — ${formatFindingLocation(finding)}`);
|
|
250
|
+
|
|
251
|
+
const statusLine = `Status: ${output.status} • Findings: ${output.findings.length}`;
|
|
252
|
+
const summaryLine = `Summary: ${output.summary}`;
|
|
253
|
+
const severityLine = buildCountLine("Severity", REVIEW_SEVERITY_ORDER, severityCounts);
|
|
254
|
+
const priorityLine = buildCountLine("Priority", REVIEW_PRIORITY_ORDER, priorityCounts);
|
|
255
|
+
const processingLine = [
|
|
256
|
+
`Validation: ${session.validateFindings ? "done" : "skipped"}`,
|
|
257
|
+
`Consolidation: ${session.consolidate ? "done" : "skipped"}`,
|
|
258
|
+
].join(" • ");
|
|
259
|
+
const reportPathLine = `Findings document: ${findingsReportPath}`;
|
|
260
|
+
|
|
261
|
+
const lines = [statusLine, summaryLine, severityLine, priorityLine, processingLine, reportPathLine];
|
|
262
|
+
if (topFindings.length > 0) {
|
|
263
|
+
lines.push("Top findings:", ...topFindings.map((finding, index) => `${index + 1}. ${finding}`));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
statusLine,
|
|
268
|
+
summaryLine,
|
|
269
|
+
severityLine,
|
|
270
|
+
priorityLine,
|
|
271
|
+
processingLine,
|
|
272
|
+
reportPathLine,
|
|
273
|
+
topFindings,
|
|
274
|
+
lines,
|
|
275
|
+
helpText: lines.join("\n"),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function resolveReviewResultsAction(choice: string | null): ReviewPostConsolidationAction | null {
|
|
280
|
+
if (!choice) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return REVIEW_RESULTS_ACTIONS.find((action) => action.label === choice || action.value === choice)?.value ?? null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function wrapScreenLines(lines: string[], width: number): string[] {
|
|
288
|
+
const safeWidth = Math.max(1, width);
|
|
289
|
+
return lines.flatMap((line) => {
|
|
290
|
+
if (line === "") {
|
|
291
|
+
return [""];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const wrapped = wrapTextWithAnsi(line, safeWidth);
|
|
295
|
+
return wrapped.length > 0 ? wrapped : [truncateToWidth(line, safeWidth)];
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
function createReviewResultsApprovalScreen(
|
|
301
|
+
tui: any,
|
|
302
|
+
done: (result: ReviewPostConsolidationAction | null) => void,
|
|
303
|
+
summary: ReviewResultsSummary,
|
|
304
|
+
): Component & Focusable & { dispose(): void } {
|
|
305
|
+
let selectedIndex = 0;
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
focused: true,
|
|
309
|
+
|
|
310
|
+
dispose(): void {
|
|
311
|
+
// No cleanup required for this ephemeral screen.
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
invalidate(): void {
|
|
315
|
+
// No cached state to reset.
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
handleInput(data: string): void {
|
|
319
|
+
if (matchesKey(data, "escape")) {
|
|
320
|
+
done(null);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (matchesKey(data, "enter")) {
|
|
325
|
+
done(REVIEW_RESULTS_ACTIONS[selectedIndex]?.value ?? null);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (matchesKey(data, "up")) {
|
|
330
|
+
selectedIndex = selectedIndex === 0 ? REVIEW_RESULTS_ACTIONS.length - 1 : selectedIndex - 1;
|
|
331
|
+
tui.requestRender();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (matchesKey(data, "down")) {
|
|
336
|
+
selectedIndex = selectedIndex === REVIEW_RESULTS_ACTIONS.length - 1 ? 0 : selectedIndex + 1;
|
|
337
|
+
tui.requestRender();
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
render(width: number): string[] {
|
|
342
|
+
const lines = [
|
|
343
|
+
bright("Review results"),
|
|
344
|
+
"",
|
|
345
|
+
...summary.lines,
|
|
346
|
+
"",
|
|
347
|
+
bright("Choose next step"),
|
|
348
|
+
...REVIEW_RESULTS_ACTIONS.map((action, index) =>
|
|
349
|
+
index === selectedIndex
|
|
350
|
+
? accent(`> ${action.label} — ${action.description}`)
|
|
351
|
+
: ` ${action.label} — ${action.description}`
|
|
352
|
+
),
|
|
353
|
+
"",
|
|
354
|
+
muted("↑/↓ select • Enter confirm • Esc cancel"),
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
return wrapScreenLines(lines, width);
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function selectReviewResultsAction(
|
|
363
|
+
ctx: any,
|
|
364
|
+
summary: ReviewResultsSummary,
|
|
365
|
+
): Promise<ReviewPostConsolidationAction | null> {
|
|
366
|
+
if (typeof ctx.ui.custom === "function") {
|
|
367
|
+
const choice = await ctx.ui.custom((_tui: any, _theme: any, _kb: any, done: any) =>
|
|
368
|
+
createReviewResultsApprovalScreen(_tui, done, summary),
|
|
369
|
+
);
|
|
370
|
+
return resolveReviewResultsAction(choice ?? null);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const choice = await ctx.ui.select(
|
|
374
|
+
"Review results",
|
|
375
|
+
REVIEW_RESULTS_ACTIONS.map((action) => action.label),
|
|
376
|
+
{ helpText: summary.helpText },
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
return resolveReviewResultsAction(choice ?? null);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function buildDiscussionPrompt(
|
|
383
|
+
platform: Platform,
|
|
384
|
+
ctx: any,
|
|
385
|
+
session: ReviewSession,
|
|
386
|
+
summary: ReviewResultsSummary,
|
|
387
|
+
): string {
|
|
388
|
+
const sessionDir = buildSavedSessionPath(platform, ctx, session);
|
|
389
|
+
const artifactLines = [
|
|
390
|
+
`- session: ${sessionDir}/session.json`,
|
|
391
|
+
`- scope: ${sessionDir}/${session.artifacts.scope}`,
|
|
392
|
+
`- iterations: ${sessionDir}/${session.artifacts.iterationsDir}/`,
|
|
393
|
+
...(session.artifacts.rawFindings ? [`- raw findings: ${sessionDir}/${session.artifacts.rawFindings}`] : []),
|
|
394
|
+
...(session.artifacts.validatedFindings
|
|
395
|
+
? [`- validated findings: ${sessionDir}/${session.artifacts.validatedFindings}`]
|
|
396
|
+
: []),
|
|
397
|
+
...(session.artifacts.consolidatedFindings
|
|
398
|
+
? [`- consolidated findings: ${sessionDir}/${session.artifacts.consolidatedFindings}`]
|
|
399
|
+
: []),
|
|
400
|
+
...(session.artifacts.findingsReport
|
|
401
|
+
? [`- findings document: ${sessionDir}/${session.artifacts.findingsReport}`]
|
|
402
|
+
: []),
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
const topFindings = summary.topFindings.length > 0
|
|
406
|
+
? summary.topFindings.map((finding, index) => `${index + 1}. ${finding}`).join("\n")
|
|
407
|
+
: "None.";
|
|
408
|
+
|
|
409
|
+
return [
|
|
410
|
+
`Review session ${session.id} is saved and ready for discussion.`,
|
|
411
|
+
"",
|
|
412
|
+
`Scope: ${session.scope.description}`,
|
|
413
|
+
summary.statusLine,
|
|
414
|
+
summary.summaryLine,
|
|
415
|
+
summary.reportPathLine,
|
|
416
|
+
"",
|
|
417
|
+
"Top findings:",
|
|
418
|
+
topFindings,
|
|
419
|
+
"",
|
|
420
|
+
"Saved artifacts:",
|
|
421
|
+
...artifactLines,
|
|
422
|
+
"",
|
|
423
|
+
"Discuss the findings and plan the fixes before changing code. Do not auto-apply fixes yet.",
|
|
424
|
+
].join("\n");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function buildPostConsolidationDetail(action: ReviewPostConsolidationAction): string {
|
|
428
|
+
switch (action) {
|
|
429
|
+
case "fix-now":
|
|
430
|
+
return "fix now";
|
|
431
|
+
case "document-only":
|
|
432
|
+
return "document only";
|
|
433
|
+
case "discuss-before-fixing":
|
|
434
|
+
return "discuss";
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function buildSavedSessionPath(platform: Platform, ctx: any, session: ReviewSession): string {
|
|
439
|
+
return platform.paths.project(ctx.cwd, "reviews", session.id);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function buildActionLabel(action: ReviewPostConsolidationAction | null): string {
|
|
443
|
+
switch (action) {
|
|
444
|
+
case "fix-now":
|
|
445
|
+
return "fix now";
|
|
446
|
+
case "document-only":
|
|
447
|
+
return "document only";
|
|
448
|
+
case "discuss-before-fixing":
|
|
449
|
+
return "discuss before fixing";
|
|
450
|
+
default:
|
|
451
|
+
return "none";
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
interface ReviewIterationResult {
|
|
457
|
+
output: ReviewOutput;
|
|
458
|
+
rawOutput: ReviewOutput;
|
|
459
|
+
agentResults: MultiAgentAgentResult[];
|
|
460
|
+
validatedOutput?: ReviewOutput;
|
|
461
|
+
consolidatedOutput?: ReviewOutput;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export interface AiReviewCommandDependencies {
|
|
465
|
+
loadModelConfig: typeof loadModelConfig;
|
|
466
|
+
createModelBridge: typeof createModelBridge;
|
|
467
|
+
resolveModelForAction: typeof resolveModelForAction;
|
|
468
|
+
selectReviewScope: typeof selectReviewScope;
|
|
469
|
+
loadReviewAgents: typeof loadMergedReviewAgents;
|
|
470
|
+
runQuickReview: typeof runQuickReview;
|
|
471
|
+
runDeepReview: typeof runDeepReview;
|
|
472
|
+
runMultiAgentReview: typeof runMultiAgentReview;
|
|
473
|
+
validateReviewFindings: typeof validateReviewFindings;
|
|
474
|
+
consolidateReviewOutputs: typeof consolidateReviewOutputs;
|
|
475
|
+
runAutoFix: typeof runAutoFix;
|
|
476
|
+
createReviewSession: typeof createReviewSession;
|
|
477
|
+
updateReviewSession: typeof updateReviewSession;
|
|
478
|
+
writeReviewArtifact: typeof writeReviewArtifact;
|
|
479
|
+
generateReviewSessionId: typeof generateReviewSessionId;
|
|
480
|
+
notifyInfo: typeof notifyInfo;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const AI_REVIEW_COMMAND_DEPENDENCIES: AiReviewCommandDependencies = {
|
|
484
|
+
loadModelConfig,
|
|
485
|
+
createModelBridge,
|
|
486
|
+
resolveModelForAction,
|
|
487
|
+
selectReviewScope,
|
|
488
|
+
loadReviewAgents: loadMergedReviewAgents,
|
|
489
|
+
runQuickReview,
|
|
490
|
+
runDeepReview,
|
|
491
|
+
runMultiAgentReview,
|
|
492
|
+
validateReviewFindings,
|
|
493
|
+
consolidateReviewOutputs,
|
|
494
|
+
runAutoFix,
|
|
495
|
+
createReviewSession,
|
|
496
|
+
updateReviewSession,
|
|
497
|
+
writeReviewArtifact,
|
|
498
|
+
generateReviewSessionId,
|
|
499
|
+
notifyInfo,
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
function capitalize(value: string): string {
|
|
503
|
+
return value.slice(0, 1).toUpperCase() + value.slice(1);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function createAiReviewSteps(level: ReviewLevel, agents: ConfiguredReviewAgent[]) {
|
|
507
|
+
return [
|
|
508
|
+
{ key: "scope", label: "Scope discovery" },
|
|
509
|
+
...(level === "multi-agent"
|
|
510
|
+
? agents.map((agent) => ({ key: `agent-${agent.name}`, label: `${capitalize(agent.name)} agent` }))
|
|
511
|
+
: [{ key: "review", label: `${capitalize(level)} review` }]),
|
|
512
|
+
{ key: "validate", label: "Validate findings" },
|
|
513
|
+
{ key: "consolidate", label: "Consolidate" },
|
|
514
|
+
{ key: "review-results", label: "Review results" },
|
|
515
|
+
{ key: "fix", label: "Fix findings" },
|
|
516
|
+
{ key: "rerun", label: "Review loop" },
|
|
517
|
+
{ key: "save", label: "Save session" },
|
|
518
|
+
];
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function truncateDetail(detail: string, maxLength = 64): string {
|
|
522
|
+
const normalized = detail.replace(/\s+/g, " ").trim();
|
|
523
|
+
if (normalized.length <= maxLength) {
|
|
524
|
+
return normalized;
|
|
525
|
+
}
|
|
526
|
+
return `${normalized.slice(0, maxLength - 1)}…`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function createAiReviewProgress(ctx: any, level: ReviewLevel, agents: ConfiguredReviewAgent[]) {
|
|
530
|
+
const progress = createWorkflowProgress(ctx.ui, {
|
|
531
|
+
title: "supi:review",
|
|
532
|
+
statusKey: "supi-ai-review",
|
|
533
|
+
widgetKey: "supi-ai-review",
|
|
534
|
+
steps: createAiReviewSteps(level, agents),
|
|
535
|
+
});
|
|
536
|
+
let activeStep: string | null = null;
|
|
537
|
+
|
|
538
|
+
function activate(stepKey: string, detail?: string) {
|
|
539
|
+
activeStep = stepKey;
|
|
540
|
+
progress.activate(stepKey, detail ? truncateDetail(detail) : undefined);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function complete(stepKey: string, detail?: string) {
|
|
544
|
+
if (activeStep === stepKey) {
|
|
545
|
+
activeStep = null;
|
|
546
|
+
}
|
|
547
|
+
progress.complete(stepKey, detail ? truncateDetail(detail) : undefined);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function skip(stepKey: string, detail: string) {
|
|
551
|
+
if (activeStep === stepKey) {
|
|
552
|
+
activeStep = null;
|
|
553
|
+
}
|
|
554
|
+
progress.skip(stepKey, truncateDetail(detail));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function fail(stepKey: string, detail: string) {
|
|
558
|
+
if (activeStep === stepKey) {
|
|
559
|
+
activeStep = null;
|
|
560
|
+
}
|
|
561
|
+
progress.fail(stepKey, truncateDetail(detail));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
completeScope(scope: ReviewScope) {
|
|
566
|
+
complete("scope", `${scope.stats.filesChanged} changed file(s)`);
|
|
567
|
+
},
|
|
568
|
+
startSingle(levelName: ReviewLevel) {
|
|
569
|
+
activate("review", `${levelName} review`);
|
|
570
|
+
},
|
|
571
|
+
completeSingle(output: ReviewOutput) {
|
|
572
|
+
complete("review", `${output.findings.length} finding(s)`);
|
|
573
|
+
},
|
|
574
|
+
startAgent(agent: ConfiguredReviewAgent) {
|
|
575
|
+
activate(`agent-${agent.name}`, "running");
|
|
576
|
+
},
|
|
577
|
+
completeAgent(result: MultiAgentAgentResult) {
|
|
578
|
+
complete(`agent-${result.agent.name}`, `${result.output.findings.length} finding(s)`);
|
|
579
|
+
},
|
|
580
|
+
skipValidate(reason: string) {
|
|
581
|
+
skip("validate", reason);
|
|
582
|
+
},
|
|
583
|
+
startValidate() {
|
|
584
|
+
activate("validate", "cross-checking code");
|
|
585
|
+
},
|
|
586
|
+
completeValidate(output: ReviewOutput) {
|
|
587
|
+
complete("validate", `${output.findings.length} finding(s)`);
|
|
588
|
+
},
|
|
589
|
+
skipConsolidate(reason: string) {
|
|
590
|
+
skip("consolidate", reason);
|
|
591
|
+
},
|
|
592
|
+
startConsolidate() {
|
|
593
|
+
activate("consolidate", "merging findings");
|
|
594
|
+
},
|
|
595
|
+
completeConsolidate(output: ReviewOutput) {
|
|
596
|
+
complete("consolidate", `${output.findings.length} unique finding(s)`);
|
|
597
|
+
},
|
|
598
|
+
skipReviewResults(reason: string) {
|
|
599
|
+
skip("review-results", reason);
|
|
600
|
+
},
|
|
601
|
+
startReviewResults() {
|
|
602
|
+
activate("review-results", "awaiting approval");
|
|
603
|
+
},
|
|
604
|
+
completeReviewResults(detail: string) {
|
|
605
|
+
complete("review-results", detail);
|
|
606
|
+
},
|
|
607
|
+
skipFix(reason: string) {
|
|
608
|
+
skip("fix", reason);
|
|
609
|
+
},
|
|
610
|
+
startFix() {
|
|
611
|
+
activate("fix", "applying fixes");
|
|
612
|
+
},
|
|
613
|
+
completeFix(summary: string) {
|
|
614
|
+
complete("fix", summary);
|
|
615
|
+
},
|
|
616
|
+
skipRerun(reason: string) {
|
|
617
|
+
skip("rerun", reason);
|
|
618
|
+
},
|
|
619
|
+
startRerun(iteration: number, maxIterations: number) {
|
|
620
|
+
activate("rerun", `iteration ${iteration}/${maxIterations}`);
|
|
621
|
+
},
|
|
622
|
+
completeRerun(iteration: number, maxIterations: number) {
|
|
623
|
+
complete("rerun", `iteration ${iteration}/${maxIterations}`);
|
|
624
|
+
},
|
|
625
|
+
startSave() {
|
|
626
|
+
activate("save", "writing session");
|
|
627
|
+
},
|
|
628
|
+
completeSave(status: ReviewSession["status"]) {
|
|
629
|
+
complete("save", status);
|
|
630
|
+
},
|
|
631
|
+
failActive(detail: string) {
|
|
632
|
+
fail(activeStep ?? "save", detail);
|
|
633
|
+
},
|
|
634
|
+
dispose() {
|
|
635
|
+
progress.dispose();
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function createInitialReviewSession(
|
|
641
|
+
sessionId: string,
|
|
642
|
+
level: ReviewLevel,
|
|
643
|
+
scope: ReviewScope,
|
|
644
|
+
agents: ConfiguredReviewAgent[],
|
|
645
|
+
): ReviewSession {
|
|
646
|
+
const now = new Date().toISOString();
|
|
647
|
+
return {
|
|
648
|
+
id: sessionId,
|
|
649
|
+
createdAt: now,
|
|
650
|
+
updatedAt: now,
|
|
651
|
+
level,
|
|
652
|
+
status: "running",
|
|
653
|
+
scope,
|
|
654
|
+
validateFindings: false,
|
|
655
|
+
consolidate: false,
|
|
656
|
+
postConsolidationAction: null,
|
|
657
|
+
maxIterations: 1,
|
|
658
|
+
currentIteration: 0,
|
|
659
|
+
iterations: [],
|
|
660
|
+
fixes: [],
|
|
661
|
+
artifacts: {
|
|
662
|
+
scope: "scope.json",
|
|
663
|
+
iterationsDir: ITERATIONS_DIR,
|
|
664
|
+
agentsDir: AGENTS_DIR,
|
|
665
|
+
},
|
|
666
|
+
agents: agents.map((agent) => agent.name),
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function selectReviewLevel(ctx: any): Promise<ReviewLevel | null> {
|
|
671
|
+
const choice = await ctx.ui.select(
|
|
672
|
+
"Review level",
|
|
673
|
+
[
|
|
674
|
+
"Quick — fast high-signal review",
|
|
675
|
+
"Deep — thorough single-agent review",
|
|
676
|
+
"Multi-agent — focused specialist agents",
|
|
677
|
+
],
|
|
678
|
+
{ helpText: "Choose the review depth · Esc to cancel" },
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
if (!choice) {
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
if (choice.startsWith("Quick")) {
|
|
685
|
+
return "quick";
|
|
686
|
+
}
|
|
687
|
+
if (choice.startsWith("Deep")) {
|
|
688
|
+
return "deep";
|
|
689
|
+
}
|
|
690
|
+
return "multi-agent";
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function selectYesNo(ctx: any, title: string, helpText: string): Promise<boolean | null> {
|
|
694
|
+
const choice = await ctx.ui.select(title, ["No", "Yes"], { helpText });
|
|
695
|
+
if (!choice) {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
return choice === "Yes";
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function selectMaxIterations(ctx: any, defaultValue = 3): Promise<number | null> {
|
|
702
|
+
const raw = await ctx.ui.input("Max iterations", {
|
|
703
|
+
helpText: `Maximum review/fix cycles (default ${defaultValue}).`,
|
|
704
|
+
placeholder: String(defaultValue),
|
|
705
|
+
});
|
|
706
|
+
if (raw === null || raw === undefined || raw === "") {
|
|
707
|
+
return defaultValue;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const parsed = Number.parseInt(raw, 10);
|
|
711
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
712
|
+
throw new Error("Max iterations must be a positive integer.");
|
|
713
|
+
}
|
|
714
|
+
return parsed;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function buildCompletionDetail(session: ReviewSession, output: ReviewOutput): string {
|
|
718
|
+
return [
|
|
719
|
+
`session: ${session.id}`,
|
|
720
|
+
`status: ${output.status}`,
|
|
721
|
+
`findings: ${output.findings.length}`,
|
|
722
|
+
`iterations: ${session.currentIteration}`,
|
|
723
|
+
`action: ${buildActionLabel(session.postConsolidationAction)}`,
|
|
724
|
+
].join(" | ");
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function writeFindingsReport(
|
|
728
|
+
deps: AiReviewCommandDependencies,
|
|
729
|
+
platform: Platform,
|
|
730
|
+
ctx: any,
|
|
731
|
+
session: ReviewSession,
|
|
732
|
+
output: ReviewOutput,
|
|
733
|
+
options: { preFixSnapshot?: boolean } = {},
|
|
734
|
+
): string {
|
|
735
|
+
const artifactPath = deps.writeReviewArtifact(
|
|
736
|
+
platform.paths,
|
|
737
|
+
ctx.cwd,
|
|
738
|
+
session.id,
|
|
739
|
+
FINDINGS_REPORT_FILE,
|
|
740
|
+
buildReviewFindingsMarkdown(output, session, options),
|
|
741
|
+
);
|
|
742
|
+
session.artifacts.findingsReport = FINDINGS_REPORT_FILE;
|
|
743
|
+
return artifactPath;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function persistIteration(
|
|
747
|
+
deps: AiReviewCommandDependencies,
|
|
748
|
+
platform: Platform,
|
|
749
|
+
ctx: any,
|
|
750
|
+
session: ReviewSession,
|
|
751
|
+
iteration: number,
|
|
752
|
+
output: ReviewOutput,
|
|
753
|
+
extra: Record<string, unknown> = {},
|
|
754
|
+
): void {
|
|
755
|
+
const relativePath = `${ITERATIONS_DIR}/${iteration}.json`;
|
|
756
|
+
deps.writeReviewArtifact(platform.paths, ctx.cwd, session.id, relativePath, {
|
|
757
|
+
output,
|
|
758
|
+
...extra,
|
|
759
|
+
});
|
|
760
|
+
session.iterations.push({
|
|
761
|
+
iteration,
|
|
762
|
+
findings: output.findings.length,
|
|
763
|
+
status: output.status,
|
|
764
|
+
file: relativePath,
|
|
765
|
+
createdAt: new Date().toISOString(),
|
|
766
|
+
});
|
|
767
|
+
session.currentIteration = iteration;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function runReviewPass(
|
|
771
|
+
platform: Platform,
|
|
772
|
+
ctx: any,
|
|
773
|
+
deps: AiReviewCommandDependencies,
|
|
774
|
+
scope: ReviewScope,
|
|
775
|
+
level: ReviewLevel,
|
|
776
|
+
agents: ConfiguredReviewAgent[],
|
|
777
|
+
progress: ReturnType<typeof createAiReviewProgress>,
|
|
778
|
+
resolvedModel: { model: string | undefined; thinkingLevel: string | null },
|
|
779
|
+
): Promise<{ rawOutput: ReviewOutput; agentResults: MultiAgentAgentResult[] }> {
|
|
780
|
+
if (level === "quick") {
|
|
781
|
+
progress.startSingle(level);
|
|
782
|
+
const result = await deps.runQuickReview({
|
|
783
|
+
cwd: ctx.cwd,
|
|
784
|
+
scope,
|
|
785
|
+
createAgentSession: platform.createAgentSession.bind(platform),
|
|
786
|
+
model: resolvedModel.model,
|
|
787
|
+
thinkingLevel: resolvedModel.thinkingLevel,
|
|
788
|
+
});
|
|
789
|
+
progress.completeSingle(result.output);
|
|
790
|
+
return {
|
|
791
|
+
rawOutput: result.output,
|
|
792
|
+
agentResults: [],
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (level === "deep") {
|
|
797
|
+
progress.startSingle(level);
|
|
798
|
+
const result = await deps.runDeepReview({
|
|
799
|
+
cwd: ctx.cwd,
|
|
800
|
+
scope,
|
|
801
|
+
createAgentSession: platform.createAgentSession.bind(platform),
|
|
802
|
+
model: resolvedModel.model,
|
|
803
|
+
thinkingLevel: resolvedModel.thinkingLevel,
|
|
804
|
+
});
|
|
805
|
+
progress.completeSingle(result.output);
|
|
806
|
+
return {
|
|
807
|
+
rawOutput: result.output,
|
|
808
|
+
agentResults: [],
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const result = await deps.runMultiAgentReview({
|
|
813
|
+
cwd: ctx.cwd,
|
|
814
|
+
scope,
|
|
815
|
+
agents,
|
|
816
|
+
createAgentSession: platform.createAgentSession.bind(platform),
|
|
817
|
+
model: resolvedModel.model,
|
|
818
|
+
thinkingLevel: resolvedModel.thinkingLevel,
|
|
819
|
+
onAgentStart: (agent) => progress.startAgent(agent),
|
|
820
|
+
onAgentComplete: (agentResult) => progress.completeAgent(agentResult),
|
|
821
|
+
});
|
|
822
|
+
return {
|
|
823
|
+
rawOutput: result.output,
|
|
824
|
+
agentResults: result.agents,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async function runAiReviewSession(
|
|
829
|
+
platform: Platform,
|
|
830
|
+
ctx: any,
|
|
831
|
+
deps: AiReviewCommandDependencies = AI_REVIEW_COMMAND_DEPENDENCIES,
|
|
832
|
+
): Promise<void> {
|
|
833
|
+
if (!ctx.hasUI) {
|
|
834
|
+
ctx.ui.notify("/supi:review requires interactive mode.", "warning");
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const scope = await deps.selectReviewScope(platform, ctx);
|
|
839
|
+
if (!scope) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const level = await selectReviewLevel(ctx);
|
|
844
|
+
if (!level) {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const loadedAgents = level === "multi-agent"
|
|
849
|
+
? await deps.loadReviewAgents(platform.paths, ctx.cwd)
|
|
850
|
+
: null;
|
|
851
|
+
const agents = loadedAgents?.agents ?? [];
|
|
852
|
+
if (level === "multi-agent" && agents.length === 0) {
|
|
853
|
+
throw new Error("No enabled review agents are configured.");
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const modelConfig = deps.loadModelConfig(platform.paths, ctx.cwd);
|
|
857
|
+
const modelBridge = deps.createModelBridge(platform);
|
|
858
|
+
const resolvedModel = deps.resolveModelForAction("ai-review", modelRegistry, modelConfig, modelBridge);
|
|
859
|
+
|
|
860
|
+
const session = createInitialReviewSession(
|
|
861
|
+
deps.generateReviewSessionId(),
|
|
862
|
+
level,
|
|
863
|
+
scope,
|
|
864
|
+
agents,
|
|
865
|
+
);
|
|
866
|
+
deps.createReviewSession(platform.paths, ctx.cwd, session);
|
|
867
|
+
deps.writeReviewArtifact(platform.paths, ctx.cwd, session.id, session.artifacts.scope, scope);
|
|
868
|
+
|
|
869
|
+
const progress = createAiReviewProgress(ctx, level, agents);
|
|
870
|
+
progress.completeScope(scope);
|
|
871
|
+
|
|
872
|
+
function saveSession(status: ReviewSession["status"]): void {
|
|
873
|
+
progress.startSave();
|
|
874
|
+
session.status = status;
|
|
875
|
+
deps.updateReviewSession(platform.paths, ctx.cwd, session);
|
|
876
|
+
progress.completeSave(status);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
try {
|
|
881
|
+
const initialRun = await runReviewPass(platform, ctx, deps, scope, level, agents, progress, resolvedModel);
|
|
882
|
+
deps.writeReviewArtifact(platform.paths, ctx.cwd, session.id, RAW_FINDINGS_FILE, initialRun.rawOutput);
|
|
883
|
+
session.artifacts.rawFindings = RAW_FINDINGS_FILE;
|
|
884
|
+
for (const agentResult of initialRun.agentResults) {
|
|
885
|
+
deps.writeReviewArtifact(
|
|
886
|
+
platform.paths,
|
|
887
|
+
ctx.cwd,
|
|
888
|
+
session.id,
|
|
889
|
+
`${AGENTS_DIR}/${agentResult.agent.name}.json`,
|
|
890
|
+
agentResult,
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
let currentOutput = initialRun.rawOutput;
|
|
895
|
+
|
|
896
|
+
if (currentOutput.findings.length > 0) {
|
|
897
|
+
session.validateFindings = true;
|
|
898
|
+
progress.startValidate();
|
|
899
|
+
const validation = await deps.validateReviewFindings({
|
|
900
|
+
cwd: ctx.cwd,
|
|
901
|
+
scope,
|
|
902
|
+
findings: currentOutput.findings,
|
|
903
|
+
createAgentSession: platform.createAgentSession.bind(platform),
|
|
904
|
+
model: resolvedModel.model,
|
|
905
|
+
thinkingLevel: resolvedModel.thinkingLevel,
|
|
906
|
+
});
|
|
907
|
+
const validatedOutput = preserveBlockedReviewStatus(validation.output, currentOutput.status);
|
|
908
|
+
currentOutput = prepareReviewOutputForFollowUp(validatedOutput, currentOutput.status);
|
|
909
|
+
deps.writeReviewArtifact(platform.paths, ctx.cwd, session.id, VALIDATED_FINDINGS_FILE, validatedOutput);
|
|
910
|
+
session.artifacts.validatedFindings = VALIDATED_FINDINGS_FILE;
|
|
911
|
+
progress.completeValidate(validatedOutput);
|
|
912
|
+
} else {
|
|
913
|
+
session.validateFindings = false;
|
|
914
|
+
progress.skipValidate("no findings");
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (level === "multi-agent" && currentOutput.findings.length > 0) {
|
|
918
|
+
session.consolidate = true;
|
|
919
|
+
progress.startConsolidate();
|
|
920
|
+
currentOutput = prepareReviewOutputForFollowUp(
|
|
921
|
+
deps.consolidateReviewOutputs([currentOutput]),
|
|
922
|
+
currentOutput.status,
|
|
923
|
+
);
|
|
924
|
+
deps.writeReviewArtifact(platform.paths, ctx.cwd, session.id, CONSOLIDATED_FINDINGS_FILE, currentOutput);
|
|
925
|
+
session.artifacts.consolidatedFindings = CONSOLIDATED_FINDINGS_FILE;
|
|
926
|
+
progress.completeConsolidate(currentOutput);
|
|
927
|
+
} else {
|
|
928
|
+
session.consolidate = false;
|
|
929
|
+
progress.skipConsolidate(level === "multi-agent" ? "no findings" : "single-agent");
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
persistIteration(deps, platform, ctx, session, 1, currentOutput);
|
|
933
|
+
let findingsReportPath = writeFindingsReport(deps, platform, ctx, session, currentOutput);
|
|
934
|
+
let findingsReportIsPreFixSnapshot = false;
|
|
935
|
+
|
|
936
|
+
async function cancelSession(): Promise<void> {
|
|
937
|
+
if (findingsReportIsPreFixSnapshot) {
|
|
938
|
+
findingsReportPath = writeFindingsReport(deps, platform, ctx, session, currentOutput, { preFixSnapshot: true });
|
|
939
|
+
}
|
|
940
|
+
saveSession("cancelled");
|
|
941
|
+
}
|
|
942
|
+
const reviewResultsSummary = buildReviewResultsSummary(currentOutput, session, findingsReportPath);
|
|
943
|
+
|
|
944
|
+
if (currentOutput.findings.length === 0) {
|
|
945
|
+
progress.skipReviewResults("no findings");
|
|
946
|
+
progress.skipFix("no findings");
|
|
947
|
+
progress.skipRerun("no findings");
|
|
948
|
+
} else {
|
|
949
|
+
progress.startReviewResults();
|
|
950
|
+
const action = await selectReviewResultsAction(ctx, reviewResultsSummary);
|
|
951
|
+
if (action === null) {
|
|
952
|
+
await cancelSession();
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
session.postConsolidationAction = action;
|
|
957
|
+
progress.completeReviewResults(buildPostConsolidationDetail(action));
|
|
958
|
+
|
|
959
|
+
if (action === "document-only") {
|
|
960
|
+
progress.skipFix("document only");
|
|
961
|
+
progress.skipRerun("document only");
|
|
962
|
+
saveSession(currentOutput.status === "blocked" ? "blocked" : "completed");
|
|
963
|
+
deps.notifyInfo(
|
|
964
|
+
ctx,
|
|
965
|
+
"AI review documented without fixes",
|
|
966
|
+
`session: ${session.id} | findings: ${currentOutput.findings.length} | report: ${findingsReportPath}`,
|
|
967
|
+
);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (action === "discuss-before-fixing") {
|
|
972
|
+
progress.skipFix("discussion requested");
|
|
973
|
+
progress.skipRerun("discussion requested");
|
|
974
|
+
saveSession(currentOutput.status === "blocked" ? "blocked" : "completed");
|
|
975
|
+
deps.notifyInfo(
|
|
976
|
+
ctx,
|
|
977
|
+
"AI review saved for discussion",
|
|
978
|
+
`session: ${session.id} | findings: ${currentOutput.findings.length} | report: ${findingsReportPath}`,
|
|
979
|
+
);
|
|
980
|
+
platform.sendUserMessage(buildDiscussionPrompt(platform, ctx, session, reviewResultsSummary));
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
progress.startFix();
|
|
985
|
+
const initialFix = await deps.runAutoFix({
|
|
986
|
+
cwd: ctx.cwd,
|
|
987
|
+
scope,
|
|
988
|
+
findings: currentOutput.findings,
|
|
989
|
+
createAgentSession: platform.createAgentSession.bind(platform),
|
|
990
|
+
model: resolvedModel.model,
|
|
991
|
+
thinkingLevel: resolvedModel.thinkingLevel,
|
|
992
|
+
});
|
|
993
|
+
session.fixes.push(...initialFix.output.fixes);
|
|
994
|
+
findingsReportIsPreFixSnapshot = initialFix.output.fixes.some((record) => record.status === "applied");
|
|
995
|
+
progress.completeFix(initialFix.output.status);
|
|
996
|
+
|
|
997
|
+
const reviewLoop = await selectYesNo(
|
|
998
|
+
ctx,
|
|
999
|
+
"Run review loop?",
|
|
1000
|
+
"If you continue, supipowers will re-run the same review after the fixes, validate findings again, refresh the findings.md report, and keep going until the findings are cleared or the iteration limit is reached.",
|
|
1001
|
+
);
|
|
1002
|
+
if (reviewLoop === null) {
|
|
1003
|
+
await cancelSession();
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (reviewLoop) {
|
|
1008
|
+
session.maxIterations = await selectMaxIterations(ctx, 3) ?? 3;
|
|
1009
|
+
let previousOutput = currentOutput;
|
|
1010
|
+
|
|
1011
|
+
for (let iteration = 2; iteration <= session.maxIterations; iteration += 1) {
|
|
1012
|
+
progress.startRerun(iteration, session.maxIterations);
|
|
1013
|
+
const rerun = await runReviewPass(platform, ctx, deps, scope, level, agents, progress, resolvedModel);
|
|
1014
|
+
let rerunOutput = rerun.rawOutput;
|
|
1015
|
+
|
|
1016
|
+
if (rerunOutput.findings.length > 0) {
|
|
1017
|
+
progress.startValidate();
|
|
1018
|
+
const validation = await deps.validateReviewFindings({
|
|
1019
|
+
cwd: ctx.cwd,
|
|
1020
|
+
scope,
|
|
1021
|
+
findings: rerunOutput.findings,
|
|
1022
|
+
createAgentSession: platform.createAgentSession.bind(platform),
|
|
1023
|
+
model: resolvedModel.model,
|
|
1024
|
+
thinkingLevel: resolvedModel.thinkingLevel,
|
|
1025
|
+
});
|
|
1026
|
+
const validatedOutput = preserveBlockedReviewStatus(validation.output, rerunOutput.status);
|
|
1027
|
+
rerunOutput = prepareReviewOutputForFollowUp(validatedOutput, rerunOutput.status);
|
|
1028
|
+
progress.completeValidate(validatedOutput);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if (session.consolidate && rerunOutput.findings.length > 0) {
|
|
1032
|
+
progress.startConsolidate();
|
|
1033
|
+
rerunOutput = prepareReviewOutputForFollowUp(
|
|
1034
|
+
deps.consolidateReviewOutputs([rerunOutput]),
|
|
1035
|
+
rerunOutput.status,
|
|
1036
|
+
);
|
|
1037
|
+
progress.completeConsolidate(rerunOutput);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const delta = compareReviewOutputs(previousOutput, rerunOutput);
|
|
1041
|
+
persistIteration(deps, platform, ctx, session, iteration, rerunOutput, { delta });
|
|
1042
|
+
findingsReportPath = writeFindingsReport(deps, platform, ctx, session, rerunOutput);
|
|
1043
|
+
findingsReportIsPreFixSnapshot = false;
|
|
1044
|
+
progress.completeRerun(iteration, session.maxIterations);
|
|
1045
|
+
|
|
1046
|
+
previousOutput = rerunOutput;
|
|
1047
|
+
|
|
1048
|
+
if (rerunOutput.findings.length === 0) {
|
|
1049
|
+
currentOutput = rerunOutput;
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
progress.startFix();
|
|
1054
|
+
const loopFix = await deps.runAutoFix({
|
|
1055
|
+
cwd: ctx.cwd,
|
|
1056
|
+
scope,
|
|
1057
|
+
findings: rerunOutput.findings,
|
|
1058
|
+
createAgentSession: platform.createAgentSession.bind(platform),
|
|
1059
|
+
model: resolvedModel.model,
|
|
1060
|
+
thinkingLevel: resolvedModel.thinkingLevel,
|
|
1061
|
+
});
|
|
1062
|
+
session.fixes.push(...loopFix.output.fixes);
|
|
1063
|
+
findingsReportIsPreFixSnapshot = loopFix.output.fixes.some((record) => record.status === "applied");
|
|
1064
|
+
progress.completeFix(loopFix.output.status);
|
|
1065
|
+
currentOutput = rerunOutput;
|
|
1066
|
+
}
|
|
1067
|
+
} else {
|
|
1068
|
+
progress.skipRerun("not requested");
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (findingsReportIsPreFixSnapshot) {
|
|
1073
|
+
findingsReportPath = writeFindingsReport(deps, platform, ctx, session, currentOutput, { preFixSnapshot: true });
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
saveSession(currentOutput.status === "blocked" ? "blocked" : "completed");
|
|
1077
|
+
deps.notifyInfo(
|
|
1078
|
+
ctx,
|
|
1079
|
+
`AI review complete: ${findingsReportIsPreFixSnapshot ? "post-fix verification pending" : currentOutput.status}`,
|
|
1080
|
+
`${buildCompletionDetail(session, currentOutput)} | report: ${findingsReportIsPreFixSnapshot ? `${findingsReportPath} (pre-fix snapshot)` : findingsReportPath}`,
|
|
1081
|
+
);
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
session.status = "blocked";
|
|
1084
|
+
deps.updateReviewSession(platform.paths, ctx.cwd, session);
|
|
1085
|
+
progress.failActive((error as Error).message);
|
|
1086
|
+
throw error;
|
|
1087
|
+
} finally {
|
|
1088
|
+
progress.dispose();
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
export function handleAiReview(platform: Platform, ctx: any): void {
|
|
1093
|
+
void runAiReviewSession(platform, ctx, AI_REVIEW_COMMAND_DEPENDENCIES).catch((error) => {
|
|
1094
|
+
ctx.ui.notify(`AI review failed: ${(error as Error).message}`, "error");
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
export async function runAiReviewSessionForTest(
|
|
1099
|
+
platform: Platform,
|
|
1100
|
+
ctx: any,
|
|
1101
|
+
deps: AiReviewCommandDependencies,
|
|
1102
|
+
): Promise<void> {
|
|
1103
|
+
await runAiReviewSession(platform, ctx, deps);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
export function registerAiReviewCommand(platform: Platform): void {
|
|
1107
|
+
platform.registerCommand("supi:review", {
|
|
1108
|
+
description: "Run the AI code review pipeline",
|
|
1109
|
+
async handler(_args: string | undefined, ctx: any) {
|
|
1110
|
+
handleAiReview(platform, ctx);
|
|
1111
|
+
},
|
|
1112
|
+
});
|
|
1113
|
+
}
|