alanbox 0.1.2 → 0.1.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/0boxer/AGENTS.md +26 -0
- package/0boxer/src/AGENTS.md +16 -0
- package/0boxer/src/cli.js +53 -0
- package/0boxer/src/commands/AGENTS.md +16 -0
- package/{0commondflowv1 → 0boxer}/src/commands/install.js +56 -0
- package/{0commondflowv1 → 1swarmer}/AGENTS.md +14 -12
- package/1swarmer/src/AGENTS.md +28 -0
- package/{0commondflowv1 → 1swarmer}/src/args.js +8 -1
- package/{0commondflowv1 → 1swarmer}/src/cli.js +27 -17
- package/1swarmer/src/commands/AGENTS.md +31 -0
- package/{0commondflowv1 → 1swarmer}/src/commands/doctor.js +2 -2
- package/1swarmer/src/commands/review-file.js +997 -0
- package/{0commondflowv1 → 1swarmer}/src/core/AGENTS.md +2 -2
- package/{0commondflowv1 → 1swarmer}/src/core/prompt-templates.js +1 -1
- package/{0commondflowv1 → 1swarmer}/src/core/storage.js +1 -1
- package/{0commondflowv1 → 1swarmer}/src/prompt/AGENTS.md +1 -1
- package/{0commondflowv1 → 1swarmer}/src/prompt/default.md +1 -1
- package/{0commondflowv1 → 1swarmer}/src/prompt/synthesizer.md +1 -1
- package/{0commondflowv1 → 1swarmer}/src/prompt/verifier.md +1 -1
- package/{0commondflowv1 → 1swarmer}/src/runner/AGENTS.md +4 -3
- package/{0commondflowv1 → 1swarmer}/src/runner/codex-runner.js +23 -3
- package/2designer/README.md +42 -0
- package/2designer/dist/cdp-engine-4AIWSWXO.js +314 -0
- package/2designer/dist/cdp-engine-4AIWSWXO.js.map +1 -0
- package/2designer/dist/cdp-engine-SG4K2BCX.js +10 -0
- package/2designer/dist/cdp-engine-SG4K2BCX.js.map +1 -0
- package/2designer/dist/chunk-7X7PTLZH.js +185 -0
- package/2designer/dist/chunk-7X7PTLZH.js.map +1 -0
- package/2designer/dist/chunk-DPOWNFOH.js +313 -0
- package/2designer/dist/chunk-DPOWNFOH.js.map +1 -0
- package/2designer/dist/chunk-ISUUIOO7.js +58 -0
- package/2designer/dist/chunk-ISUUIOO7.js.map +1 -0
- package/2designer/dist/chunk-NLYFLQ3C.js +74 -0
- package/2designer/dist/chunk-NLYFLQ3C.js.map +1 -0
- package/2designer/dist/chunk-UVKSRKXR.js +71 -0
- package/2designer/dist/chunk-UVKSRKXR.js.map +1 -0
- package/2designer/dist/cli.js +748 -0
- package/2designer/dist/cli.js.map +1 -0
- package/2designer/dist/index.d.ts +118 -0
- package/2designer/dist/index.js +37 -0
- package/2designer/dist/index.js.map +1 -0
- package/2designer/dist/playwright-engine-YXBY3KEN.js +186 -0
- package/2designer/dist/playwright-engine-YXBY3KEN.js.map +1 -0
- package/2designer/dist/playwright-engine-YXGDTSZ5.js +8 -0
- package/2designer/dist/playwright-engine-YXGDTSZ5.js.map +1 -0
- package/2designer/dist/tint-UD4CJ7S2.js +7 -0
- package/2designer/dist/tint-UD4CJ7S2.js.map +1 -0
- package/2designer/dist/tint-YN63MLVN.js +60 -0
- package/2designer/dist/tint-YN63MLVN.js.map +1 -0
- package/2designer/package.json +56 -0
- package/4reporter/README.md +24 -0
- package/4reporter/dist/cli.js +464 -0
- package/4reporter/dist/cli.js.map +1 -0
- package/4reporter/dist/index.d.ts +108 -0
- package/4reporter/dist/index.js +445 -0
- package/4reporter/dist/index.js.map +1 -0
- package/4reporter/package.json +39 -0
- package/README.md +20 -9
- package/bin/alanbox.js +11 -0
- package/bin/designer.js +10 -0
- package/bin/reporter.js +11 -0
- package/bin/swarmer.js +11 -0
- package/cli.js +178 -0
- package/hooks/hooks.json +1 -1
- package/mcp/README.md +7 -1
- package/mcp/config.toml +4 -0
- package/package.json +28 -11
- package/plugin/AGENTS.md +2 -2
- package/plugin/plugin.json +7 -7
- package/shared/AGENTS.md +15 -0
- package/shared/package-args.js +68 -0
- package/skills/AGENTS.md +9 -5
- package/skills/aitool/SKILL.md +36 -0
- package/skills/desginer/SKILL.md +142 -0
- package/skills/swarmer/SKILL.md +146 -0
- package/0commondflowv1/src/AGENTS.md +0 -26
- package/0commondflowv1/src/commands/AGENTS.md +0 -29
- package/bin/multirunagent.js +0 -15
- package/skills/aibox-swam/SKILL.md +0 -77
- package/skills/sub-codex-doctor/SKILL.md +0 -27
- package/skills/sub-codex-swarm/SKILL.md +0 -56
- /package/{0commondflowv1 → 1swarmer}/res/three-lens-review.js +0 -0
- /package/{0commondflowv1 → 1swarmer}/src/commands/info.js +0 -0
- /package/{0commondflowv1 → 1swarmer}/src/commands/swarm/auto.js +0 -0
- /package/{0commondflowv1 → 1swarmer}/src/commands/swarm/custom.js +0 -0
- /package/{0commondflowv1 → 1swarmer}/src/commands/swarm/index.js +0 -0
- /package/{0commondflowv1 → 1swarmer}/src/core/handoff.js +0 -0
- /package/{0commondflowv1 → 1swarmer}/src/core/prompt-builder.js +0 -0
- /package/{0commondflowv1 → 1swarmer}/src/core/swarm-executor.js +0 -0
- /package/{0commondflowv1 → 1swarmer}/src/core/workers.js +0 -0
- /package/{0commondflowv1 → 1swarmer}/src/core/workflow-planner.js +0 -0
- /package/{0commondflowv1 → 1swarmer}/src/core/workflow-storage.js +0 -0
- /package/{0commondflowv1 → 1swarmer}/src/prompt/reviewer.md +0 -0
- /package/{0commondflowv1 → 1swarmer}/src/runner/config.json +0 -0
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件/目录双 provider review 命令。
|
|
3
|
+
* Codex 与 Claude 并行初审,再用共识链条目写入同一个 Markdown 报告。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const { execPrompt } = require('../runner/codex-runner');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_PROVIDERS = ['codex', 'claude'];
|
|
14
|
+
const PEER_REPORT_EXCERPT_LIMIT = 8000;
|
|
15
|
+
const DEFAULT_MAX_CONSENSUS_ROUNDS = 4;
|
|
16
|
+
|
|
17
|
+
async function runReviewFile({ args, config, cwd, timeoutMs, account }) {
|
|
18
|
+
const target = resolveReviewTarget(args, cwd);
|
|
19
|
+
const providers = resolveReviewProviders(args);
|
|
20
|
+
const phase = resolveReviewPhase(args);
|
|
21
|
+
const reportRoot = resolveReportRoot(args, cwd);
|
|
22
|
+
const reportPath = resolveReviewReportPath(args, reportRoot, target);
|
|
23
|
+
const executionCwd = args.cwd || args.C || target.executionCwd;
|
|
24
|
+
const commonOptions = {
|
|
25
|
+
config,
|
|
26
|
+
cwd: executionCwd,
|
|
27
|
+
timeoutMs,
|
|
28
|
+
model: args.model || args.m,
|
|
29
|
+
profile: args.profile || args.p,
|
|
30
|
+
sandbox: args.sandbox || args.s,
|
|
31
|
+
account,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
initializeReport(reportPath);
|
|
35
|
+
console.error(`[review-file] target: ${target.path}`);
|
|
36
|
+
console.error(`[review-file] report: ${reportPath}`);
|
|
37
|
+
|
|
38
|
+
const initialResults = phase === 'cross-check'
|
|
39
|
+
? loadExistingConsensusItems(reportPath, resolveCrossCheckInitialProviders(args, providers))
|
|
40
|
+
: await Promise.all(providers.map((provider) => runProviderReview({
|
|
41
|
+
phase: 'initial',
|
|
42
|
+
provider,
|
|
43
|
+
target,
|
|
44
|
+
args,
|
|
45
|
+
commonOptions,
|
|
46
|
+
reportPath,
|
|
47
|
+
})));
|
|
48
|
+
|
|
49
|
+
const mergeResults = phase === 'cross-check'
|
|
50
|
+
? initialResults
|
|
51
|
+
: loadProviderInitialResultsFromReports(initialResults);
|
|
52
|
+
|
|
53
|
+
const items = phase === 'cross-check'
|
|
54
|
+
? mergeResults.items
|
|
55
|
+
: buildConsensusItems(mergeResults, target, providers);
|
|
56
|
+
|
|
57
|
+
appendReportSection(reportPath, buildConsensusItemsSection(items));
|
|
58
|
+
|
|
59
|
+
const rounds = phase === 'initial' || providers.length <= 1
|
|
60
|
+
? []
|
|
61
|
+
: await runConsensusRounds({
|
|
62
|
+
items,
|
|
63
|
+
providers,
|
|
64
|
+
target,
|
|
65
|
+
args,
|
|
66
|
+
commonOptions,
|
|
67
|
+
reportPath,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (rounds.length > 0) {
|
|
71
|
+
appendReportSection(reportPath, buildFinalConsensusSummary({ items }));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(`[review-file] report: ${reportPath}`);
|
|
75
|
+
|
|
76
|
+
const failed = Array.isArray(initialResults)
|
|
77
|
+
? initialResults.some((item) => isInitialExecutionFailure(item))
|
|
78
|
+
: initialResults.results?.some((item) => isInitialExecutionFailure(item));
|
|
79
|
+
const requiresFinalConsensus = phase !== 'initial' && providers.length > 1;
|
|
80
|
+
const incomplete = requiresFinalConsensus && items.some((item) => !isConsensusComplete(item));
|
|
81
|
+
if (incomplete || failed) {
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function runProviderReview({
|
|
87
|
+
phase,
|
|
88
|
+
provider,
|
|
89
|
+
target,
|
|
90
|
+
args,
|
|
91
|
+
commonOptions,
|
|
92
|
+
reportPath,
|
|
93
|
+
other,
|
|
94
|
+
}) {
|
|
95
|
+
const startedAt = new Date();
|
|
96
|
+
const extraContext = buildExtraContext(args);
|
|
97
|
+
const reviewKind = resolveReviewKind({ args, context: extraContext });
|
|
98
|
+
const prompt = phase === 'cross-check'
|
|
99
|
+
? buildCrossCheckPrompt({ provider, target, other, extraContext })
|
|
100
|
+
: buildInitialReviewPrompt({ provider, target, extraContext, reviewKind });
|
|
101
|
+
|
|
102
|
+
console.error(`[review-file] start ${provider}/${phase}`);
|
|
103
|
+
|
|
104
|
+
let result;
|
|
105
|
+
let error = null;
|
|
106
|
+
try {
|
|
107
|
+
result = await execPrompt(prompt, {
|
|
108
|
+
...commonOptions,
|
|
109
|
+
provider,
|
|
110
|
+
worker: `review-${provider}-${phase}`,
|
|
111
|
+
stream: false,
|
|
112
|
+
account: args[`${provider}-account`] || commonOptions.account,
|
|
113
|
+
permissionMode: resolveProviderPermissionMode(args, provider),
|
|
114
|
+
claudeArgs: resolveClaudeArgs(args, provider),
|
|
115
|
+
});
|
|
116
|
+
} catch (caught) {
|
|
117
|
+
error = caught;
|
|
118
|
+
result = {
|
|
119
|
+
code: 1,
|
|
120
|
+
signal: '',
|
|
121
|
+
stdout: '',
|
|
122
|
+
stderr: caught?.stack || caught?.message || String(caught),
|
|
123
|
+
cwd: commonOptions.cwd,
|
|
124
|
+
args: [],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const finishedAt = new Date();
|
|
129
|
+
const stdout = result.stdout || '';
|
|
130
|
+
const stderr = result.stderr || '';
|
|
131
|
+
const findings = parseFindingsFromOutput(`${stdout}\n${stderr}`, provider, target);
|
|
132
|
+
const invalidReason = findings.length === 0
|
|
133
|
+
? detectInvalidInitialReview({ stdout, stderr, error, code: result.code })
|
|
134
|
+
: '';
|
|
135
|
+
const item = {
|
|
136
|
+
phase,
|
|
137
|
+
provider,
|
|
138
|
+
reviewKind,
|
|
139
|
+
code: result.code,
|
|
140
|
+
signal: result.signal,
|
|
141
|
+
reportPath,
|
|
142
|
+
otherReportPath: other?.reportPath || '',
|
|
143
|
+
error: error ? (error.message || String(error)) : '',
|
|
144
|
+
startedAt: startedAt.toISOString(),
|
|
145
|
+
finishedAt: finishedAt.toISOString(),
|
|
146
|
+
stdout,
|
|
147
|
+
stderr,
|
|
148
|
+
findings,
|
|
149
|
+
invalidReason,
|
|
150
|
+
providerReportPath: writeProviderReviewReport({
|
|
151
|
+
reportPath,
|
|
152
|
+
provider,
|
|
153
|
+
phase,
|
|
154
|
+
result,
|
|
155
|
+
error,
|
|
156
|
+
findings,
|
|
157
|
+
target,
|
|
158
|
+
reviewKind,
|
|
159
|
+
invalidReason,
|
|
160
|
+
}),
|
|
161
|
+
};
|
|
162
|
+
console.error(`[review-file] finished ${provider}/${phase}`);
|
|
163
|
+
return item;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildInitialReviewPrompt({ provider, target, extraContext, reviewKind = resolveReviewKind({ context: extraContext }) }) {
|
|
167
|
+
const improvementMode = reviewKind === 'improvements';
|
|
168
|
+
const findingSchema = '{"findings":[{"title":"...","detail":"...","evidence":"...","severity":"high|medium|low"}]}';
|
|
169
|
+
return [
|
|
170
|
+
'你是代码审查 agent。请只做 review,不要修改文件。',
|
|
171
|
+
'',
|
|
172
|
+
`当前 provider: ${provider}`,
|
|
173
|
+
`审查目标: ${target.path}`,
|
|
174
|
+
`目标类型: ${target.type}`,
|
|
175
|
+
`审查模式: ${improvementMode ? 'improvements' : 'defects'}`,
|
|
176
|
+
'',
|
|
177
|
+
extraContext ? `额外背景:\n${extraContext}` : '',
|
|
178
|
+
'',
|
|
179
|
+
'要求:',
|
|
180
|
+
improvementMode
|
|
181
|
+
? '1. 重点找高置信、可落地、值得改的改进项,包括构建体积、依赖清理、部署可复现性、类型安全、可维护性、用户体验、性能和可靠性。'
|
|
182
|
+
: '1. 重点找高置信的正确性问题、数据丢失、并发/状态错误、命令参数错误、路径/Windows 兼容问题和会让 CLI 无法工作的缺陷。',
|
|
183
|
+
improvementMode
|
|
184
|
+
? '2. 可以把非阻塞但证据明确的改进项写入 findings;每条必须给出文件路径、行号或可定位证据、影响、建议修法和验证方式。'
|
|
185
|
+
: '2. 发现问题时按严重程度排序,给出文件路径、行号或可定位的代码片段,并解释触发条件和影响。',
|
|
186
|
+
'3. 不要把纯风格偏好当成问题;不确定、无法落地或没有项目内证据的点不要写入 findings。',
|
|
187
|
+
improvementMode
|
|
188
|
+
? '4. 如果找到可落地改进项,即使不是阻塞 bug,也必须写入 CONSENSUS_JSON findings;如果确实没有,明确写“未发现高置信可落地改进项”,并列出实际检查范围。'
|
|
189
|
+
: '4. 如果未发现高置信问题,明确写“未发现需要阻塞的 review 问题”,并列出实际检查范围。',
|
|
190
|
+
`5. 必须在回答末尾输出一个 CONSENSUS_JSON 代码块,格式为 ${findingSchema}。没有可报告项时 findings 为空数组。`,
|
|
191
|
+
'6. 用中文输出。先列 Findings,再列检查范围、验证建议和结论。',
|
|
192
|
+
].filter(Boolean).join('\n');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildCrossCheckPrompt({ provider, target, other, extraContext }) {
|
|
196
|
+
const otherReportText = other?.sectionText
|
|
197
|
+
? trimText(other.sectionText, PEER_REPORT_EXCERPT_LIMIT)
|
|
198
|
+
: readTextWithLimit(other?.reportPath || '', PEER_REPORT_EXCERPT_LIMIT);
|
|
199
|
+
const otherProvider = other?.provider || '另一个 provider';
|
|
200
|
+
return [
|
|
201
|
+
'你是代码审查复核 agent。请只做 review,不要修改文件。',
|
|
202
|
+
'',
|
|
203
|
+
`当前 provider: ${provider}`,
|
|
204
|
+
`审查目标: ${target.path}`,
|
|
205
|
+
`目标类型: ${target.type}`,
|
|
206
|
+
`需要复核的对方 provider: ${otherProvider}`,
|
|
207
|
+
`同一个总报告文件: ${other?.reportPath || '<missing>'}`,
|
|
208
|
+
'',
|
|
209
|
+
extraContext ? `额外背景:\n${extraContext}` : '',
|
|
210
|
+
'',
|
|
211
|
+
'复核要求:',
|
|
212
|
+
'1. 检查下方待判断 finding 是否成立。',
|
|
213
|
+
'2. 如果认同,decision 必须是 "agree";如果不认同或证据不足,decision 必须是 "disagree"。',
|
|
214
|
+
'3. 如果不认同,必须给出原因和下一步应检查的证据。',
|
|
215
|
+
'4. 必须在回答末尾输出一个 CONSENSUS_JSON 代码块,格式为 {"decision":"agree|disagree","reason":"...","detail":"..."}。',
|
|
216
|
+
'5. 用中文输出。不要简单复述对方报告;要给出你的判断。',
|
|
217
|
+
'',
|
|
218
|
+
'待判断 finding 如下:',
|
|
219
|
+
'```markdown',
|
|
220
|
+
otherReportText || '<empty report>',
|
|
221
|
+
'```',
|
|
222
|
+
].join('\n');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function runConsensusRounds({
|
|
226
|
+
items,
|
|
227
|
+
providers,
|
|
228
|
+
target,
|
|
229
|
+
args,
|
|
230
|
+
commonOptions,
|
|
231
|
+
reportPath,
|
|
232
|
+
}) {
|
|
233
|
+
const parsedMaxRounds = Number(args['max-consensus-rounds'] ?? DEFAULT_MAX_CONSENSUS_ROUNDS);
|
|
234
|
+
const maxRounds = Number.isFinite(parsedMaxRounds) && parsedMaxRounds >= 0
|
|
235
|
+
? Math.floor(parsedMaxRounds)
|
|
236
|
+
: DEFAULT_MAX_CONSENSUS_ROUNDS;
|
|
237
|
+
const rounds = [];
|
|
238
|
+
|
|
239
|
+
for (let round = 1; round <= maxRounds; round += 1) {
|
|
240
|
+
const pending = items.filter((item) => !isConsensusComplete(item));
|
|
241
|
+
if (pending.length === 0) break;
|
|
242
|
+
|
|
243
|
+
const tasks = pending.map((item) => {
|
|
244
|
+
const reviewer = chooseNextReviewer(item, providers);
|
|
245
|
+
return runConsensusReview({
|
|
246
|
+
item,
|
|
247
|
+
reviewer,
|
|
248
|
+
round,
|
|
249
|
+
target,
|
|
250
|
+
args,
|
|
251
|
+
commonOptions,
|
|
252
|
+
reportPath,
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const results = await Promise.all(tasks);
|
|
257
|
+
for (const result of results) {
|
|
258
|
+
applyConsensusReview(result.item, result.review);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
rounds.push({ round, results });
|
|
262
|
+
appendReportSection(reportPath, buildConsensusRoundSection(round, results));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const unresolved = items.filter((item) => !isConsensusComplete(item));
|
|
266
|
+
if (unresolved.length > 0) {
|
|
267
|
+
for (const item of unresolved) {
|
|
268
|
+
const forcedProvider = chooseNextReviewer(item, providers);
|
|
269
|
+
applyConsensusReview(item, {
|
|
270
|
+
provider: forcedProvider,
|
|
271
|
+
verdict: 'agree',
|
|
272
|
+
reason: `达到最大共识轮次 ${maxRounds},按协议强制继续到最终 √;需要人工复查前面的 ❌ 原因。`,
|
|
273
|
+
code: 0,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
const forcedResults = unresolved.map((item) => ({
|
|
277
|
+
item,
|
|
278
|
+
review: item.reviews[item.reviews.length - 1],
|
|
279
|
+
forced: true,
|
|
280
|
+
}));
|
|
281
|
+
rounds.push({ round: maxRounds + 1, results: forcedResults, forced: true });
|
|
282
|
+
appendReportSection(reportPath, buildConsensusRoundSection(maxRounds + 1, forcedResults));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return rounds;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function runConsensusReview({
|
|
289
|
+
item,
|
|
290
|
+
reviewer,
|
|
291
|
+
round,
|
|
292
|
+
target,
|
|
293
|
+
args,
|
|
294
|
+
commonOptions,
|
|
295
|
+
reportPath,
|
|
296
|
+
}) {
|
|
297
|
+
const prompt = buildConsensusPrompt({ item, reviewer, round, target, reportPath, extraContext: buildExtraContext(args) });
|
|
298
|
+
console.error(`[review-file] consensus round ${round} ${reviewer} item ${item.id}`);
|
|
299
|
+
|
|
300
|
+
let result;
|
|
301
|
+
let error = null;
|
|
302
|
+
try {
|
|
303
|
+
result = await execPrompt(prompt, {
|
|
304
|
+
...commonOptions,
|
|
305
|
+
provider: reviewer,
|
|
306
|
+
worker: `review-${reviewer}-consensus-${round}`,
|
|
307
|
+
stream: false,
|
|
308
|
+
account: args[`${reviewer}-account`] || commonOptions.account,
|
|
309
|
+
permissionMode: resolveProviderPermissionMode(args, reviewer),
|
|
310
|
+
claudeArgs: resolveClaudeArgs(args, reviewer),
|
|
311
|
+
});
|
|
312
|
+
} catch (caught) {
|
|
313
|
+
error = caught;
|
|
314
|
+
result = {
|
|
315
|
+
code: 1,
|
|
316
|
+
stdout: '',
|
|
317
|
+
stderr: caught?.stack || caught?.message || String(caught),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const parsed = parseConsensusDecision(result.stdout || '');
|
|
322
|
+
const review = {
|
|
323
|
+
provider: reviewer,
|
|
324
|
+
verdict: parsed.decision === 'agree' && result.code === 0 && !error ? 'agree' : 'disagree',
|
|
325
|
+
reason: parsed.reason || parsed.detail || result.stderr || error?.message || '未给出有效同意结论',
|
|
326
|
+
detail: parsed.detail || '',
|
|
327
|
+
code: result.code,
|
|
328
|
+
stderr: result.stderr || '',
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
return { item, review };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function buildConsensusPrompt({ item, reviewer, round, target, reportPath, extraContext }) {
|
|
335
|
+
const hasDisagreement = item.reviews.some((review) => review.verdict === 'disagree');
|
|
336
|
+
const taskLine = hasDisagreement
|
|
337
|
+
? '当前条目已有 ❌。请针对前面的不同意原因继续分析:如果你认为条目经过澄清后成立,输出 agree;如果仍不成立,输出 disagree 并给出下一轮应检查的证据。'
|
|
338
|
+
: '请检查这个来源于对方 provider 的合并报告条目是否成立;不要简单复述,要给出你的判断。';
|
|
339
|
+
return [
|
|
340
|
+
'你是代码审查共识复核 agent。请只做 review,不要修改文件。',
|
|
341
|
+
'',
|
|
342
|
+
`当前 provider: ${reviewer}`,
|
|
343
|
+
`共识轮次: ${round}`,
|
|
344
|
+
`审查目标: ${target.path}`,
|
|
345
|
+
`目标类型: ${target.type}`,
|
|
346
|
+
`合并报告文件: ${reportPath || '<memory>'}`,
|
|
347
|
+
'',
|
|
348
|
+
extraContext ? `额外背景:\n${extraContext}` : '',
|
|
349
|
+
'',
|
|
350
|
+
taskLine,
|
|
351
|
+
'',
|
|
352
|
+
'当前合并报告条目:',
|
|
353
|
+
`${formatConsensusTag(item)} ${item.title}`,
|
|
354
|
+
'',
|
|
355
|
+
`- id: ${item.id}`,
|
|
356
|
+
`- 来源 provider: ${item.origin}`,
|
|
357
|
+
`- 标题: ${item.title}`,
|
|
358
|
+
`- 严重度: ${item.severity}`,
|
|
359
|
+
`- 详情: ${item.detail}`,
|
|
360
|
+
`- 证据: ${item.evidence}`,
|
|
361
|
+
'',
|
|
362
|
+
'已有判断:',
|
|
363
|
+
...item.reviews.map((review) => `- ${review.provider}: ${review.verdict === 'agree' ? '同意' : '不同意'};${review.reason || review.detail || ''}`),
|
|
364
|
+
'',
|
|
365
|
+
'要求:',
|
|
366
|
+
'1. 判断这个 finding 当前是否应该被确认。',
|
|
367
|
+
'2. 如果认可该 finding 成立,输出 decision=agree。',
|
|
368
|
+
'3. 如果不认可、证据不足、描述需要改写,输出 decision=disagree,并说明下一轮应该检查什么。',
|
|
369
|
+
'4. 必须在回答末尾输出 CONSENSUS_JSON 代码块,格式 {"decision":"agree|disagree","reason":"...","detail":"..."}。',
|
|
370
|
+
].join('\n');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function buildConsensusItems(initialResults, target, providers = DEFAULT_PROVIDERS) {
|
|
374
|
+
const items = [];
|
|
375
|
+
const reviewKind = initialResults.find((result) => result.reviewKind)?.reviewKind || 'defects';
|
|
376
|
+
for (const result of initialResults) {
|
|
377
|
+
const findings = result.findings.length > 0
|
|
378
|
+
? result.findings
|
|
379
|
+
: fallbackFindingFromResult(result, target);
|
|
380
|
+
for (const finding of findings) {
|
|
381
|
+
const initialVerdict = finding.initialVerdict || 'agree';
|
|
382
|
+
const item = {
|
|
383
|
+
id: `F${String(items.length + 1).padStart(3, '0')}`,
|
|
384
|
+
origin: result.provider,
|
|
385
|
+
title: compactText(finding.title || `${result.provider} review finding`).slice(0, 160),
|
|
386
|
+
detail: compactText(finding.detail || finding.summary || '').slice(0, 4000),
|
|
387
|
+
evidence: compactText(finding.evidence || finding.file || target.path).slice(0, 1000),
|
|
388
|
+
severity: normalizeSeverity(finding.severity),
|
|
389
|
+
reviews: [{
|
|
390
|
+
provider: result.provider,
|
|
391
|
+
verdict: initialVerdict,
|
|
392
|
+
reason: finding.initialReason || (initialVerdict === 'agree' ? '初审提出并确认。' : `初审退出码 ${result.code}`),
|
|
393
|
+
code: result.code,
|
|
394
|
+
}],
|
|
395
|
+
};
|
|
396
|
+
items.push(item);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (items.length === 0) {
|
|
401
|
+
const activeProviders = uniqueStrings(initialResults
|
|
402
|
+
.filter((result) => !isInitialExecutionFailure(result))
|
|
403
|
+
.map((result) => result.provider)
|
|
404
|
+
.filter(Boolean));
|
|
405
|
+
const reviewedProviders = activeProviders.length > 0 ? activeProviders : ['system'];
|
|
406
|
+
items.push({
|
|
407
|
+
id: 'F001',
|
|
408
|
+
origin: 'system',
|
|
409
|
+
title: reviewKind === 'improvements'
|
|
410
|
+
? '未发现高置信可落地改进项'
|
|
411
|
+
: '未发现需要阻塞的 review 问题',
|
|
412
|
+
detail: reviewKind === 'improvements'
|
|
413
|
+
? `${reviewedProviders.join(' 和 ')} 初审没有提出结构化高置信可落地改进项。`
|
|
414
|
+
: `${reviewedProviders.join(' 和 ')} 初审没有提出结构化高置信 finding。`,
|
|
415
|
+
evidence: target.path,
|
|
416
|
+
severity: 'low',
|
|
417
|
+
reviews: reviewedProviders.map((provider) => ({
|
|
418
|
+
provider,
|
|
419
|
+
verdict: 'agree',
|
|
420
|
+
reason: reviewKind === 'improvements' ? '未发现高置信可落地改进项。' : '无阻塞问题。',
|
|
421
|
+
code: 0,
|
|
422
|
+
})),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return items;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function fallbackFindingFromResult(result, target) {
|
|
430
|
+
return [];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function applyConsensusReview(item, review) {
|
|
434
|
+
item.reviews.push(review);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function chooseNextReviewer(item, providers) {
|
|
438
|
+
const last = item.reviews[item.reviews.length - 1]?.provider;
|
|
439
|
+
const candidate = providers.find((provider) => provider !== last);
|
|
440
|
+
return candidate || providers[0] || DEFAULT_PROVIDERS[0];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function isConsensusComplete(item) {
|
|
444
|
+
const last = item.reviews[item.reviews.length - 1];
|
|
445
|
+
if (item.origin === 'system') return last?.verdict === 'agree';
|
|
446
|
+
return last?.verdict === 'agree' && item.reviews.length >= 2;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function formatConsensusTag(item) {
|
|
450
|
+
const body = item.reviews
|
|
451
|
+
.map((review) => `${review.provider}${review.verdict === 'agree' ? '√' : '❌'}`)
|
|
452
|
+
.join('');
|
|
453
|
+
return `[${body}]`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function buildConsensusItemsSection(items) {
|
|
457
|
+
return items.flatMap((item) => formatConsensusItemLines(item)).join('\n');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function buildConsensusRoundSection(round, results) {
|
|
461
|
+
return [
|
|
462
|
+
...results.flatMap(({ item, review, forced }) => [
|
|
463
|
+
`${formatConsensusTag(item)} ${item.title}`,
|
|
464
|
+
'',
|
|
465
|
+
`- item: ${item.id}`,
|
|
466
|
+
`- round: ${round}`,
|
|
467
|
+
`- reviewer: ${review.provider}`,
|
|
468
|
+
`- verdict: ${review.verdict === 'agree' ? '√' : '❌'}${forced ? ' (forced-final)' : ''}`,
|
|
469
|
+
`- reason: ${review.reason || ''}`,
|
|
470
|
+
review.detail ? `- detail: ${review.detail}` : '',
|
|
471
|
+
'',
|
|
472
|
+
].filter(Boolean)),
|
|
473
|
+
].join('\n');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function buildFinalConsensusSummary({ items }) {
|
|
477
|
+
return items.flatMap((item) => formatConsensusItemLines(item)).join('\n');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function formatConsensusItemLines(item) {
|
|
481
|
+
return [
|
|
482
|
+
`${formatConsensusTag(item)} ${item.title}`,
|
|
483
|
+
'',
|
|
484
|
+
`- id: ${item.id}`,
|
|
485
|
+
`- severity: ${item.severity}`,
|
|
486
|
+
`- detail: ${item.detail}`,
|
|
487
|
+
`- evidence: ${item.evidence}`,
|
|
488
|
+
'',
|
|
489
|
+
];
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function initializeReport(reportPath) {
|
|
493
|
+
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
494
|
+
if (fs.existsSync(reportPath) && fs.statSync(reportPath).size > 0) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
fs.writeFileSync(reportPath, '', 'utf8');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function writeProviderReviewReport({ reportPath, provider, phase, result, error, findings = [], target, reviewKind = 'defects', invalidReason = '' }) {
|
|
502
|
+
if (!reportPath || phase !== 'initial') return '';
|
|
503
|
+
const providerReportPath = resolveProviderReportPath(reportPath, provider, phase);
|
|
504
|
+
const failed = Boolean(error || Number(result?.code || 0) !== 0);
|
|
505
|
+
const items = findings.map((finding, index) => ({
|
|
506
|
+
id: `F${String(index + 1).padStart(3, '0')}`,
|
|
507
|
+
origin: provider,
|
|
508
|
+
title: compactText(finding.title || `${provider} review finding`).slice(0, 160),
|
|
509
|
+
detail: compactText(finding.detail || finding.summary || '').slice(0, 4000),
|
|
510
|
+
evidence: compactText(finding.evidence || finding.file || target?.path || '').slice(0, 1000),
|
|
511
|
+
severity: normalizeSeverity(finding.severity),
|
|
512
|
+
reviews: [{
|
|
513
|
+
provider,
|
|
514
|
+
verdict: 'agree',
|
|
515
|
+
reason: '初审提出并确认。',
|
|
516
|
+
code: result?.code || 0,
|
|
517
|
+
}],
|
|
518
|
+
}));
|
|
519
|
+
const body = items.length > 0
|
|
520
|
+
? buildConsensusItemsSection(items)
|
|
521
|
+
: invalidReason
|
|
522
|
+
? [
|
|
523
|
+
`[${provider}❌] ${provider} 初审无效`,
|
|
524
|
+
'',
|
|
525
|
+
'- id: F001',
|
|
526
|
+
'- severity: medium',
|
|
527
|
+
`- detail: ${invalidReason}`,
|
|
528
|
+
`- evidence: ${target?.path || ''}`,
|
|
529
|
+
].join('\n')
|
|
530
|
+
: failed
|
|
531
|
+
? [
|
|
532
|
+
`[${provider}❌] ${provider} 初审执行失败`,
|
|
533
|
+
'',
|
|
534
|
+
'- id: F001',
|
|
535
|
+
'- severity: medium',
|
|
536
|
+
`- detail: ${compactText(error?.message || result?.stderr || `provider exited with code ${result?.code || 1}`).slice(0, 1000)}`,
|
|
537
|
+
`- evidence: ${target?.path || ''}`,
|
|
538
|
+
].join('\n')
|
|
539
|
+
: [
|
|
540
|
+
`[${provider}√] ${reviewKind === 'improvements' ? '未发现高置信可落地改进项' : '未发现结构化 finding'}`,
|
|
541
|
+
'',
|
|
542
|
+
'- id: F001',
|
|
543
|
+
'- severity: low',
|
|
544
|
+
`- detail: ${provider} 初审没有提出${reviewKind === 'improvements' ? '结构化高置信可落地改进项' : '结构化 finding'}。`,
|
|
545
|
+
`- evidence: ${target?.path || ''}`,
|
|
546
|
+
].join('\n');
|
|
547
|
+
fs.mkdirSync(path.dirname(providerReportPath), { recursive: true });
|
|
548
|
+
const diagnostics = [
|
|
549
|
+
result?.stderr ? `\n\n<!-- ${provider} stderr\n${result.stderr}\n-->` : '',
|
|
550
|
+
error ? `\n\n<!-- ${provider} error\n${error.stack || error.message || String(error)}\n-->` : '',
|
|
551
|
+
].join('');
|
|
552
|
+
fs.writeFileSync(providerReportPath, `${body}${diagnostics}\n`, 'utf8');
|
|
553
|
+
return providerReportPath;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function loadProviderInitialResultsFromReports(initialResults) {
|
|
557
|
+
return initialResults.map((result) => {
|
|
558
|
+
const items = readProviderInitialItems(result.providerReportPath, result.provider);
|
|
559
|
+
if (!items.length) {
|
|
560
|
+
return {
|
|
561
|
+
...result,
|
|
562
|
+
findings: [],
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
...result,
|
|
568
|
+
findings: items.map((item) => ({
|
|
569
|
+
title: item.title,
|
|
570
|
+
detail: item.detail,
|
|
571
|
+
evidence: item.evidence,
|
|
572
|
+
severity: item.severity,
|
|
573
|
+
initialVerdict: item.reviews[0]?.verdict || 'agree',
|
|
574
|
+
initialReason: item.reviews[0]?.reason || '从 provider 初审 md 合并。',
|
|
575
|
+
})),
|
|
576
|
+
};
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function readProviderInitialItems(providerReportPath, provider) {
|
|
581
|
+
if (!providerReportPath || !fs.existsSync(providerReportPath)) return [];
|
|
582
|
+
const markdown = fs.readFileSync(providerReportPath, 'utf8');
|
|
583
|
+
return parseConsensusItemsFromReport(markdown)
|
|
584
|
+
.filter((item) => item.reviews[0]?.provider === provider)
|
|
585
|
+
.filter((item) => item.reviews[0]?.verdict === 'agree')
|
|
586
|
+
.filter((item) => !isNoFindingMarker(item));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function detectInvalidInitialReview({ stdout = '', stderr = '', error = null, code = 0 } = {}) {
|
|
590
|
+
if (error || Number(code || 0) !== 0) return '';
|
|
591
|
+
const text = `${stdout}\n${stderr}`;
|
|
592
|
+
const patterns = [
|
|
593
|
+
{
|
|
594
|
+
pattern: /CreateProcessWithLogonW failed:\s*1326/i,
|
|
595
|
+
reason: 'provider CLI 无法启动本地只读命令,Windows sandbox 返回 CreateProcessWithLogonW failed: 1326,初审没有有效读取目标文件。',
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
pattern: /未能有效读取目标目录文件/u,
|
|
599
|
+
reason: 'provider 明确说明未能有效读取目标目录文件,空 findings 不能视为项目无改进项。',
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
pattern: /所有本地只读命令.*被环境错误阻断/u,
|
|
603
|
+
reason: 'provider 明确说明本地只读命令被环境错误阻断,初审结果无效。',
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
pattern: /MCP 资源没有暴露仓库内容/u,
|
|
607
|
+
reason: 'provider 无法通过本地命令或 MCP 资源读取仓库内容,初审结果无效。',
|
|
608
|
+
},
|
|
609
|
+
];
|
|
610
|
+
const match = patterns.find((item) => item.pattern.test(text));
|
|
611
|
+
return match ? match.reason : '';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function isNoFindingMarker(item) {
|
|
615
|
+
const title = compactText(item?.title || '');
|
|
616
|
+
return title === '未发现结构化 finding'
|
|
617
|
+
|| title === '未发现高置信可落地改进项'
|
|
618
|
+
|| title === '未发现需要阻塞的 review 问题';
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function resolveProviderReportPath(reportPath, provider, phase) {
|
|
622
|
+
const parsed = path.parse(reportPath);
|
|
623
|
+
return path.join(parsed.dir, `${parsed.name}.${provider}.${phase}.md`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function appendReportSection(reportPath, text) {
|
|
627
|
+
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
628
|
+
const body = String(text || '').replace(/\s+$/u, '');
|
|
629
|
+
if (!body) return;
|
|
630
|
+
fs.appendFileSync(reportPath, `${body}\n\n`, 'utf8');
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function resolveReviewTarget(args, cwd) {
|
|
634
|
+
const value = args.target || args.file || args.f || args._[0];
|
|
635
|
+
if (!value) {
|
|
636
|
+
throw new Error('missing review target. Example: swarmer review-file C:\\path\\to\\file.ts');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const targetPath = path.resolve(cwd || process.cwd(), value);
|
|
640
|
+
const stat = fs.statSync(targetPath);
|
|
641
|
+
const type = stat.isDirectory() ? 'directory' : 'file';
|
|
642
|
+
return {
|
|
643
|
+
path: targetPath,
|
|
644
|
+
type,
|
|
645
|
+
executionCwd: type === 'directory' ? targetPath : path.dirname(targetPath),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function resolveReviewProviders(args) {
|
|
650
|
+
if (args.providers) {
|
|
651
|
+
const providers = String(args.providers)
|
|
652
|
+
.split(',')
|
|
653
|
+
.map((item) => item.trim().toLowerCase())
|
|
654
|
+
.filter(Boolean);
|
|
655
|
+
validateProviders(providers);
|
|
656
|
+
return providers;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (args.codex && !args.claude && !args.both) return ['codex'];
|
|
660
|
+
if (args.claude && !args.codex && !args.both) return ['claude'];
|
|
661
|
+
return DEFAULT_PROVIDERS.slice();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function resolveReviewPhase(args) {
|
|
665
|
+
const phase = String(args.phase || 'all').trim().toLowerCase();
|
|
666
|
+
if (!['all', 'initial', 'cross-check'].includes(phase)) {
|
|
667
|
+
throw new Error('invalid --phase. Supported: all, initial, cross-check');
|
|
668
|
+
}
|
|
669
|
+
return phase;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function resolveCrossCheckInitialProviders(args, providers) {
|
|
673
|
+
if (args['peer-providers']) {
|
|
674
|
+
const peers = String(args['peer-providers'])
|
|
675
|
+
.split(',')
|
|
676
|
+
.map((item) => item.trim().toLowerCase())
|
|
677
|
+
.filter(Boolean);
|
|
678
|
+
validateProviders(peers);
|
|
679
|
+
return uniqueStrings([...providers, ...peers]);
|
|
680
|
+
}
|
|
681
|
+
return uniqueStrings([...providers, ...DEFAULT_PROVIDERS]);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function loadExistingConsensusItems(reportPath, providers) {
|
|
685
|
+
const fullReport = fs.existsSync(reportPath) ? fs.readFileSync(reportPath, 'utf8') : '';
|
|
686
|
+
const items = parseConsensusItemsFromReport(fullReport);
|
|
687
|
+
return {
|
|
688
|
+
items,
|
|
689
|
+
results: providers.map((provider) => {
|
|
690
|
+
const hasProvider = items.some((item) => item.reviews.some((review) => review.provider === provider));
|
|
691
|
+
return {
|
|
692
|
+
phase: 'initial',
|
|
693
|
+
provider,
|
|
694
|
+
code: hasProvider ? 0 : 1,
|
|
695
|
+
error: hasProvider ? '' : 'missing provider consensus item',
|
|
696
|
+
};
|
|
697
|
+
}),
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function uniqueStrings(values) {
|
|
702
|
+
return [...new Set(values.filter(Boolean))];
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function isInitialExecutionFailure(result) {
|
|
706
|
+
return Boolean(result?.error || (Number(result?.code || 0) !== 0 && (!result?.findings || result.findings.length === 0)));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function parseFindingsFromOutput(text, provider, target) {
|
|
710
|
+
const parsed = extractConsensusJson(text);
|
|
711
|
+
if (parsed && Array.isArray(parsed.findings)) {
|
|
712
|
+
return parsed.findings
|
|
713
|
+
.filter((item) => item && typeof item === 'object')
|
|
714
|
+
.map((item) => ({
|
|
715
|
+
title: item.title || item.summary || `${provider} review finding`,
|
|
716
|
+
detail: item.detail || item.description || item.reason || '',
|
|
717
|
+
evidence: item.evidence || item.file || target.path,
|
|
718
|
+
severity: item.severity || 'medium',
|
|
719
|
+
}));
|
|
720
|
+
}
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function resolveReviewKind({ args = {}, context = '' } = {}) {
|
|
725
|
+
const explicit = String(args['review-kind'] || args.kind || args.mode || '').trim().toLowerCase();
|
|
726
|
+
if (['improvement', 'improvements', 'improve', 'optimize', 'optimization', '优化', '改进'].includes(explicit)) {
|
|
727
|
+
return 'improvements';
|
|
728
|
+
}
|
|
729
|
+
if (['defect', 'defects', 'bug', 'bugs', 'correctness', 'blocker', 'blocking'].includes(explicit)) {
|
|
730
|
+
return 'defects';
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const text = String(context || args.context || '').toLowerCase();
|
|
734
|
+
if (/(改进|优化|可维护|性能|体验|体积|依赖清理|部署|improvement|improve|optimi[sz]e|performance|maintainability|ux|bundle)/u.test(text)) {
|
|
735
|
+
return 'improvements';
|
|
736
|
+
}
|
|
737
|
+
return 'defects';
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function parseConsensusDecision(text) {
|
|
741
|
+
const parsed = extractConsensusJson(text);
|
|
742
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
743
|
+
return { decision: '', reason: '', detail: '' };
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
decision: String(parsed.decision || '').toLowerCase(),
|
|
747
|
+
reason: compactText(parsed.reason || ''),
|
|
748
|
+
detail: compactText(parsed.detail || parsed.summary || ''),
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function extractConsensusJson(text) {
|
|
753
|
+
const source = String(text || '');
|
|
754
|
+
const patterns = [
|
|
755
|
+
/```CONSENSUS_JSON\s*([\s\S]*?)```/i,
|
|
756
|
+
/```json\s*([\s\S]*?"(?:findings|decision)"[\s\S]*?)```/i,
|
|
757
|
+
];
|
|
758
|
+
for (const pattern of patterns) {
|
|
759
|
+
const match = source.match(pattern);
|
|
760
|
+
if (!match) continue;
|
|
761
|
+
try {
|
|
762
|
+
return JSON.parse(match[1].trim());
|
|
763
|
+
} catch {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function parseConsensusItemsFromReport(markdown) {
|
|
771
|
+
const items = [];
|
|
772
|
+
const pattern = /^\[((?:codex|claude|system)[√✗❌](?:(?:codex|claude|system)[√✗❌])*)\]\s+(.+)$/gm;
|
|
773
|
+
let match;
|
|
774
|
+
while ((match = pattern.exec(String(markdown || ''))) !== null) {
|
|
775
|
+
const tagBody = match[1];
|
|
776
|
+
const title = match[2].trim();
|
|
777
|
+
const tail = markdown.slice(match.index, markdown.indexOf('\n[', match.index + 1) > -1
|
|
778
|
+
? markdown.indexOf('\n[', match.index + 1)
|
|
779
|
+
: markdown.length);
|
|
780
|
+
const fallbackId = `F${String(items.length + 1).padStart(3, '0')}`;
|
|
781
|
+
const item = {
|
|
782
|
+
id: extractListValue(tail, 'id') || extractListValue(tail, 'item') || fallbackId,
|
|
783
|
+
origin: '',
|
|
784
|
+
title,
|
|
785
|
+
severity: extractListValue(tail, 'severity') || 'medium',
|
|
786
|
+
detail: extractListValue(tail, 'detail') || '',
|
|
787
|
+
evidence: extractListValue(tail, 'evidence') || '',
|
|
788
|
+
reviews: parseConsensusTagBody(tagBody),
|
|
789
|
+
};
|
|
790
|
+
item.origin = item.reviews[0]?.provider || '';
|
|
791
|
+
items.push(item);
|
|
792
|
+
}
|
|
793
|
+
return dedupeConsensusItems(items);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function parseConsensusTagBody(tagBody) {
|
|
797
|
+
const reviews = [];
|
|
798
|
+
const pattern = /(codex|claude|system)(√|✗|❌)/g;
|
|
799
|
+
let match;
|
|
800
|
+
while ((match = pattern.exec(tagBody)) !== null) {
|
|
801
|
+
reviews.push({
|
|
802
|
+
provider: match[1],
|
|
803
|
+
verdict: match[2] === '√' ? 'agree' : 'disagree',
|
|
804
|
+
reason: '',
|
|
805
|
+
code: 0,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
return reviews;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function extractListValue(text, key) {
|
|
812
|
+
const pattern = new RegExp(`^- ${escapeRegExp(key)}:\\s*(.*)$`, 'm');
|
|
813
|
+
const match = String(text || '').match(pattern);
|
|
814
|
+
return match ? match[1].trim() : '';
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function dedupeConsensusItems(items) {
|
|
818
|
+
const byId = new Map();
|
|
819
|
+
for (const item of items) {
|
|
820
|
+
const existing = byId.get(item.id);
|
|
821
|
+
if (!existing) {
|
|
822
|
+
byId.set(item.id, item);
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
byId.set(item.id, {
|
|
826
|
+
...existing,
|
|
827
|
+
...item,
|
|
828
|
+
severity: item.severity || existing.severity,
|
|
829
|
+
detail: item.detail || existing.detail,
|
|
830
|
+
evidence: item.evidence || existing.evidence,
|
|
831
|
+
reviews: item.reviews.length >= existing.reviews.length ? item.reviews : existing.reviews,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
return [...byId.values()];
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function normalizeSeverity(value) {
|
|
838
|
+
const text = String(value || '').toLowerCase();
|
|
839
|
+
if (['high', 'medium', 'low'].includes(text)) return text;
|
|
840
|
+
if (text.includes('高')) return 'high';
|
|
841
|
+
if (text.includes('低')) return 'low';
|
|
842
|
+
return 'medium';
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function compactText(text) {
|
|
846
|
+
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function escapeRegExp(text) {
|
|
850
|
+
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function validateProviders(providers) {
|
|
854
|
+
if (providers.length === 0) {
|
|
855
|
+
throw new Error('providers is empty');
|
|
856
|
+
}
|
|
857
|
+
for (const provider of providers) {
|
|
858
|
+
if (!DEFAULT_PROVIDERS.includes(provider)) {
|
|
859
|
+
throw new Error(`unsupported review provider "${provider}". Supported: ${DEFAULT_PROVIDERS.join(', ')}`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function resolveProviderPermissionMode(args, provider) {
|
|
865
|
+
if (provider !== 'claude') return args['permission-mode'] || args.permissionMode || '';
|
|
866
|
+
return args['claude-permission-mode']
|
|
867
|
+
|| args['permission-mode']
|
|
868
|
+
|| args.permissionMode
|
|
869
|
+
|| 'bypassPermissions';
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function resolveClaudeArgs(args, provider) {
|
|
873
|
+
if (provider !== 'claude') return [];
|
|
874
|
+
const values = [];
|
|
875
|
+
if (!args['no-claude-safe-mode']) {
|
|
876
|
+
values.push('--safe-mode');
|
|
877
|
+
}
|
|
878
|
+
if (!args['claude-effort']) {
|
|
879
|
+
values.push('--effort', 'low');
|
|
880
|
+
} else if (args['claude-effort'] !== 'default') {
|
|
881
|
+
values.push('--effort', args['claude-effort']);
|
|
882
|
+
}
|
|
883
|
+
if (args['claude-arg']) {
|
|
884
|
+
const extra = Array.isArray(args['claude-arg']) ? args['claude-arg'] : [args['claude-arg']];
|
|
885
|
+
values.push(...extra.filter(Boolean));
|
|
886
|
+
}
|
|
887
|
+
return values;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function resolveReportRoot(args, cwd) {
|
|
891
|
+
const explicit = args['report-root'] || args['output-root'];
|
|
892
|
+
if (explicit) {
|
|
893
|
+
return path.resolve(cwd || process.cwd(), explicit);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (process.env.ALANBOX_REVIEW_ROOT) {
|
|
897
|
+
return path.resolve(process.env.ALANBOX_REVIEW_ROOT);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const desktopAllProject = path.join(os.homedir(), 'Desktop', 'all-project');
|
|
901
|
+
if (fs.existsSync(desktopAllProject)) {
|
|
902
|
+
return path.join(desktopAllProject, '.tmp', 'swarm');
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return path.resolve(cwd || process.cwd(), '.tmp', 'swarm');
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function resolveReviewReportPath(args, reportRoot, target) {
|
|
909
|
+
if (args['report-file']) {
|
|
910
|
+
return path.resolve(args['report-file']);
|
|
911
|
+
}
|
|
912
|
+
if (args['report-dir']) {
|
|
913
|
+
return path.join(path.resolve(args['report-dir']), defaultReportFileName(target));
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return path.join(reportRoot, defaultReportFileName(target));
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function defaultReportFileName(target) {
|
|
920
|
+
return `${formatLocalDateTime(new Date())}-${buildRunHash(target.path)}.md`;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function buildRunHash(targetPath) {
|
|
924
|
+
return crypto
|
|
925
|
+
.createHash('sha1')
|
|
926
|
+
.update([targetPath, Date.now(), process.pid, crypto.randomBytes(8).toString('hex')].join('\n'))
|
|
927
|
+
.digest('hex')
|
|
928
|
+
.slice(0, 8);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function formatLocalDateTime(date) {
|
|
932
|
+
const year = date.getFullYear();
|
|
933
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
934
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
935
|
+
const hour = String(date.getHours()).padStart(2, '0');
|
|
936
|
+
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
937
|
+
const second = String(date.getSeconds()).padStart(2, '0');
|
|
938
|
+
return `${year}-${month}-${day}-${hour}${minute}${second}`;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function buildExtraContext(args) {
|
|
942
|
+
const parts = [];
|
|
943
|
+
if (args.context) parts.push(String(args.context));
|
|
944
|
+
if (args['context-file']) {
|
|
945
|
+
parts.push(readTextWithLimit(path.resolve(args['context-file']), PEER_REPORT_EXCERPT_LIMIT));
|
|
946
|
+
}
|
|
947
|
+
return parts.join('\n\n').trim();
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function readTextWithLimit(filePath, limit) {
|
|
951
|
+
if (!filePath) return '';
|
|
952
|
+
let text = '';
|
|
953
|
+
try {
|
|
954
|
+
text = fs.readFileSync(filePath, 'utf8');
|
|
955
|
+
} catch (error) {
|
|
956
|
+
return `<failed to read ${filePath}: ${error.message}>`;
|
|
957
|
+
}
|
|
958
|
+
return trimText(text, limit);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function trimText(text, limit) {
|
|
962
|
+
const source = String(text || '');
|
|
963
|
+
if (source.length <= limit) return source;
|
|
964
|
+
return `${source.slice(0, limit)}\n\n[truncated ${source.length - limit} chars]`;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function escapeMarkdownInline(text) {
|
|
968
|
+
return String(text || '').replace(/\s+/g, ' ').replace(/\|/g, '\\|');
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
module.exports = {
|
|
972
|
+
runReviewFile,
|
|
973
|
+
resolveReviewTarget,
|
|
974
|
+
resolveReviewProviders,
|
|
975
|
+
resolveReviewPhase,
|
|
976
|
+
resolveCrossCheckInitialProviders,
|
|
977
|
+
resolveReportRoot,
|
|
978
|
+
resolveReviewReportPath,
|
|
979
|
+
resolveProviderPermissionMode,
|
|
980
|
+
resolveClaudeArgs,
|
|
981
|
+
buildInitialReviewPrompt,
|
|
982
|
+
buildCrossCheckPrompt,
|
|
983
|
+
buildConsensusPrompt,
|
|
984
|
+
buildConsensusItems,
|
|
985
|
+
buildConsensusItemsSection,
|
|
986
|
+
buildConsensusRoundSection,
|
|
987
|
+
buildFinalConsensusSummary,
|
|
988
|
+
formatConsensusTag,
|
|
989
|
+
parseFindingsFromOutput,
|
|
990
|
+
detectInvalidInitialReview,
|
|
991
|
+
parseConsensusDecision,
|
|
992
|
+
extractConsensusJson,
|
|
993
|
+
parseConsensusItemsFromReport,
|
|
994
|
+
resolveReviewKind,
|
|
995
|
+
isConsensusComplete,
|
|
996
|
+
formatLocalDateTime,
|
|
997
|
+
};
|