@unifan/pi-review-zh 1.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.
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Target workspace prep — clone (or worktree) the target repository so
3
+ * reviewer children can read its source, history and unchanged files.
4
+ *
5
+ * Real failure (PR #18689): reviewers shared cwd with the plugin repo, so
6
+ * `history-context` and `code-comments` had no relevant code. The plugin now
7
+ * - for PRs: shallow-clones `owner/repo` into a scratch dir (gh first so
8
+ * private repos use the gh credential), checks out the PR head from
9
+ * FETCH_HEAD, and verifies the landed SHA against the diff's head SHA.
10
+ * - for local-git dirty: uses the user's cwd directly (already correct).
11
+ * - for local-git clean vs default branch: uses the user's cwd after
12
+ * `git fetch origin <base>`.
13
+ *
14
+ * The workspace is **read-only by convention**: reviewers are not given
15
+ * write tools and the plugin never modifies it.
16
+ *
17
+ * A failed PR clone is a hard error (not a silent fallback to the user's
18
+ * cwd): a fresh GitHub diff plus a stale local checkout is the #1 false
19
+ * positive source observed in the field (diff@new, files@old).
20
+ */
21
+ import { spawn } from "node:child_process";
22
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
23
+ import { tmpdir } from "node:os";
24
+ import { join } from "node:path";
25
+
26
+ import { parsePrRepo } from "./review-report.js";
27
+
28
+ export type RunCmd = (
29
+ cmd: string,
30
+ args: string[],
31
+ opts: { cwd: string },
32
+ ) => Promise<{ stdout: string; stderr: string; exitCode: number }>;
33
+
34
+ let _runCmd: RunCmd = defaultRunCmd;
35
+ export function setTargetWorkspaceCmd(fn: RunCmd): void {
36
+ _runCmd = fn;
37
+ }
38
+ export function resetTargetWorkspaceCmd(): void {
39
+ _runCmd = defaultRunCmd;
40
+ }
41
+
42
+ async function defaultRunCmd(
43
+ cmd: string,
44
+ args: string[],
45
+ opts: { cwd: string },
46
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
47
+ return new Promise((resolve) => {
48
+ try {
49
+ const child = spawn(cmd, args, { cwd: opts.cwd, stdio: ["ignore", "pipe", "pipe"] });
50
+ let stdout = "";
51
+ let stderr = "";
52
+ child.stdout?.setEncoding("utf-8");
53
+ child.stderr?.setEncoding("utf-8");
54
+ child.stdout?.on("data", (d: string) => (stdout += d));
55
+ child.stderr?.on("data", (d: string) => (stderr += d));
56
+ child.on("error", () => resolve({ stdout, stderr, exitCode: 1 }));
57
+ child.on("close", (code) => resolve({ stdout, stderr, exitCode: code ?? 1 }));
58
+ } catch {
59
+ resolve({ stdout: "", stderr: "spawn failed", exitCode: 1 });
60
+ }
61
+ });
62
+ }
63
+
64
+ /** Allocate a scratch root under the OS tmpdir; created on disk. */
65
+ export function allocateWorkspaceRoot(prefix = "pi-review-ws"): string {
66
+ const root = join(
67
+ tmpdir(),
68
+ `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
69
+ );
70
+ mkdirSync(root, { recursive: true });
71
+ return root;
72
+ }
73
+
74
+ /** Best-effort cleanup; ignores errors. */
75
+ export function removeWorkspaceRoot(path: string): void {
76
+ try {
77
+ rmSync(path, { recursive: true, force: true });
78
+ } catch {
79
+ /* ignore */
80
+ }
81
+ }
82
+
83
+ export interface WorkspaceResult {
84
+ /** Absolute path reviewers should use as cwd. */
85
+ workspacePath: string;
86
+ /** True when `git log` / `blame` will work in the workspace. */
87
+ historyAvailable: boolean;
88
+ /** Optional failure note — when set, reviewers must `skipped` the affected lane. */
89
+ warning?: string;
90
+ /** Whether we cloned (true) or reused the user cwd (false). */
91
+ cloned: boolean;
92
+ /** HEAD SHA landed in the workspace, when determinable. */
93
+ workspaceHeadSha?: string;
94
+ }
95
+
96
+ /**
97
+ * Resolve the right workspace for a given target. `cwd` is the user's cwd.
98
+ * Pure function with side effects limited to fs + git/gh subprocesses.
99
+ */
100
+ export async function prepareWorkspace(input: {
101
+ cwd: string;
102
+ target: {
103
+ kind: "pr" | "diff-file" | "local-git";
104
+ prRef?: string;
105
+ /** Diff-side head SHA to verify the checkout against. */
106
+ expectedHeadSha?: string;
107
+ };
108
+ }): Promise<WorkspaceResult> {
109
+ const { cwd, target } = input;
110
+
111
+ if (target.kind === "local-git") {
112
+ // Reuse the user's cwd; reviewers will read source directly.
113
+ const inRepo = await isGitRepo(cwd);
114
+ return {
115
+ workspacePath: cwd,
116
+ historyAvailable: inRepo,
117
+ cloned: false,
118
+ warning: inRepo ? undefined : "Not a git repository — history-context will skip.",
119
+ };
120
+ }
121
+
122
+ if (target.kind !== "pr" || !target.prRef) {
123
+ return {
124
+ workspacePath: cwd,
125
+ historyAvailable: false,
126
+ cloned: false,
127
+ warning: "PR target missing prRef — reviewers will see only the diff.",
128
+ };
129
+ }
130
+
131
+ const parsed = parsePrRepo(target.prRef);
132
+ if (!parsed) {
133
+ return {
134
+ workspacePath: cwd,
135
+ historyAvailable: false,
136
+ cloned: false,
137
+ warning: "Could not parse PR URL — reviewers will see only the diff.",
138
+ };
139
+ }
140
+
141
+ const root = allocateWorkspaceRoot();
142
+ const cloneDir = join(root, `${parsed.repo}-${parsed.number}`);
143
+ mkdirSync(cloneDir, { recursive: true });
144
+
145
+ // Clone (gh first so private repos ride the gh credential; plain https
146
+ // fallback for anonymous/public setups). depth 50 keeps history-context
147
+ // usable without a full clone.
148
+ const url = `https://github.com/${parsed.owner}/${parsed.repo}.git`;
149
+ const ghClone = await _runCmd(
150
+ "gh",
151
+ ["repo", "clone", `${parsed.owner}/${parsed.repo}`, cloneDir, "--", "--depth", "50"],
152
+ { cwd: root },
153
+ );
154
+ const clone = ghClone.exitCode === 0
155
+ ? ghClone
156
+ : await _runCmd("git", ["clone", "--depth", "50", url, cloneDir], { cwd: root });
157
+ if (clone.exitCode !== 0) {
158
+ removeWorkspaceRoot(root);
159
+ throw new Error(
160
+ `pi-review: could not clone ${parsed.owner}/${parsed.repo} (${(ghClone.stderr || clone.stderr).trim().slice(0, 200)}). A fresh diff over a stale local checkout produces false positives, so the review stops here — check gh auth / network and re-run.`,
161
+ );
162
+ }
163
+
164
+ // Fetch the PR head into FETCH_HEAD and detach onto it (no named branch →
165
+ // nothing stale can survive between runs).
166
+ const headFetch = await _runCmd(
167
+ "git",
168
+ ["fetch", "origin", `pull/${parsed.number}/head`, "--quiet"],
169
+ { cwd: cloneDir },
170
+ );
171
+ if (headFetch.exitCode !== 0) {
172
+ removeWorkspaceRoot(root);
173
+ throw new Error(
174
+ `pi-review: git fetch pull/${parsed.number}/head failed (${headFetch.stderr.trim().slice(0, 200)}) — aborting instead of reviewing a mismatched checkout.`,
175
+ );
176
+ }
177
+ const fetchHead = (await _runCmd("git", ["rev-parse", "FETCH_HEAD"], { cwd: cloneDir })).stdout.trim();
178
+ if (
179
+ target.expectedHeadSha &&
180
+ fetchHead &&
181
+ fetchHead !== target.expectedHeadSha
182
+ ) {
183
+ // One refetch — a force-push may have raced the clone.
184
+ const retry = await _runCmd(
185
+ "git",
186
+ ["fetch", "origin", `pull/${parsed.number}/head`, "--quiet"],
187
+ { cwd: cloneDir },
188
+ );
189
+ const retryHead = retry.exitCode === 0
190
+ ? (await _runCmd("git", ["rev-parse", "FETCH_HEAD"], { cwd: cloneDir })).stdout.trim()
191
+ : "";
192
+ if (retryHead && retryHead !== target.expectedHeadSha) {
193
+ removeWorkspaceRoot(root);
194
+ throw new Error(
195
+ `pi-review: PR ${parsed.number} head moved to ${retryHead.slice(0, 12)} while the diff was captured at ${target.expectedHeadSha.slice(0, 12)} — re-run /review to get a consistent pair.`,
196
+ );
197
+ }
198
+ }
199
+ const headCheckout = await _runCmd("git", ["checkout", "--detach", "FETCH_HEAD"], { cwd: cloneDir });
200
+ if (headCheckout.exitCode !== 0) {
201
+ removeWorkspaceRoot(root);
202
+ throw new Error(
203
+ `pi-review: checkout of PR ${parsed.number} head failed (${headCheckout.stderr.trim().slice(0, 200)}).`,
204
+ );
205
+ }
206
+
207
+ const workspaceHeadSha = await safeHead(cloneDir);
208
+ return { workspacePath: cloneDir, historyAvailable: true, cloned: true, workspaceHeadSha };
209
+ }
210
+
211
+ async function safeHead(cwd: string): Promise<string | undefined> {
212
+ const r = await _runCmd("git", ["rev-parse", "HEAD"], { cwd });
213
+ if (r.exitCode !== 0) return undefined;
214
+ return r.stdout.trim() || undefined;
215
+ }
216
+
217
+ async function isGitRepo(cwd: string): Promise<boolean> {
218
+ const r = await _runCmd("git", ["rev-parse", "--git-dir"], { cwd });
219
+ return r.exitCode === 0;
220
+ }
221
+
222
+ /** Optional: write a `.pi-review-meta.json` so reviewers can find the run dir. */
223
+ export function writeWorkspaceMarker(workspacePath: string, payload: Record<string, unknown>): void {
224
+ const path = join(workspacePath, ".pi-review-meta.json");
225
+ try {
226
+ writeFileSync(path, JSON.stringify(payload, null, 2) + "\n", "utf-8");
227
+ } catch {
228
+ /* read-only fs, etc. */
229
+ }
230
+ }
231
+
232
+ /** Quiet helper to remove the marker when the workspace is torn down. */
233
+ export function clearWorkspaceMarker(workspacePath: string): void {
234
+ try {
235
+ rmSync(join(workspacePath, ".pi-review-meta.json"), { force: true });
236
+ } catch {
237
+ /* ignore */
238
+ }
239
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Thin wrapper that adapts `runReportTool` to `pi.registerTool`. The tool
3
+ * loads the manifest, validates the workflow return value's reviewer/gate
4
+ * structuredOutput, runs the deterministic verdict + report builder, and
5
+ * persists a session entry.
6
+ */
7
+ import { Type } from "typebox";
8
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
+
10
+ import { runReportTool } from "./report-tool.js";
11
+
12
+ export function registerReviewReportTool(pi: ExtensionAPI): void {
13
+ pi.registerTool({
14
+ name: "pi_review_report",
15
+ label: "pi-review Report",
16
+ description:
17
+ "Finalize a pi-review run. Inputs: runId + the workflow return value from the reviewer's `runs.all`/`runs.run` call. Loads the manifest, enforces the verdict in code, and persists a session entry.",
18
+ parameters: Type.Object({
19
+ runId: Type.String({ description: "Run id from the prepared manifest (e.g. 'xyz123-abc')." }),
20
+ workflowReturn: Type.Any({
21
+ description: "The workflow return value `{ reviewers, gate }` from the most recent subagent() call.",
22
+ }),
23
+ threshold: Type.Optional(Type.Integer({ minimum: 0, maximum: 10 })),
24
+ verdictPolicy: Type.Optional(Type.Union([
25
+ Type.Literal("strict"),
26
+ Type.Literal("legacy"),
27
+ ])),
28
+ }),
29
+ async execute(_id, params, _signal, _onUpdate, ctx) {
30
+ const result = runReportTool({
31
+ runId: params.runId,
32
+ workflowReturn: params.workflowReturn,
33
+ threshold: params.threshold,
34
+ verdictPolicy: params.verdictPolicy,
35
+ cwd: ctx.cwd,
36
+ });
37
+ if (!result.ok) {
38
+ return {
39
+ content: [{ type: "text", text: `pi_review_report failed: ${result.error}` }],
40
+ details: { error: result.error },
41
+ };
42
+ }
43
+ // Persist a session entry so the TUI renderer can render a
44
+ // collapsible card and `/review-show` can re-render later.
45
+ try {
46
+ pi.appendEntry("pi-review", {
47
+ runId: result.runId,
48
+ verdict: result.verdict,
49
+ markdown: result.markdown,
50
+ report: result.report,
51
+ createdAt: Date.now(),
52
+ });
53
+ } catch {
54
+ // appendEntry is best-effort; the agent still has the markdown.
55
+ }
56
+ // Also push the markdown into chat so the user sees the report.
57
+ pi.sendMessage({
58
+ customType: "pi-review",
59
+ content: result.markdown,
60
+ display: true,
61
+ });
62
+ void ctx;
63
+ const t = result.report.totals.bySeverity;
64
+ return {
65
+ content: [
66
+ {
67
+ type: "text",
68
+ text: `pi-review result: ${result.verdict} · ${t.blocker} blocker · ${t.major} major · ${t.minor} minor · ${t.nit} nit — full report rendered in the pi-review card below.`,
69
+ },
70
+ ],
71
+ details: { runId: result.runId, verdict: result.verdict, report: result.report },
72
+ };
73
+ },
74
+ });
75
+ }
@@ -0,0 +1,92 @@
1
+ import { Box, Text } from "@earendil-works/pi-tui";
2
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+
4
+ import type { IssueSeverity, Verdict } from "./types.js";
5
+
6
+ interface SeverityTotals {
7
+ blocker: number;
8
+ major: number;
9
+ minor: number;
10
+ nit: number;
11
+ }
12
+
13
+ interface ReportHeader {
14
+ verdict: Verdict | "no-gate" | "error" | "partial";
15
+ totals?: SeverityTotals;
16
+ }
17
+
18
+ function extractHeader(markdown: string): ReportHeader {
19
+ const m = markdown.match(/(?:Verdict|审查裁决):\s*([A-Za-z_\u4e00-\u9fa5]+)\s*(?:([^)]*)|\([^)]*\))?\s*\*{0,2}\s*\(([^)]*)\)/);
20
+ if (!m) return { verdict: "comment" };
21
+ const raw = m[1]?.toLowerCase();
22
+ let verdict: ReportHeader["verdict"] = "comment";
23
+ if (raw?.includes("approve") || raw?.includes("通过")) verdict = "approve";
24
+ else if (raw?.includes("request_changes") || raw?.includes("需要修改")) verdict = "request_changes";
25
+ else if (raw?.includes("no-gate") || raw?.includes("无门禁")) verdict = "no-gate";
26
+ else if (raw?.includes("error") || raw?.includes("异常")) verdict = "error";
27
+ else if (raw?.includes("partial") || raw?.includes("部分")) verdict = "partial";
28
+ else verdict = "comment";
29
+
30
+ const counts = (m[2] ?? "").match(/(\d+)\s*(?:blocker|致命阻断)\s*[·•]\s*(\d+)\s*(?:major|严重)\s*[·•]\s*(\d+)\s*(?:minor|次要)\s*[·•]\s*(\d+)\s*(?:nit|细节优化)/);
31
+ const totals: SeverityTotals | undefined = counts
32
+ ? {
33
+ blocker: Number(counts[1] ?? 0),
34
+ major: Number(counts[2] ?? 0),
35
+ minor: Number(counts[3] ?? 0),
36
+ nit: Number(counts[4] ?? 0),
37
+ }
38
+ : undefined;
39
+ return { verdict, totals };
40
+ }
41
+
42
+ function displayVerdict(v: ReportHeader["verdict"]): string {
43
+ switch (v) {
44
+ case "approve":
45
+ return "审核通过 (Approve)";
46
+ case "request_changes":
47
+ return "需要修改 (Request Changes)";
48
+ case "comment":
49
+ return "普通建议 (Comment)";
50
+ case "no-gate":
51
+ return "无门禁 (No Gate)";
52
+ case "error":
53
+ return "审查异常 (Error)";
54
+ case "partial":
55
+ return "部分完成 (Partial)";
56
+ }
57
+ }
58
+
59
+ export function summaryLine(header: ReportHeader): string {
60
+ const t = header.totals;
61
+ const counts = t
62
+ ? ` · ${t.blocker} 致命阻断 · ${t.major} 严重 · ${t.minor} 次要 · ${t.nit} 细节优化`
63
+ : "";
64
+ return `pi-review 代码审查裁决: ${displayVerdict(header.verdict)}${counts}`;
65
+ }
66
+
67
+ export function registerPiReviewRenderer(pi: ExtensionAPI): void {
68
+ pi.registerMessageRenderer("pi-review", (message, _options, theme) => {
69
+ const contentText = typeof message.content === "string"
70
+ ? message.content
71
+ : (() => {
72
+ const parts: string[] = [];
73
+ for (const block of message.content) {
74
+ if (block.type === "text") parts.push(block.text);
75
+ }
76
+ return parts.join("\n");
77
+ })();
78
+ if (contentText.startsWith("/review")) {
79
+ const echo = theme.fg("toolTitle", "[pi-review] ") + contentText;
80
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
81
+ box.addChild(new Text(echo, 0, 0));
82
+ return box;
83
+ }
84
+ const header = extractHeader(contentText);
85
+ const summary = theme.bold(theme.fg("toolTitle", summaryLine(header)));
86
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
87
+ box.addChild(new Text(summary, 0, 0));
88
+ box.addChild(new Text("", 0, 0));
89
+ box.addChild(new Text(contentText, 0, 0));
90
+ return box;
91
+ });
92
+ }
package/src/types.ts ADDED
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Shared type definitions for pi-review (v0.7+).
3
+ *
4
+ * The active path is the foreground workflowScript:
5
+ * /review → plugin prep (diff + target workspace + manifest)
6
+ * → main agent runs one subagent({ workflowScript })
7
+ * → pi_review_report tool renders the deterministic report
8
+ *
9
+ * No legacy spawn-pipeline types remain.
10
+ */
11
+
12
+ /** Final verdict produced by the code-side gate enforcement. */
13
+ export type Verdict = "approve" | "request_changes" | "comment";
14
+
15
+ /** Issue severity bucket. */
16
+ export type IssueSeverity = "blocker" | "major" | "minor" | "nit";
17
+
18
+ /** Issue category — informs routing and which reviewer surfaced it. */
19
+ export type IssueCategory =
20
+ | "compliance"
21
+ | "bug"
22
+ | "convention"
23
+ | "history"
24
+ | "security"
25
+ | "performance"
26
+ | "docs"
27
+ | "other";
28
+
29
+ /** A single finding reported by a reviewer (or surfaced by the gate). */
30
+ export interface Issue {
31
+ /** Repo-relative path, or "global" for cross-cutting findings. */
32
+ file: string;
33
+ /** 1-indexed line in the file. Omit for findings that span ranges or are file-wide. */
34
+ line?: number;
35
+ /** Optional line in the **introduced** diff this finding maps to. */
36
+ relatedChangedLine?: number;
37
+ category: IssueCategory;
38
+ severity: IssueSeverity;
39
+ /** 1-10 confidence score (calibrated like Claude's code-review plugin). */
40
+ confidence: number;
41
+ /** Short evidence quote or paraphrase. Max 280 chars. */
42
+ evidence: string;
43
+ /** Stable id for cross-reviewer dedupe (file:line:category[:short-hash]). */
44
+ fingerprint?: string;
45
+ }
46
+
47
+ /** Per-reviewer status (separate from runtime `ok`/`failed`). */
48
+ export type ReviewerStatus = "ok" | "limited" | "skipped" | "failed";
49
+
50
+ /** Structured payload a reviewer subagent must produce. */
51
+ export interface ReviewerOutput {
52
+ status: ReviewerStatus;
53
+ issues: Issue[];
54
+ summary: string;
55
+ coverage: {
56
+ filesChecked: string[];
57
+ commandsRun: string[];
58
+ limitations: string[];
59
+ };
60
+ }
61
+
62
+ /** Per-candidate disposition emitted by the gate for audit. */
63
+ export interface GateDisposition {
64
+ fingerprint: string;
65
+ decision: "kept" | "dropped" | "merged";
66
+ originalConfidence: number;
67
+ finalConfidence: number;
68
+ sourceReviewers: string[];
69
+ reason: string;
70
+ }
71
+
72
+ /** Structured payload the gate subagent must produce. */
73
+ export interface GateOutput {
74
+ status: ReviewerStatus;
75
+ verdict: Verdict;
76
+ issues: Issue[];
77
+ dispositions: GateDisposition[];
78
+ reason: string;
79
+ coverage: {
80
+ limitations: string[];
81
+ };
82
+ }
83
+
84
+ /** Definition of a single reviewer, loaded from config + bundled prompt. */
85
+ export interface ReviewerSpec {
86
+ id: string;
87
+ label: string;
88
+ enabled: boolean;
89
+ /** "inherit" resolves to the parent session model at run time. */
90
+ model: string | "inherit";
91
+ /** Optional per-reviewer thinking level ("off"|"low"|"medium"|"high"|...). */
92
+ thinking?: string;
93
+ /**
94
+ * Optional absolute path override to the reviewer prompt markdown.
95
+ * When omitted, the runner derives `agents/<id>.md` from `id`.
96
+ */
97
+ promptPath?: string;
98
+ }
99
+
100
+ /** Per-run outcome of a single reviewer subagent. */
101
+ export interface ReviewerRunResult {
102
+ id: string;
103
+ label: string;
104
+ /** Resolved model id (post-"inherit" substitution). */
105
+ model: string;
106
+ ok: boolean;
107
+ output?: ReviewerOutput;
108
+ error?: string;
109
+ /** Process exit code. null = killed by timeout or signal. undefined = never started. */
110
+ exitCode?: number | null;
111
+ durationMs: number;
112
+ }
113
+
114
+ /** Aggregated verdict produced by the code-side gate enforcement. */
115
+ export interface GateVerdict {
116
+ verdict: Verdict;
117
+ /** Deduped + threshold-filtered issues from the reviewer pool. */
118
+ issues: Issue[];
119
+ /** Per-candidate audit trail (kept / dropped / merged). */
120
+ dispositions: GateDisposition[];
121
+ /** Reviewer status reflecting coverage (e.g. "limited" if bugbot failed). */
122
+ status: ReviewerStatus;
123
+ /** One-sentence rationale, max 500 chars. */
124
+ reason: string;
125
+ }
126
+
127
+ /** Per-run outcome of the gate subagent. */
128
+ export interface GateRunResult {
129
+ ok: boolean;
130
+ verdict?: GateVerdict;
131
+ error?: string;
132
+ /** Process exit code. null = killed by timeout or signal. undefined = never started. */
133
+ exitCode?: number | null;
134
+ durationMs: number;
135
+ model: string;
136
+ }
137
+
138
+ /** Verdict policy used by the code-side gate enforcement. */
139
+ export type VerdictPolicy = "strict" | "legacy";
140
+
141
+ /** Adaptive routing controls whether obviously-inapplicable lanes are dropped up front. */
142
+ export type RoutingMode = "adaptive" | "all";
143
+
144
+ /** Top-level user-editable config (v0.7). */
145
+ export interface PiReviewConfig {
146
+ schemaVersion: 1;
147
+ gate: {
148
+ /** "inherit" → parent session model at run time. */
149
+ model: string | "inherit";
150
+ thinking?: string;
151
+ /** When false, gate is always skipped. */
152
+ enabled: boolean;
153
+ /** Default confidence floor for the gate (issues with confidence < threshold are dropped). */
154
+ threshold: number;
155
+ /** Verdict policy: strict (any blocker/major) | legacy (≥3 majors). */
156
+ verdictPolicy: VerdictPolicy;
157
+ };
158
+ /** Adaptive routing: drop clearly-inapplicable reviewer lanes up front. */
159
+ routing: {
160
+ mode: RoutingMode;
161
+ };
162
+ reviewers: Record<string, Omit<ReviewerSpec, "promptPath"> & { promptPath?: string }>;
163
+ /**
164
+ * Optional budgets for the foreground directive path (pi-subagents).
165
+ * turnBudget.maxTurns defaults to 20 (cap 48).
166
+ */
167
+ budgets?: {
168
+ turnBudget?: { maxTurns?: number; graceTurns?: number };
169
+ };
170
+ }
171
+
172
+ /**
173
+ * What to review — agent-driven (v0.4+). The plugin does **not** embed a full
174
+ * diff; the extension prepares the diff + target workspace, then reviewer
175
+ * children read it.
176
+ */
177
+ export type ReviewTargetKind = "pr" | "diff-file" | "local-git";
178
+
179
+ export interface ReviewTarget {
180
+ kind: ReviewTargetKind;
181
+ /** Short human label for the report header. */
182
+ label: string;
183
+ /** CC-style freeform user context (PR URL, instructions, etc.). */
184
+ userContext?: string;
185
+ /** Parsed PR URL or number when kind === "pr". */
186
+ prRef?: string;
187
+ /** Absolute path to an explicit `--diff` file when kind === "diff-file". */
188
+ diffPath?: string;
189
+ /** Hint for local-git: dirty working tree vs base...HEAD. */
190
+ hint?: string;
191
+ /** Optional short probe note for dry-run. */
192
+ probeNote?: string;
193
+ }
194
+
195
+ /** Top-level run report. */
196
+ export interface ReviewReport {
197
+ startedAt: number;
198
+ durationMs: number;
199
+ input: ReviewTarget;
200
+ reviewers: ReviewerRunResult[];
201
+ gate: GateRunResult | null;
202
+ totals: {
203
+ issues: number;
204
+ bySeverity: Record<IssueSeverity, number>;
205
+ };
206
+ verdict: Verdict | "no-gate" | "error" | "partial";
207
+ }