@unifan/pi-review-zh 1.0.2 → 1.0.4
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/agents/perf-review.md +67 -0
- package/index.ts +16 -7
- package/package.json +1 -1
- package/src/cli-args.ts +10 -19
- package/src/directive.ts +91 -186
- package/src/lean-agents.ts +8 -30
- package/src/review-run.ts +11 -5
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: perf-review
|
|
3
|
+
package: pi-review
|
|
4
|
+
description: 性能与基准测试审查专家:专项排查内存泄漏、高频GC分配、算法复杂度退化、锁竞争,并主动探测执行基准测试(Benchmark)。
|
|
5
|
+
tools: read, grep, bash
|
|
6
|
+
systemPromptMode: replace
|
|
7
|
+
inheritProjectContext: false
|
|
8
|
+
inheritSkills: false
|
|
9
|
+
---
|
|
10
|
+
你是**性能与基准测试审查专家(Performance & Benchmark Reviewer)**。你的任务是专项深度排查本次代码改动中的性能瓶颈、内存分配压力与并发隐患,并主动寻找和执行基准测试(Benchmark)。
|
|
11
|
+
|
|
12
|
+
## 核心审查维度(跨语言与全栈)
|
|
13
|
+
|
|
14
|
+
1. **内存与 GC 分配(Memory & Allocation Pressure)**:
|
|
15
|
+
- **高频循环堆分配**:检查循环(Update / Tick / 帧循环 / 高频事件处理器)中是否存在 `new` 对象、隐式闭包分配、Lambda 捕获外部变量、LINQ、装箱/拆箱。
|
|
16
|
+
- **集合与缓冲区**:集合未预设初始容量导致的频繁扩容与重分配、频繁的数组拷贝、低效字符串拼接(应使用 StringBuilder / Span)。
|
|
17
|
+
- **内存泄漏**:未注销的事件监听(Event)、未释放的非托管资源/句柄/流/GPU 纹理、长生命周期单例持有大对象强引用。
|
|
18
|
+
|
|
19
|
+
2. **CPU 与算法复杂度(CPU & Complexity)**:
|
|
20
|
+
- **复杂度退化**:高频热点中 $O(N^2)$ 或更高复杂度的嵌套循环与线性查找(应替换为 Dictionary/HashMap/HashSet 索引)。
|
|
21
|
+
- **昂贵反射与深拷贝**:热点路径中频繁反射(Reflection)、动态解析、低效序列化。
|
|
22
|
+
- **主线程阻塞**:UI 线程或渲染主线程中执行同步大文件读取或阻塞性 I/O。
|
|
23
|
+
|
|
24
|
+
3. **并发与锁竞争(Concurrency & Contention)**:
|
|
25
|
+
- 锁粒度过大、在锁内执行耗时 I/O、无界队列、死锁与假共享(False Sharing)。
|
|
26
|
+
|
|
27
|
+
4. **基准测试探索与执行(Benchmark Runner)**:
|
|
28
|
+
- **自动探测**:主动检查工作区是否存在基准测试套件(如 `BenchmarkDotNet`、`go test -bench`、`cargo bench`、`pytest-benchmark`、`vitest bench` 等)。
|
|
29
|
+
- **有条件执行**:若存在现成的基准测试且安全可运行,使用 `bash` 运行一次并提取关键指标(单次耗时、单次分配字节数)。
|
|
30
|
+
- **基准代码生成**:若当前改动属于核心热点但缺乏 Benchmark,在报告中给出一段针对该场景的标准基准测试代码建议。
|
|
31
|
+
|
|
32
|
+
## 执行步骤
|
|
33
|
+
1. 读取任务中的 diff 文件与 manifest 清单。
|
|
34
|
+
2. 重点分析改动中涉及性能与资源管理的关键代码,必要时读取上下文文件。
|
|
35
|
+
3. 检查并运行基准测试(如适用)。
|
|
36
|
+
4. 输出格式规范的 Markdown 报告并在最末尾附带 JSON 块。
|
|
37
|
+
|
|
38
|
+
## 输出格式(必须使用中文撰写总结与描述)
|
|
39
|
+
|
|
40
|
+
请以 Markdown 格式输出最终回复:
|
|
41
|
+
|
|
42
|
+
## Summary
|
|
43
|
+
一句话中文性能总结(评定本次修改的性能影响:优秀 / 无明显影响 / 存在性能隐患 / 严重性能退化)。
|
|
44
|
+
|
|
45
|
+
## Findings
|
|
46
|
+
每个发现一行:`- [SEVERITY|category|confidence] 文件路径:行号 — 中文问题描述、分配分析与优化建议`。无问题写 `No findings.`。
|
|
47
|
+
|
|
48
|
+
## Benchmark Analysis
|
|
49
|
+
说明工作区中基准测试探测结果与运行数据。若无基准测试,可针对改动热点提供一段简短的 Benchmark 编写建议。
|
|
50
|
+
|
|
51
|
+
## Coverage
|
|
52
|
+
- Files checked: 检查的文件
|
|
53
|
+
- Commands run: 执行的基准测试或检查命令
|
|
54
|
+
- Limitations: 局限说明
|
|
55
|
+
|
|
56
|
+
然后在最末尾严格输出一个被 ```json 代码块包裹的 JSON:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"status": "ok",
|
|
61
|
+
"issues": [
|
|
62
|
+
{ "file": "src/Manager.cs", "line": 42, "category": "perf", "severity": "major", "confidence": 9, "evidence": "Update 循环中存在 new GC 堆分配,且高频调用 LINQ 查询", "fingerprint": "src/Manager.cs:42:perf:a1b2c3" }
|
|
63
|
+
],
|
|
64
|
+
"summary": "中文一句话性能总结",
|
|
65
|
+
"coverage": { "filesChecked": ["src/Manager.cs"], "commandsRun": [], "limitations": [] }
|
|
66
|
+
}
|
|
67
|
+
```
|
package/index.ts
CHANGED
|
@@ -25,18 +25,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
25
25
|
registerReviewReportTool(pi);
|
|
26
26
|
registerPiReviewRenderer(pi);
|
|
27
27
|
pi.registerCommand("review", {
|
|
28
|
-
description: "启动 AI 并发代码审查 (支持多专家子代理 + 门禁总裁判系统)。--lite =
|
|
28
|
+
description: "启动 AI 并发代码审查 (支持多专家子代理 + 门禁总裁判系统)。--lite = 极速单专家审查,--perf = 性能与基准测试审查。",
|
|
29
29
|
getArgumentCompletions: (prefix: string) => {
|
|
30
|
+
const options = [
|
|
31
|
+
{ value: "--lite", label: "--lite", description: "极速单专家审查 (无门禁,低延迟省 Token)" },
|
|
32
|
+
{ value: "--perf", label: "--perf", description: "专项性能与基准测试审查 (GC/内存分配/CPU/Benchmark)" },
|
|
33
|
+
{ value: "--gate-model", label: "--gate-model", description: "指定当前审查的门禁裁判模型" },
|
|
34
|
+
];
|
|
30
35
|
const trimmed = prefix.trimStart();
|
|
36
|
+
if (!trimmed) return options;
|
|
31
37
|
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
32
38
|
const last = tokens[tokens.length - 1] ?? "";
|
|
33
39
|
if (last.startsWith("--")) {
|
|
34
|
-
return
|
|
35
|
-
{ value: "--lite", label: "--lite", description: "极速单专家审查 (无门禁,低延迟省 Token)" },
|
|
36
|
-
{ value: "--gate-model", label: "--gate-model", description: "指定当前审查的门禁裁判模型" },
|
|
37
|
-
].filter((o) => o.value.startsWith(last));
|
|
40
|
+
return options.filter((o) => o.value.startsWith(last));
|
|
38
41
|
}
|
|
39
|
-
return
|
|
42
|
+
return options;
|
|
40
43
|
},
|
|
41
44
|
handler: async (args, ctx) => {
|
|
42
45
|
const notify = (msg: string, level: "info" | "warning" | "error" = "info") => {
|
|
@@ -61,7 +64,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
61
64
|
return;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
const prepared = await prepareRun({
|
|
67
|
+
const prepared = await prepareRun({
|
|
68
|
+
cwd: ctx.cwd,
|
|
69
|
+
input: parsed.input,
|
|
70
|
+
lite: parsed.lite,
|
|
71
|
+
perf: parsed.perf,
|
|
72
|
+
gateModel: parsed.gateModel,
|
|
73
|
+
});
|
|
65
74
|
if (!prepared) {
|
|
66
75
|
notify("没有检测到需要审查的内容 (未找到修改、PR 或非 Git 仓库)。", "info");
|
|
67
76
|
return;
|
package/package.json
CHANGED
package/src/cli-args.ts
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parse `/review` command arguments.
|
|
3
|
-
*
|
|
4
|
-
* Surface is intentionally minimal:
|
|
5
|
-
* - `--lite` fast single-agent review (no fan-out, no gate)
|
|
6
|
-
* - `--no-spawn` hidden dry-run (prints the resolved plan, no subprocess)
|
|
7
|
-
* - trailing freeform text → user review request (focus / requirements /
|
|
8
|
-
* PR url / context), injected into reviewers and gate via target.userContext
|
|
9
|
-
* (see src/prep.ts and src/git-input.ts)
|
|
10
|
-
*
|
|
11
|
-
* Removed flags (--threshold / --reviewer / --score-per-issue / --diff) are
|
|
12
|
-
* accepted-but-ignored for graceful degradation of old invocations: a valued
|
|
13
|
-
* legacy flag also consumes its next token so its value does not leak into
|
|
14
|
-
* `input`. Those capabilities now live in config.json (`/review-config`).
|
|
15
|
-
* `--gate-model <id>` remains an active per-run override.
|
|
16
3
|
*/
|
|
17
4
|
|
|
18
5
|
export interface ParsedReviewArgs {
|
|
@@ -22,11 +9,12 @@ export interface ParsedReviewArgs {
|
|
|
22
9
|
noSpawn: boolean;
|
|
23
10
|
/** Single-agent fast mode: one reviewer, no gate. */
|
|
24
11
|
lite: boolean;
|
|
12
|
+
/** Dedicated performance review mode: perf-reviewer with benchmark capabilities. */
|
|
13
|
+
perf: boolean;
|
|
25
14
|
/** Override the gate model for this run (otherwise config.gate.model). */
|
|
26
15
|
gateModel?: string;
|
|
27
16
|
}
|
|
28
17
|
|
|
29
|
-
/** Removed valued flags — silently skip flag + value to keep input clean. */
|
|
30
18
|
const LEGACY_VALUED_FLAGS = new Set([
|
|
31
19
|
"--threshold",
|
|
32
20
|
"--reviewer",
|
|
@@ -36,13 +24,13 @@ const LEGACY_VALUED_FLAGS = new Set([
|
|
|
36
24
|
|
|
37
25
|
export function parseReviewArgs(raw: string): ParsedReviewArgs {
|
|
38
26
|
const tokens = tokenize(raw);
|
|
39
|
-
const result: ParsedReviewArgs = { noSpawn: false, lite: false };
|
|
27
|
+
const result: ParsedReviewArgs = { noSpawn: false, lite: false, perf: false };
|
|
40
28
|
const inputParts: string[] = [];
|
|
41
29
|
|
|
42
30
|
for (let i = 0; i < tokens.length; i++) {
|
|
43
31
|
const t = tokens[i];
|
|
44
32
|
if (LEGACY_VALUED_FLAGS.has(t)) {
|
|
45
|
-
i++;
|
|
33
|
+
i++;
|
|
46
34
|
continue;
|
|
47
35
|
}
|
|
48
36
|
if (t === "--no-spawn") {
|
|
@@ -53,13 +41,17 @@ export function parseReviewArgs(raw: string): ParsedReviewArgs {
|
|
|
53
41
|
result.lite = true;
|
|
54
42
|
continue;
|
|
55
43
|
}
|
|
44
|
+
if (t === "--perf" || t === "--performance") {
|
|
45
|
+
result.perf = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
56
48
|
if (t === "--gate-model") {
|
|
57
49
|
const id = tokens[++i];
|
|
58
50
|
if (id) result.gateModel = id;
|
|
59
51
|
continue;
|
|
60
52
|
}
|
|
61
53
|
if (t.startsWith("-")) {
|
|
62
|
-
continue;
|
|
54
|
+
continue;
|
|
63
55
|
}
|
|
64
56
|
inputParts.push(t);
|
|
65
57
|
}
|
|
@@ -72,7 +64,6 @@ export function parseReviewArgs(raw: string): ParsedReviewArgs {
|
|
|
72
64
|
return result;
|
|
73
65
|
}
|
|
74
66
|
|
|
75
|
-
/** Split on whitespace preserving quoted segments. */
|
|
76
67
|
function tokenize(raw: string): string[] {
|
|
77
68
|
const out: string[] = [];
|
|
78
69
|
let cur = "";
|
|
@@ -102,4 +93,4 @@ function tokenize(raw: string): string[] {
|
|
|
102
93
|
}
|
|
103
94
|
if (cur.length > 0) out.push(cur);
|
|
104
95
|
return out;
|
|
105
|
-
}
|
|
96
|
+
}
|
package/src/directive.ts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Build the review directive injected into the main agent
|
|
3
|
-
* `sendMessage` with `display:false` + `triggerTurn:true`).
|
|
4
|
-
*
|
|
5
|
-
* v0.7.0 contract (post-mortem from PR #18689 review):
|
|
6
|
-
* - chatProgress must be "auto" | "off" | "live-card" — anything else is
|
|
7
|
-
* rejected by pi-subagents schema validation.
|
|
8
|
-
* - Every reviewer child declares `cwd` (target workspace) and
|
|
9
|
-
* `outputSchema` so pi-subagents returns `result.structuredOutput`.
|
|
10
|
-
* - "inherit" reviewer models are NOT expanded into concrete model ids.
|
|
11
|
-
* The workflow script leaves `model:` off so the orchestrator keeps the
|
|
12
|
-
* inheritance link.
|
|
13
|
-
* - The gate consumes reviewer `structuredOutput` objects directly, never
|
|
14
|
-
* Markdown code fences.
|
|
15
|
-
* - Step 3 hands off to the `pi_review_report` tool, which is the only
|
|
16
|
-
* authoritative report renderer (deterministic code-side verdict).
|
|
2
|
+
* Build the review directive injected into the main agent.
|
|
17
3
|
*/
|
|
18
4
|
import { writeFileSync } from "node:fs";
|
|
19
5
|
|
|
@@ -31,111 +17,102 @@ import type { ReviewerSpec, ReviewTarget } from "./types.js";
|
|
|
31
17
|
export interface ReviewDirectiveInput {
|
|
32
18
|
target: ReviewTarget;
|
|
33
19
|
reviewers: ReviewerSpec[];
|
|
34
|
-
/** Resolved gate model id (from config.gate.model or --gate-model). */
|
|
35
20
|
gateModel: string;
|
|
36
|
-
/** Optional gate thinking from config (appended as model:thinking). */
|
|
37
21
|
gateThinking?: string;
|
|
38
22
|
threshold: number;
|
|
39
|
-
/** Verdict policy passed to the gate task (code-side authoritative). */
|
|
40
23
|
verdictPolicy?: "strict" | "legacy";
|
|
41
24
|
lite: boolean;
|
|
42
|
-
|
|
25
|
+
perf?: boolean;
|
|
43
26
|
gateEnabled?: boolean;
|
|
44
27
|
cwd: string;
|
|
45
|
-
/** Absolute path to the plugin-prepared target workspace (reviewer cwd). */
|
|
46
28
|
workspacePath: string;
|
|
47
|
-
/** Absolute path to the run manifest.json. */
|
|
48
29
|
manifestPath: string;
|
|
49
|
-
/** Absolute path to the captured change.diff. */
|
|
50
30
|
diffPath: string;
|
|
51
|
-
/**
|
|
52
|
-
* Absolute path to write the raw workflowScript text. When set, the raw
|
|
53
|
-
* script is persisted here and the directive points the main agent at it
|
|
54
|
-
* (retry path) instead of asking it to re-derive the script from a
|
|
55
|
-
* double-escaped JSON string — see the 2026-08-25 PR 19395 incident where
|
|
56
|
-
* the main agent's copy/unescape of the script produced a syntax error
|
|
57
|
-
* three times and then drifted into hand-debugging.
|
|
58
|
-
*/
|
|
59
31
|
workflowPath?: string;
|
|
60
|
-
/** Optional turnBudget override from config.budgets. */
|
|
61
32
|
budgets?: LeanBudgetSpec;
|
|
62
33
|
}
|
|
63
34
|
|
|
64
35
|
export function buildReviewDirective(input: ReviewDirectiveInput): string {
|
|
65
|
-
const { target, reviewers, gateModel, gateThinking, threshold, lite, cwd, workspacePath, manifestPath, diffPath, workflowPath } = input;
|
|
36
|
+
const { target, reviewers, gateModel, gateThinking, threshold, lite, perf, cwd, workspacePath, manifestPath, diffPath, workflowPath } = input;
|
|
37
|
+
const isSingle = lite || Boolean(perf);
|
|
66
38
|
const policy = input.verdictPolicy ?? "strict";
|
|
67
|
-
const gateOn = !
|
|
39
|
+
const gateOn = !isSingle && input.gateEnabled !== false;
|
|
68
40
|
const budgets = input.budgets ?? resolveLeanBudgets();
|
|
69
41
|
const gateModelWithThinking = withThinkingSuffix(gateModel, gateThinking);
|
|
70
42
|
const blocks: string[] = [];
|
|
71
43
|
|
|
72
|
-
|
|
44
|
+
const modeLabel = perf ? " (性能与基准测试专属模式)" : lite ? " (极速单兵模式)" : "";
|
|
45
|
+
|
|
46
|
+
blocks.push(`# 代码审查流程${modeLabel}`);
|
|
73
47
|
blocks.push("");
|
|
74
48
|
if (target.userContext?.trim()) {
|
|
75
|
-
blocks.push(
|
|
49
|
+
blocks.push(`**用户指令/侧重点:** ${target.userContext.trim()}`);
|
|
76
50
|
blocks.push("");
|
|
77
51
|
}
|
|
78
52
|
blocks.push(
|
|
79
|
-
|
|
53
|
+
`请对本次代码改动 (${target.label}) 进行审查。插件已准备好目标工作区、diff 文件及运行清单。你需要执行一次 workflowScript 启动 ${reviewers.length} 个审查专家${modeLabel}${gateOn ? " + 门禁裁判长" : ""},随后调用 \`pi_review_report\` 工具完成报告渲染与归档。切勿在聊天中直接重复书写未加工的原始问题细节。`,
|
|
80
54
|
);
|
|
81
55
|
blocks.push("");
|
|
82
|
-
blocks.push("##
|
|
56
|
+
blocks.push("## 硬性规则(严禁违反)");
|
|
83
57
|
blocks.push("");
|
|
84
|
-
blocks.push("-
|
|
58
|
+
blocks.push("- **语言要求**:所有面向用户的输出(包括工作流待办清单、状态汇报、问题总结与回复)**必须使用纯正中文**。");
|
|
59
|
+
blocks.push("- 在本次审查中**只能且必须调用一次** `subagent` 工具:即第 2 步的 workflowScript 调用。");
|
|
85
60
|
blocks.push(
|
|
86
|
-
|
|
87
|
-
? "-
|
|
61
|
+
isSingle
|
|
62
|
+
? "- 第 2 步必须是**单次** `subagent({ workflowScript, async:false, ... })` 调用,通过 `runs.all([...])` 并发执行专家——严禁多次调用。"
|
|
88
63
|
: gateOn
|
|
89
|
-
? "-
|
|
90
|
-
: "-
|
|
64
|
+
? "- 第 2 步必须是**单次** `subagent({ workflowScript, async:false, ... })` 调用,通过 `runs.all([...])` 并发执行**所有**专家,并通过 `runs.run(\"gate\", ...)` 运行门禁裁判长——严禁每个专家单独调用一次,严禁串行多波次调用。"
|
|
65
|
+
: "- 第 2 步必须是**单次** `subagent({ workflowScript, async:false, ... })` 调用,通过 `runs.all([...])` 并发执行**所有**专家——严禁多波次单独调用。",
|
|
91
66
|
);
|
|
92
67
|
blocks.push(
|
|
93
|
-
"-
|
|
68
|
+
"- **切勿重试失败的子代理**:若某个专家超时、耗尽额度或报错,`runs.all` 会自动收集错误;流程继续进行并在最终报告中标注失败即可。",
|
|
94
69
|
);
|
|
95
|
-
const retryScriptHint = workflowPath ?
|
|
70
|
+
const retryScriptHint = workflowPath ? `使用 Read 工具读取 \`${workflowPath}\`` : "使用 Read 工具读取 workflow.js 文件";
|
|
96
71
|
blocks.push(
|
|
97
|
-
"-
|
|
72
|
+
"- **脚本级解析错误例外**:若 `subagent` 因 `workflowScript` 语法解析失败而拒绝执行(无任何专家启动),允许重试**一次**:" + retryScriptHint + " 并以该文件内容作为 `workflowScript` 重试。切勿手动篡改脚本内容。",
|
|
98
73
|
);
|
|
99
|
-
blocks.push("-
|
|
74
|
+
blocks.push("- **严禁**调用 `subagent` 进行额外的事后验证或重写报告。");
|
|
100
75
|
blocks.push(
|
|
101
|
-
"-
|
|
76
|
+
"- **严禁读取 `.pi-subagents/`(工件、对话日志)或尝试从磁盘拼凑审查发现。** `pi_review_report` 所需数据全部来自第 2 步 workflowScript 的返回值。",
|
|
102
77
|
);
|
|
103
78
|
blocks.push(
|
|
104
|
-
"-
|
|
79
|
+
"- 严格使用下方指定的 `pi-review.*` 代理名称,保持每个子代理的 `toolBudget` / `turnBudget` 与全局的 `async:false` / `context:\"fresh\"` / `timeoutMs`。",
|
|
105
80
|
);
|
|
106
|
-
blocks.push("-
|
|
81
|
+
blocks.push("- 审查专家模型**默认继承**父会话(除非配置显式指定了具体模型)。");
|
|
107
82
|
blocks.push(
|
|
108
|
-
"-
|
|
83
|
+
"- 下方的 `workflowScript` 是一个模板字面量,已包含完整路径与参数。请一字不差地复制使用。",
|
|
109
84
|
);
|
|
110
85
|
blocks.push("");
|
|
111
|
-
blocks.push(
|
|
86
|
+
blocks.push(`**忽略以下误报内容:** ${FALSE_POSITIVE_GUIDANCE}。`);
|
|
112
87
|
blocks.push("");
|
|
113
88
|
|
|
114
89
|
blocks.push(
|
|
115
|
-
"
|
|
90
|
+
"首先,在聊天中输出中文工作流待办清单(Checklist),随后逐步执行,并在完成每步后将 `- [ ]` 标记为 `- [x]`:",
|
|
116
91
|
);
|
|
117
92
|
blocks.push("");
|
|
118
93
|
const todoSteps = [
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
? "
|
|
123
|
-
:
|
|
124
|
-
?
|
|
125
|
-
:
|
|
126
|
-
|
|
94
|
+
`确认插件准备的运行清单可读: ${manifestPath}`,
|
|
95
|
+
`确认目标工作区可读: ${workspacePath}`,
|
|
96
|
+
perf
|
|
97
|
+
? "执行 workflowScript: 启动专项性能与基准测试审查专家 (单次 subagent 调用)"
|
|
98
|
+
: lite
|
|
99
|
+
? "执行 workflowScript: 启动单兵极速审查专家 (单次 subagent 调用)"
|
|
100
|
+
: gateOn
|
|
101
|
+
? `执行 workflowScript: 启动 ${reviewers.length} 个并发审查专家 + 门禁裁判长 (单次 subagent 调用)`
|
|
102
|
+
: `执行 workflowScript: 启动 ${reviewers.length} 个并发审查专家 (单次 subagent 调用)`,
|
|
103
|
+
"调用 `pi_review_report` 工具提交审查结果并生成中文报告",
|
|
127
104
|
];
|
|
128
105
|
for (const s of todoSteps) blocks.push(`- [ ] ${s}`);
|
|
129
106
|
blocks.push("");
|
|
130
107
|
|
|
131
|
-
// Step 1
|
|
132
|
-
blocks.push("##
|
|
108
|
+
// Step 1
|
|
109
|
+
blocks.push("## 第 1 步 — 确认插件准备的环境(由主代理执行)");
|
|
133
110
|
blocks.push("");
|
|
134
111
|
blocks.push(
|
|
135
|
-
|
|
112
|
+
`插件已经完成了目标仓库准备,提取了准确的 diff 并计算了 SHA-256 哈希,生成了 \`${manifestPath}\` 与 \`${diffPath}\`。`,
|
|
136
113
|
);
|
|
137
114
|
blocks.push("");
|
|
138
|
-
blocks.push("
|
|
115
|
+
blocks.push("请使用单次 `bash` 调用(不使用 `&&` / `||` 复合连接符)验证文件存在:");
|
|
139
116
|
blocks.push("");
|
|
140
117
|
blocks.push("```bash");
|
|
141
118
|
blocks.push(`test -s ${JSON.stringify(diffPath)}`);
|
|
@@ -143,17 +120,17 @@ export function buildReviewDirective(input: ReviewDirectiveInput): string {
|
|
|
143
120
|
blocks.push(`test -d ${JSON.stringify(workspacePath)}`);
|
|
144
121
|
blocks.push("```");
|
|
145
122
|
blocks.push("");
|
|
146
|
-
blocks.push("
|
|
123
|
+
blocks.push("若任何检查失败,停止并通知用户;检查通过则继续执行第 2 步。");
|
|
147
124
|
blocks.push("");
|
|
148
125
|
|
|
149
|
-
// Step 2
|
|
126
|
+
// Step 2
|
|
150
127
|
const script = buildWorkflowScript({
|
|
151
128
|
reviewers,
|
|
152
129
|
gateModelWithThinking,
|
|
153
130
|
gateThinking,
|
|
154
131
|
gateModel,
|
|
155
132
|
budgets,
|
|
156
|
-
lite,
|
|
133
|
+
lite: isSingle,
|
|
157
134
|
gateEnabled: gateOn,
|
|
158
135
|
threshold,
|
|
159
136
|
verdictPolicy: policy,
|
|
@@ -164,49 +141,40 @@ export function buildReviewDirective(input: ReviewDirectiveInput): string {
|
|
|
164
141
|
diffPath,
|
|
165
142
|
});
|
|
166
143
|
|
|
167
|
-
// Parse guard (P0 regression): make sure the generated script is valid JS
|
|
168
|
-
// BEFORE it reaches the main agent. If the template ever regresses — or,
|
|
169
|
-
// critically, if it ever grows a backtick or `${` (which would break the
|
|
170
|
-
// template-literal presentation the main agent copies) — fail here instead
|
|
171
|
-
// of at subagent() time.
|
|
172
144
|
if (/[`$]/.test(script)) {
|
|
173
145
|
throw new Error(
|
|
174
|
-
"pi-review: generated workflowScript contains a backtick or `$` (template-literal conflict)
|
|
146
|
+
"pi-review: generated workflowScript contains a backtick or `$` (template-literal conflict).",
|
|
175
147
|
);
|
|
176
148
|
}
|
|
177
149
|
try {
|
|
178
150
|
new Function(`return (async () => {\n${script}\n})`);
|
|
179
151
|
} catch (err) {
|
|
180
152
|
throw new Error(
|
|
181
|
-
`pi-review: generated workflowScript is not valid JavaScript
|
|
153
|
+
`pi-review: generated workflowScript is not valid JavaScript: ${err instanceof Error ? err.message : String(err)}`,
|
|
182
154
|
);
|
|
183
155
|
}
|
|
184
156
|
|
|
185
|
-
// Persist the raw script text so the main agent has a zero-unescape
|
|
186
|
-
// retry source (see ReviewDirectiveInput.workflowPath).
|
|
187
157
|
if (workflowPath) {
|
|
188
158
|
try {
|
|
189
159
|
writeFileSync(workflowPath, script, "utf-8");
|
|
190
160
|
} catch {
|
|
191
|
-
|
|
161
|
+
/* ignore */
|
|
192
162
|
}
|
|
193
163
|
}
|
|
194
164
|
|
|
195
|
-
blocks.push("##
|
|
165
|
+
blocks.push("## 第 2 步 — 执行代码审查(仅调用一次 subagent workflowScript)");
|
|
196
166
|
blocks.push("");
|
|
197
167
|
blocks.push(
|
|
198
|
-
|
|
199
|
-
? "
|
|
168
|
+
isSingle
|
|
169
|
+
? "该脚本会启动审查专家,输出包含 JSON 块的 Markdown 报告。"
|
|
200
170
|
: gateOn
|
|
201
|
-
? "
|
|
202
|
-
: "
|
|
171
|
+
? "该脚本会并发启动各个审查专家,随后将报告汇总给门禁裁判长,裁判长输出裁决报告与 JSON 块。"
|
|
172
|
+
: "该脚本会并发启动各个审查专家,输出 Markdown 报告。",
|
|
203
173
|
);
|
|
204
174
|
blocks.push("");
|
|
205
175
|
blocks.push("```js");
|
|
206
176
|
blocks.push("subagent({");
|
|
207
177
|
blocks.push(" workflowScript: `");
|
|
208
|
-
// The raw script, verbatim (no escaping). The script contains no
|
|
209
|
-
// backticks and no ${, so the template literal is lossless.
|
|
210
178
|
blocks.push(script);
|
|
211
179
|
blocks.push("`,");
|
|
212
180
|
blocks.push(` async: false,`);
|
|
@@ -217,58 +185,35 @@ export function buildReviewDirective(input: ReviewDirectiveInput): string {
|
|
|
217
185
|
blocks.push("```");
|
|
218
186
|
blocks.push("");
|
|
219
187
|
blocks.push(
|
|
220
|
-
|
|
188
|
+
`一字不差地复制上方代码执行。若出现解析错误,请使用 \`Read\` 读取 \`${workflowPath ?? "workflow.js"}\` 内容并重试一次。`,
|
|
221
189
|
);
|
|
222
190
|
blocks.push("");
|
|
223
191
|
|
|
224
|
-
// Step 3
|
|
225
|
-
blocks.push("##
|
|
192
|
+
// Step 3
|
|
193
|
+
blocks.push("## 第 3 步 — 渲染审查报告(调用 `pi_review_report`)");
|
|
226
194
|
blocks.push("");
|
|
227
195
|
blocks.push(
|
|
228
|
-
"
|
|
196
|
+
"以 `{ runId, workflowReturn }` 调用 `pi_review_report` 工具**仅一次**。该工具会自动加载清单、解析裁判长的 JSON 裁决、执行判定规则并输出最终的中文 Markdown 审查报告。切勿自己手动重写发现。",
|
|
229
197
|
);
|
|
230
198
|
blocks.push("");
|
|
231
199
|
|
|
232
|
-
// Parse guard (P0 regression): make sure the generated script is valid JS
|
|
233
|
-
// BEFORE it reaches the main agent. If the template ever regresses (e.g. an
|
|
234
|
-
// unquoted path), fail here with a clear error instead of at subagent() time.
|
|
235
|
-
try {
|
|
236
|
-
new Function(`return (async () => {\n${script}\n})`);
|
|
237
|
-
} catch (err) {
|
|
238
|
-
throw new Error(
|
|
239
|
-
`pi-review: generated workflowScript is not valid JavaScript — refusing to hand it to the main agent. This is a plugin bug; please report it. Underlying error: ${err instanceof Error ? err.message : String(err)}`,
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
200
|
return blocks.join("\n");
|
|
244
201
|
}
|
|
245
202
|
|
|
246
|
-
/**
|
|
247
|
-
* Build the inline workflowScript string. Single-wave: one `runs.all([...])`
|
|
248
|
-
* for reviewers, one `runs.run(\"gate\")`. Every child carries `cwd`,
|
|
249
|
-
* `outputSchema`, `toolBudget`/`turnBudget`; explicit model overrides flow
|
|
250
|
-
* through only when the reviewer config is not `inherit`.
|
|
251
|
-
*/
|
|
252
203
|
export function buildWorkflowScript(input: {
|
|
253
204
|
reviewers: ReviewerSpec[];
|
|
254
205
|
gateModelWithThinking: string;
|
|
255
|
-
/** Raw gate thinking level (fallback branch passes it as a child param). */
|
|
256
206
|
gateThinking?: string;
|
|
257
207
|
gateModel: string;
|
|
258
208
|
budgets: LeanBudgetSpec;
|
|
259
209
|
lite: boolean;
|
|
260
|
-
/** Mirror of the directive-level gate switch (false when lite OR disabled). */
|
|
261
210
|
gateEnabled?: boolean;
|
|
262
211
|
threshold: number;
|
|
263
|
-
/** Verdict policy for the gate task text (strict is code-side default). */
|
|
264
212
|
verdictPolicy?: "strict" | "legacy";
|
|
265
213
|
targetLabel: string;
|
|
266
214
|
userContext?: string;
|
|
267
|
-
/** Absolute target workspace path (reviewer + gate cwd). */
|
|
268
215
|
workspacePath: string;
|
|
269
|
-
/** Absolute run manifest path. */
|
|
270
216
|
manifestPath: string;
|
|
271
|
-
/** Absolute change.diff path. */
|
|
272
217
|
diffPath: string;
|
|
273
218
|
}): string {
|
|
274
219
|
const {
|
|
@@ -289,62 +234,51 @@ export function buildWorkflowScript(input: {
|
|
|
289
234
|
} = input;
|
|
290
235
|
const gateOn = !lite && gateEnabled;
|
|
291
236
|
|
|
292
|
-
// Blanket read-only declaration. pi-subagents classifies each task text for
|
|
293
|
-
// mutation intent: with a generic-object prohibition ("do not write any
|
|
294
|
-
// files") plus "review only"/"return findings only", the task is
|
|
295
|
-
// unambiguously read-only, so a read-only agent (gate: tools read) is never
|
|
296
|
-
// rejected by the implementation-tool contract, and acceptance stays at the
|
|
297
|
-
// lightweight attested level instead of "risky write-capable".
|
|
298
237
|
const READ_ONLY_PREFIX =
|
|
299
|
-
"READ-ONLY
|
|
238
|
+
"只读任务(READ-ONLY)——仅执行审查分析与基准测试。严禁修改源码文件。仅返回审查发现。所有分析总结、问题描述与建议必须使用纯正中文。";
|
|
300
239
|
|
|
301
240
|
const lines: string[] = [];
|
|
302
|
-
// v0.8: no outputSchema on any child — the structured-output tool
|
|
303
|
-
// contract was too fragile in the field ("Missing structured_output
|
|
304
|
-
// call" after budget wrap-ups). Reviewers return Markdown reports; the
|
|
305
|
-
// gate ends with a fenced JSON verdict block that the report tool
|
|
306
|
-
// extracts. See agents/*.md "Output format" sections.
|
|
307
241
|
lines.push("");
|
|
308
|
-
// Bind the reviewer array to a local FIRST: the gate IIFE and
|
|
309
|
-
// `reviewersShaped` below both reference `reviewers`, and a bare object
|
|
310
|
-
// property (`return { reviewers: ... }`) does NOT create a variable
|
|
311
|
-
// binding — that produced `ReferenceError: reviewers is not defined` at
|
|
312
|
-
// runtime (silently surfaced as a null workflow return).
|
|
313
242
|
lines.push("const reviewers = await runs.all([");
|
|
314
243
|
for (const r of reviewers) {
|
|
315
|
-
const tb = LEAN_BUDGETS.defaultToolBudget;
|
|
244
|
+
const tb = LEAN_BUDGETS.defaultToolBudget;
|
|
316
245
|
const tbForId = r.id === "history-context" ? LEAN_BUDGETS.historyToolBudget : tb;
|
|
317
246
|
const taskParts = [
|
|
318
247
|
READ_ONLY_PREFIX,
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
"
|
|
323
|
-
"
|
|
324
|
-
"
|
|
248
|
+
`读取 ${JSON.stringify(diffPath)} 作为改动内容——diff 是权威的修改记录,工作区文件仅作上下文参考。若工作区文件与 diff 存在差异,以 diff 为准并在 coverage.limitations 中说明。所有问题描述必须使用中文。`,
|
|
249
|
+
`同时读取 ${JSON.stringify(manifestPath)} 获取改动概要(文档变更状态、文件列表、规则文件路径)。禁止通过外部命令重复拉取。`,
|
|
250
|
+
`你的当前工作区为目标工作区 (${JSON.stringify(workspacePath)})。在此目录下执行必要的 read/grep。`,
|
|
251
|
+
"在额度内完成分析;最终回复必须输出格式规范的 Markdown 审查报告(包含中文 Summary / Findings / Coverage 章节)并停止。所有问题描述、证据引用和总结必须使用纯正中文。",
|
|
252
|
+
"严禁读取 plan.md, progress.md, 以及 .pi-subagents/ 目录下的任何文件或 node_modules。",
|
|
253
|
+
"优先使用 Read/Grep。若使用 bash,仅限简单的单条命令(禁止 &&/||/; 等复合命令)。",
|
|
325
254
|
];
|
|
326
255
|
if (r.id === "claude-md-compliance") {
|
|
327
256
|
taskParts.push(
|
|
328
|
-
|
|
257
|
+
"若 change-profile.rulePaths 为空,返回跳过状态:SKIPPED: no-rules,不提出虚构的违规。",
|
|
329
258
|
);
|
|
330
259
|
}
|
|
331
260
|
if (r.id === "history-context") {
|
|
332
261
|
taskParts.push(
|
|
333
|
-
|
|
262
|
+
"若 change-profile.history.available 为 false,返回跳过状态:SKIPPED: no-history。从文件列表中选取不超过5个路径,仅执行一次 bash: git log -n 5 --oneline -- 文件1 文件2 ...",
|
|
334
263
|
);
|
|
335
264
|
}
|
|
336
265
|
if (r.id === "code-comments") {
|
|
337
266
|
taskParts.push(
|
|
338
|
-
|
|
267
|
+
"若 change-profile.docsOnly 为 true,返回跳过状态:SKIPPED: docs-only。",
|
|
339
268
|
);
|
|
340
269
|
}
|
|
341
270
|
if (r.id === "bugbot" || r.id === "security-review") {
|
|
342
271
|
taskParts.push(
|
|
343
|
-
|
|
272
|
+
"若 change-profile.docsOnly 为 true,返回跳过状态:SKIPPED: docs-only。否则优先从 diff 本身分析,最多只读取 3 个额外上下文文件。",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (r.id === "perf-review") {
|
|
276
|
+
taskParts.push(
|
|
277
|
+
"重点排查循环内高频GC分配、算法复杂度、锁竞争、内存泄漏,并主动探测和执行工作区中的Benchmark基准测试。",
|
|
344
278
|
);
|
|
345
279
|
}
|
|
346
280
|
if (userContext?.trim()) {
|
|
347
|
-
taskParts.push(
|
|
281
|
+
taskParts.push(`用户需求: ${userContext.trim()}`);
|
|
348
282
|
}
|
|
349
283
|
|
|
350
284
|
const modelClause =
|
|
@@ -354,9 +288,6 @@ export function buildWorkflowScript(input: {
|
|
|
354
288
|
lines.push(" {");
|
|
355
289
|
lines.push(` key: ${JSON.stringify(r.id)},`);
|
|
356
290
|
lines.push(` agent: ${JSON.stringify(leanAgentName(r.id))},`);
|
|
357
|
-
// Task as an array joined at runtime — one short quoted line per
|
|
358
|
-
// instruction. A single JSON.stringify of the whole task produced
|
|
359
|
-
// 900+ char lines, the other fragile copy point.
|
|
360
291
|
lines.push(` task: [`);
|
|
361
292
|
for (const part of taskParts) {
|
|
362
293
|
lines.push(` ${JSON.stringify(part)},`);
|
|
@@ -375,52 +306,34 @@ export function buildWorkflowScript(input: {
|
|
|
375
306
|
lines.push("]);");
|
|
376
307
|
lines.push("");
|
|
377
308
|
|
|
378
|
-
//
|
|
379
|
-
// Top-level statements ONLY: pi-subagents' workflowScript AST walker
|
|
380
|
-
// rejects nested async functions ("Use top-level await, plain helper
|
|
381
|
-
// functions, or explicit Promise chains"). The pre-0.7.4 form
|
|
382
|
-
// `gate: await (async () => { ... })()` therefore never passed upstream
|
|
383
|
-
// validation — every prior failure that survived the copy stage died
|
|
384
|
-
// here (2026-08-26 session: "validation failed before child launch").
|
|
309
|
+
// Gate
|
|
385
310
|
if (gateOn) {
|
|
386
311
|
const gateTaskParts = [
|
|
387
312
|
READ_ONLY_PREFIX,
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
313
|
+
`对 ${targetLabel} 的所有专家审查发现进行综合仲裁与去重。所有分析、裁决理由与总结必须使用纯正中文。`,
|
|
314
|
+
`完整 diff 位于 ${JSON.stringify(diffPath)},当前工作区为目标工作区——你可以且应当亲自核验候选问题。`,
|
|
315
|
+
`置信度阈值 ${threshold}:过滤掉最终置信度小于 ${threshold} 的假警报与无意义建议。`,
|
|
316
|
+
`输入为各专家的 Markdown 报告(每个专家包含 ## Summary / ## Findings / ## Coverage)。`,
|
|
317
|
+
`重新评估每个问题的置信度(1–10 分)。对每个 blocker(致命)或 major(严重)候选问题,首先通过阅读 diff 块和目标文件进行核验,并在 disposition 的 reason 中用中文说明核验结果。`,
|
|
318
|
+
`若未经你自己核验证实,严禁将候选问题评分提升至 8 分以上。`,
|
|
319
|
+
`若因缺少上下文或 diff 截断而无法核验某个 blocker/major 问题,切勿静默丢弃:保留原置信度并在 reason 前缀注明 "未核验:",交由人工判断。`,
|
|
320
|
+
`每个问题必须记录在 dispositions 中,包含 decision (kept | dropped | merged), originalConfidence, finalConfidence, sourceReviewers, reason(中文理由)。`,
|
|
396
321
|
verdictPolicy === "legacy"
|
|
397
|
-
?
|
|
398
|
-
:
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
`End your report with exactly one fenced json block containing { status, verdict, issues[], dispositions[], reason } — the parent machine-reads that block.`,
|
|
322
|
+
? `裁决规则: 存在任何 blocker 或 >=3 个 major 则判定为 request_changes; 无 blocker/major 则 approve; 否则 comment。`
|
|
323
|
+
: `裁决规则: 存在任何存活的 blocker 或 major 则判定为 request_changes; 仅有 minor/nit 则 comment; 无存活问题则 approve。`,
|
|
324
|
+
`过滤假警报: ${FALSE_POSITIVE_GUIDANCE}。`,
|
|
325
|
+
`在报告末尾必须输出且仅输出一个被 json 代码块包裹的裁决 JSON 对象 { status, verdict, issues[], dispositions[], reason }(reason 与 evidence 必须为中文)——上层工具将机器读取该 JSON。`,
|
|
402
326
|
];
|
|
403
327
|
|
|
404
|
-
// Inline the reviewers' Markdown reports for the gate to arbitrate.
|
|
405
|
-
// (Sync arrow — allowed; only async functions are rejected upstream.)
|
|
406
328
|
lines.push("const reviewerSections = reviewers.map((r) => {");
|
|
407
|
-
lines.push(" const head = '##
|
|
408
|
-
lines.push(" return head + '\\n\\n' + String(r.output || '(
|
|
329
|
+
lines.push(" const head = '## 审查专家: ' + r.key + (r.ok ? '' : ' (执行失败: ' + String(r.error || 'run failed').slice(0, 120) + ')');");
|
|
330
|
+
lines.push(" return head + '\\n\\n' + String(r.output || '(无输出)').slice(0, 6000);");
|
|
409
331
|
lines.push("});");
|
|
410
|
-
// Gate task as an array join (short lines) — same copy-safety rule as
|
|
411
|
-
// the reviewer tasks above.
|
|
412
332
|
lines.push("const gateTask = [");
|
|
413
333
|
for (const part of gateTaskParts) {
|
|
414
334
|
lines.push(` ${JSON.stringify(part)},`);
|
|
415
335
|
}
|
|
416
|
-
lines.push(`].join(" ") + '\\n\\n#
|
|
417
|
-
// Proxy providers often report bare model ids from the child ("MiniMax-M2.7")
|
|
418
|
-
// that fail the launcher's strict model verification against the launch
|
|
419
|
-
// candidate ("CPA/Minimax/MiniMax-M2.7:high") — observed 2026-08-27. The
|
|
420
|
-
// reviewers never hit this (they inherit). So: try the configured model
|
|
421
|
-
// first; on launch failure retry once with an inherited model under a
|
|
422
|
-
// DIFFERENT key (the runtime rejects same-key launches with different
|
|
423
|
-
// params). A second failure rejects as before.
|
|
336
|
+
lines.push(`].join(" ") + '\\n\\n# 专家审查报告汇总 (Markdown)\\n\\n' + reviewerSections.join('\\n\\n---\\n\\n');`);
|
|
424
337
|
lines.push("let gateRun;");
|
|
425
338
|
lines.push("try {");
|
|
426
339
|
lines.push(" gateRun = await runs.run('gate', {");
|
|
@@ -447,7 +360,6 @@ export function buildWorkflowScript(input: {
|
|
|
447
360
|
lines.push(" ok: gateRun.ok,");
|
|
448
361
|
lines.push(" error: gateRun.error,");
|
|
449
362
|
lines.push(" output: gateRun.output,");
|
|
450
|
-
|
|
451
363
|
lines.push("};");
|
|
452
364
|
lines.push("");
|
|
453
365
|
}
|
|
@@ -459,8 +371,6 @@ export function buildWorkflowScript(input: {
|
|
|
459
371
|
} else {
|
|
460
372
|
lines.push(" gate: null,");
|
|
461
373
|
}
|
|
462
|
-
|
|
463
|
-
// ---- reviewer summary shape ----------------------------------------
|
|
464
374
|
lines.push(" reviewersShaped: reviewers.map((r) => ({");
|
|
465
375
|
lines.push(" key: r.key,");
|
|
466
376
|
lines.push(" ok: r.ok,");
|
|
@@ -471,11 +381,6 @@ export function buildWorkflowScript(input: {
|
|
|
471
381
|
return lines.join("\n");
|
|
472
382
|
}
|
|
473
383
|
|
|
474
|
-
/** Map the workflow return value into a normalized `ReviewWorkflowReturn` for the tool. */
|
|
475
384
|
export function buildWorkflowReturnShape() {
|
|
476
385
|
return "{ reviewers, reviewersShaped, gate }";
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// `gateModel` reserved for config validation parity with previous surface.
|
|
480
|
-
export const _LEGACY_PARITY = { gateModel: "" };
|
|
481
|
-
void _LEGACY_PARITY;
|
|
386
|
+
}
|
package/src/lean-agents.ts
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mapping from pi-review reviewer ids → pi-subagents runtime agent names,
|
|
3
3
|
* plus per-child budgets for the token-lean workflowScript directive path.
|
|
4
|
-
*
|
|
5
|
-
* Agents live in `agents/*.md` and are registered via package.json
|
|
6
|
-
* `pi.subagents.agents` so pi-subagents discovers them as package agents
|
|
7
|
-
* (`pi-review.<id>`).
|
|
8
|
-
*
|
|
9
|
-
* Budget model (pi-subagents ≥0.41 workflowScript API): the top-level
|
|
10
|
-
* `subagent({ workflowScript })` call carries `context`/`timeoutMs` only;
|
|
11
|
-
* `turnBudget` and per-reviewer `toolBudget` are injected onto each
|
|
12
|
-
* `runs.all` / `runs.run` child item (child params override workflow
|
|
13
|
-
* defaults). `runs.run` rejects `tasks`/`chain`/`concurrency` but accepts
|
|
14
|
-
* `toolBudget`/`turnBudget`/`model`/`output`.
|
|
15
4
|
*/
|
|
16
5
|
|
|
17
6
|
export const LEAN_AGENT_PACKAGE = "pi-review";
|
|
@@ -30,7 +19,7 @@ export interface ToolBudgetSpec {
|
|
|
30
19
|
}
|
|
31
20
|
|
|
32
21
|
export interface LeanBudgetSpec {
|
|
33
|
-
/** Per-child turn budget, injected onto each runs.all / runs.run item. */
|
|
22
|
+
/** Per-child turn budget, injected onto each runs.all / runs.run child item. */
|
|
34
23
|
turnBudget: { maxTurns: number; graceTurns: number };
|
|
35
24
|
/** Per-child tool budget for the default reviewer (injected per runs.all item). */
|
|
36
25
|
defaultToolBudget: ToolBudgetSpec;
|
|
@@ -43,13 +32,6 @@ export interface LeanBudgetSpec {
|
|
|
43
32
|
timeoutMs: number;
|
|
44
33
|
}
|
|
45
34
|
|
|
46
|
-
/**
|
|
47
|
-
* Defaults (v0.7.1): reviewers 20→26 turns (field runs kept wrapping up
|
|
48
|
-
* partial at 20); the gate 6→16 turns / 5→14 soft tools — it now carries a
|
|
49
|
-
* verification duty on high-severity candidates (read the diff hunk + the
|
|
50
|
-
* touched file) and physically could not verify anything under the old
|
|
51
|
-
* budget. Wall clock 10→17 min to match.
|
|
52
|
-
*/
|
|
53
35
|
export const LEAN_BUDGETS: LeanBudgetSpec = {
|
|
54
36
|
turnBudget: { maxTurns: 26, graceTurns: 2 },
|
|
55
37
|
defaultToolBudget: { soft: 20, hard: 32 },
|
|
@@ -90,16 +72,12 @@ export function withThinkingSuffix(model: string, thinking?: string): string {
|
|
|
90
72
|
}
|
|
91
73
|
|
|
92
74
|
/**
|
|
93
|
-
* Shared false-positive list
|
|
94
|
-
* Wording constraint: this text is embedded verbatim in the gate task, which
|
|
95
|
-
* pi-subagents classifies for read-only vs implementation intent — keep it
|
|
96
|
-
* free of bare write verbs (modify/edit/implement/…) outside explicit
|
|
97
|
-
* prohibitions, or the read-only gate gets rejected at launch.
|
|
75
|
+
* Shared false-positive list in Chinese.
|
|
98
76
|
*/
|
|
99
77
|
export const FALSE_POSITIVE_GUIDANCE = [
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
].join("; ");
|
|
78
|
+
"本次改动未触及的历史遗留代码问题",
|
|
79
|
+
"资深工程师不会指出的吹毛求疵风格琐碎建议",
|
|
80
|
+
"Linter、类型检查器或 CI 构建会自动捕获的问题",
|
|
81
|
+
"泛泛的代码质量建议(如建议补单测或文档),除非项目规则明确强制要求",
|
|
82
|
+
"表面看似 Bug 但实际属于本次改动预期特性的行为",
|
|
83
|
+
].join("; ");
|
package/src/review-run.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface PrepareRunInput {
|
|
|
47
47
|
input?: string;
|
|
48
48
|
/** Support `--lite` single-agent mode. */
|
|
49
49
|
lite?: boolean;
|
|
50
|
+
/** Support `--perf` performance & benchmark mode. */
|
|
51
|
+
perf?: boolean;
|
|
50
52
|
/** Optional per-run gate model override. */
|
|
51
53
|
gateModel?: string;
|
|
52
54
|
/** Set false for dry-runs — pruning is a side effect a dry run must not have. */
|
|
@@ -143,9 +145,12 @@ export async function prepareRun(input: PrepareRunInput): Promise<PreparedRun |
|
|
|
143
145
|
rulePaths,
|
|
144
146
|
historyAvailable: workspaceResult.historyAvailable,
|
|
145
147
|
};
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
:
|
|
148
|
+
const isSingle = input.lite || input.perf;
|
|
149
|
+
const reviewers = input.perf
|
|
150
|
+
? [{ id: "perf-review", label: "Performance Review", enabled: true, model: "inherit" }]
|
|
151
|
+
: input.lite
|
|
152
|
+
? [{ id: "lite-review", label: "Lite Review", enabled: true, model: "inherit" }]
|
|
153
|
+
: reviewersForRouting(target, config, profile);
|
|
149
154
|
const skippedReasons = adaptiveSkips(profile);
|
|
150
155
|
// The report tool uses this to reject findings that did not come from this
|
|
151
156
|
// run's roster (stale-artifact contamination guard).
|
|
@@ -165,11 +170,12 @@ export async function prepareRun(input: PrepareRunInput): Promise<PreparedRun |
|
|
|
165
170
|
target,
|
|
166
171
|
reviewers,
|
|
167
172
|
gateModel,
|
|
168
|
-
gateThinking:
|
|
173
|
+
gateThinking: isSingle ? undefined : config.gate.thinking,
|
|
169
174
|
gateEnabled: config.gate.enabled,
|
|
170
175
|
threshold: config.gate.threshold,
|
|
171
176
|
verdictPolicy: config.gate.verdictPolicy,
|
|
172
177
|
lite: Boolean(input.lite),
|
|
178
|
+
perf: Boolean(input.perf),
|
|
173
179
|
cwd,
|
|
174
180
|
workspacePath,
|
|
175
181
|
manifestPath,
|
|
@@ -179,7 +185,7 @@ export async function prepareRun(input: PrepareRunInput): Promise<PreparedRun |
|
|
|
179
185
|
});
|
|
180
186
|
// Remember which lanes adaptive routing dropped so the report can surface
|
|
181
187
|
// them as coverage rather than letting users wonder where a reviewer went.
|
|
182
|
-
if (!
|
|
188
|
+
if (!isSingle && config.routing.mode === "adaptive" && skippedReasons.length > 0) {
|
|
183
189
|
const skippedByRouting: Array<{ id: string; reason: string }> = [];
|
|
184
190
|
for (const [id, reason] of skippedReasons) {
|
|
185
191
|
if (!reviewers.some((r) => r.id === id)) skippedByRouting.push({ id, reason });
|