@boyingliu01/xp-gate 0.14.30 → 0.14.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/adapter-common.sh +0 -0
  2. package/adapters/cpp.sh +0 -0
  3. package/adapters/dart.sh +0 -0
  4. package/adapters/flutter.sh +0 -0
  5. package/adapters/go.sh +0 -0
  6. package/adapters/iac.sh +0 -0
  7. package/adapters/java.sh +0 -0
  8. package/adapters/kotlin.sh +0 -0
  9. package/adapters/objectivec.sh +0 -0
  10. package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
  11. package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
  12. package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
  13. package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
  14. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
  15. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
  16. package/adapters/powershell.sh +0 -0
  17. package/adapters/python.sh +0 -0
  18. package/adapters/shell.sh +0 -0
  19. package/adapters/swift.sh +0 -0
  20. package/adapters/typescript.sh +0 -0
  21. package/bin/xp-gate.js +0 -0
  22. package/gate-3.sh +0 -0
  23. package/gate-4.sh +0 -0
  24. package/gate-7.sh +0 -0
  25. package/gate-8.sh +0 -0
  26. package/gate-9.sh +0 -0
  27. package/hooks/adapter-common.sh +0 -0
  28. package/hooks/gate-3.sh +0 -0
  29. package/hooks/gate-4.sh +0 -0
  30. package/hooks/gate-7.sh +0 -0
  31. package/hooks/gate-8.sh +0 -0
  32. package/hooks/gate-9.sh +0 -0
  33. package/hooks/pre-commit +0 -0
  34. package/hooks/pre-push +0 -0
  35. package/lib/__tests__/phase-transition.test.js +272 -272
  36. package/lib/__tests__/sprint-audit.test.js +207 -207
  37. package/lib/phase-transition.js +249 -249
  38. package/lib/sprint-audit.js +235 -235
  39. package/mock-policy/AGENTS.md +2 -2
  40. package/mutation/AGENTS.md +2 -2
  41. package/package.json +2 -2
  42. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  43. package/plugins/claude-code/bin/delphi-review-guard.sh +0 -0
  44. package/plugins/claude-code/bin/xp-gate-check +0 -0
  45. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  46. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  47. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  48. package/plugins/opencode/package.json +1 -1
  49. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  50. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  51. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  52. package/plugins/qoder/agents/delphi-architecture.md +48 -48
  53. package/plugins/qoder/agents/delphi-feasibility.md +48 -48
  54. package/plugins/qoder/agents/delphi-technical.md +48 -48
  55. package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
  56. package/plugins/qoder/bin/xp-gate-check +0 -0
  57. package/plugins/qoder/plugin.json +1 -1
  58. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  59. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  60. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  61. package/principles/AGENTS.md +2 -2
  62. package/scripts/delphi-external-review.cjs +432 -432
  63. package/skills/delphi-review/AGENTS.md +2 -2
  64. package/skills/sprint-flow/AGENTS.md +2 -2
  65. package/skills/test-specification-alignment/AGENTS.md +2 -2
@@ -1,432 +1,432 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
7
-
8
- // ── Node.js version check ──────────────────────────────────────────────
9
- function checkNodeVersion() {
10
- const major = parseInt(process.versions.node.split('.')[0]);
11
- if (major < 18) {
12
- console.error(`[delphi-review] ERROR: Node.js >= 18 required (current: ${process.versions.node})`);
13
- process.exit(1);
14
- }
15
- return true;
16
- }
17
-
18
- // ── Argument parsing ───────────────────────────────────────────────────
19
- const VALID_EXPERTS = ['architecture', 'technical', 'feasibility'];
20
- const VALID_MODES = ['design', 'code-walkthrough'];
21
-
22
- function parseArgs(argv) {
23
- const args = {};
24
- for (let i = 0; i < argv.length; i++) {
25
- switch (argv[i]) {
26
- case '--expert': args.expert = argv[++i]; break;
27
- case '--input': args.input = argv[++i]; break;
28
- case '--input-file': args.inputFile = argv[++i]; break;
29
- case '--round': args.round = parseInt(argv[++i]); break;
30
- case '--config': args.config = argv[++i]; break;
31
- case '--mode': args.mode = argv[++i]; break;
32
- case '--profile': args.profile = argv[++i]; break;
33
- case '--other-experts-file': args.otherExpertsFile = argv[++i]; break;
34
- case '--fallback-local': args.fallbackLocal = true; break;
35
- default: break;
36
- }
37
- }
38
-
39
- // Validate required args
40
- const missing = [];
41
- if (!args.expert) missing.push('--expert');
42
- if (!args.input && !args.inputFile) missing.push('--input or --input-file');
43
- if (!args.round) missing.push('--round');
44
- if (!args.config) missing.push('--config');
45
-
46
- if (missing.length > 0) {
47
- console.error(`[delphi-review] ERROR: Missing required arguments: ${missing.join(', ')}`);
48
- process.exit(1);
49
- }
50
-
51
- // Validate expert role
52
- if (!VALID_EXPERTS.includes(args.expert)) {
53
- console.error(`[delphi-review] ERROR: Invalid expert role "${args.expert}". Must be one of: ${VALID_EXPERTS.join(', ')}`);
54
- process.exit(1);
55
- }
56
-
57
- // Validate mutual exclusivity of --input and --input-file
58
- if (args.input && args.inputFile) {
59
- console.error('[delphi-review] ERROR: --input and --input-file are mutually exclusive. Use only one.');
60
- process.exit(1);
61
- }
62
-
63
- // Defaults
64
- if (!args.mode) args.mode = 'design';
65
- if (!VALID_MODES.includes(args.mode)) {
66
- console.error(`[delphi-review] ERROR: Invalid mode "${args.mode}". Must be one of: ${VALID_MODES.join(', ')}`);
67
- process.exit(1);
68
- }
69
-
70
- return args;
71
- }
72
-
73
- // ── Config reading ─────────────────────────────────────────────────────
74
- function readConfig(configPath, profileOverride) {
75
- if (!fs.existsSync(configPath)) {
76
- console.error(`[delphi-review] ERROR: Config file not found: ${configPath}`);
77
- console.error('[delphi-review] Copy .delphi-config.json.example to .delphi-config.json and fill in your API keys.');
78
- process.exit(1);
79
- }
80
-
81
- let raw;
82
- try {
83
- raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
84
- } catch (err) {
85
- console.error(`[delphi-review] ERROR: Failed to parse config: ${err.message}`);
86
- process.exit(1);
87
- }
88
-
89
- const profileName = profileOverride || raw.active_profile || 'default';
90
- const profile = raw.profiles?.[profileName];
91
-
92
- if (!profile) {
93
- console.error(`[delphi-review] ERROR: Profile "${profileName}" not found in config.`);
94
- console.error(`[delphi-review] Available profiles: ${Object.keys(raw.profiles || {}).join(', ')}`);
95
- process.exit(1);
96
- }
97
-
98
- // Resolve ${ENV_VAR} references in provider api_key fields
99
- const providers = profile.providers || {};
100
- for (const [name, prov] of Object.entries(providers)) {
101
- if (prov.api_key && prov.api_key.startsWith('${') && prov.api_key.endsWith('}')) {
102
- const envName = prov.api_key.slice(2, -1);
103
- const envVal = process.env[envName];
104
- if (envVal) {
105
- prov.api_key = envVal;
106
- } else {
107
- console.error(`[delphi-review] WARNING: Environment variable ${envName} not set (provider: ${name}). API calls will fail.`);
108
- }
109
- }
110
- }
111
-
112
- return {
113
- active_profile: profileName,
114
- providers,
115
- experts: profile.experts || {},
116
- consensus: raw.consensus || { threshold_percent: 90, max_review_rounds: 5 },
117
- };
118
- }
119
-
120
- // ── Cross-provider validation ──────────────────────────────────────────
121
- function validateCrossProvider(experts, providers) {
122
- const nonLocalExperts = Object.values(experts).filter(e => e.provider !== 'local');
123
-
124
- if (nonLocalExperts.length === 0) {
125
- return { valid: true, warning: 'All experts are "local" — no cross-model diversity. Consider configuring external APIs.' };
126
- }
127
-
128
- const uniqueProviders = new Set(nonLocalExperts.map(e => e.provider));
129
-
130
- if (uniqueProviders.size < 2 && nonLocalExperts.length >= 2) {
131
- return {
132
- valid: false,
133
- reason: `All ${nonLocalExperts.length} non-local experts use the same provider. At least 2 different providers required for cross-model diversity.`,
134
- };
135
- }
136
-
137
- return { valid: true };
138
- }
139
-
140
- // ── JSON extraction (4-layer fallback) ─────────────────────────────────
141
- function extractJsonFromResponse(content) {
142
- if (!content || typeof content !== 'string') {
143
- return { parse_error: true, raw_content: String(content || '') };
144
- }
145
-
146
- const trimmed = content.trim();
147
-
148
- // Layer 1: Direct parse
149
- try {
150
- return JSON.parse(trimmed);
151
- } catch { /* continue */ }
152
-
153
- // Layer 2: Strip markdown code block
154
- const codeBlockMatch = trimmed.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
155
- if (codeBlockMatch) {
156
- try {
157
- return JSON.parse(codeBlockMatch[1].trim());
158
- } catch { /* continue */ }
159
- }
160
-
161
- // Layer 3: Extract first JSON object
162
- const jsonMatch = trimmed.match(/\{[\s\S]*\}/);
163
- if (jsonMatch) {
164
- try {
165
- return JSON.parse(jsonMatch[0]);
166
- } catch { /* continue */ }
167
- }
168
-
169
- // Layer 4: Fallback
170
- return { parse_error: true, raw_content: trimmed };
171
- }
172
-
173
- // ── System prompt templates ────────────────────────────────────────────
174
- const SYSTEM_PROMPTS = {
175
- architecture: `你是架构评审专家(Delphi Method - Architecture Expert)。
176
-
177
- 你的评审关注维度:
178
- 1) 需求对齐度 — 设计是否完整覆盖所有需求
179
- 2) 系统一致性 — 与现有架构风格、模式是否一致
180
- 3) 模块边界清晰度 — 职责划分是否明确,耦合度是否合理
181
- 4) 架构演进性 — 是否为未来扩展留有空间
182
- 5) 技术选型合理性 — 技术栈选择是否有充分依据
183
-
184
- 输出要求:返回结构化 JSON,包含 verdict (APPROVED/REQUEST_CHANGES/REJECTED)、confidence (1-10)、critical_issues、major_concerns、minor_concerns、summary。`,
185
-
186
- technical: `你是技术实现评审专家(Delphi Method - Technical Expert)。
187
-
188
- 你的评审关注维度:
189
- 1) 实现正确性 — 逻辑是否正确,边界条件是否处理
190
- 2) 代码质量和设计模式 — 是否遵循 SOLID 原则,代码是否清晰
191
- 3) 边界情况和错误处理 — 异常路径是否覆盖,错误处理是否健壮
192
- 4) 性能影响 — 是否有性能瓶颈或资源泄漏风险
193
- 5) 可测试性 — 代码是否易于编写单元测试
194
-
195
- 输出要求:返回结构化 JSON,包含 verdict (APPROVED/REQUEST_CHANGES/REJECTED)、confidence (1-10)、critical_issues、major_concerns、minor_concerns、summary。`,
196
-
197
- feasibility: `你是可行性分析专家(Delphi Method - Feasibility Expert)。
198
-
199
- 你的评审关注维度:
200
- 1) 实际约束(时间/资源/依赖)— 是否在现有约束下可行
201
- 2) 风险识别和缓解 — 主要风险是否已识别,缓解措施是否充分
202
- 3) 执行复杂度 — 实现难度是否被低估,依赖链是否清晰
203
- 4) 替代方案 — 是否有更简单或更可靠的替代方案
204
- 5) 回滚策略 — 如果实施失败,是否有退路
205
-
206
- 输出要求:返回结构化 JSON,包含 verdict (APPROVED/REQUEST_CHANGES/REJECTED)、confidence (1-10)、critical_issues、major_concerns、minor_concerns、summary。`,
207
- };
208
-
209
- function buildSystemPrompt(role) {
210
- return SYSTEM_PROMPTS[role] || SYSTEM_PROMPTS.architecture;
211
- }
212
-
213
- // ── User prompt construction ───────────────────────────────────────────
214
- function buildUserPrompt(reviewContent, otherExpertsJson, round) {
215
- let prompt = `请评审以下内容(Round ${round}):\n\n---\n${reviewContent}\n---`;
216
-
217
- if (otherExpertsJson && round > 1) {
218
- prompt += `\n\n以下是其他专家在 Round ${round - 1} 中的意见,请在考虑这些意见后重新评审:\n\n${otherExpertsJson}`;
219
- }
220
-
221
- return prompt;
222
- }
223
-
224
- // ── Input content resolution ───────────────────────────────────────────
225
- function resolveInputContent(args) {
226
- if (args.inputFile) {
227
- if (!fs.existsSync(args.inputFile)) {
228
- console.error(`[delphi-review] ERROR: Input file not found: ${args.inputFile}`);
229
- process.exit(1);
230
- }
231
- return fs.readFileSync(args.inputFile, 'utf8');
232
- }
233
- return args.input || '';
234
- }
235
-
236
- // ── Local fallback ─────────────────────────────────────────────────────
237
- function resolveLocalFallback(expertRole) {
238
- return {
239
- fallback: true,
240
- expert_role: expertRole,
241
- reason: 'local',
242
- };
243
- }
244
-
245
- // ── API call ───────────────────────────────────────────────────────────
246
- async function callModelAPI(providerConfig, model, systemPrompt, userPrompt) {
247
- const url = `${providerConfig.base_url.replace(/\/$/, '')}/chat/completions`;
248
-
249
- const body = {
250
- model: model,
251
- messages: [
252
- { role: 'system', content: systemPrompt },
253
- { role: 'user', content: userPrompt },
254
- ],
255
- temperature: 0.3,
256
- response_format: { type: 'json_object' },
257
- };
258
-
259
- const controller = new AbortController();
260
- const timeout = setTimeout(() => controller.abort(), 30000);
261
-
262
- try {
263
- const response = await fetch(url, {
264
- method: 'POST',
265
- headers: {
266
- 'Content-Type': 'application/json',
267
- 'Authorization': `Bearer ${providerConfig.api_key}`,
268
- },
269
- body: JSON.stringify(body),
270
- signal: controller.signal,
271
- });
272
-
273
- clearTimeout(timeout);
274
-
275
- if (response.status === 401 || response.status === 403) {
276
- return { error: true, message: `Authentication failed (${response.status}). Check API key in .delphi-config.json.` };
277
- }
278
-
279
- if (response.status === 429) {
280
- return { error: true, retryable: true, message: 'Rate limit exceeded (429).' };
281
- }
282
-
283
- if (response.status >= 500) {
284
- return { error: true, retryable: true, message: `Server error (${response.status}).` };
285
- }
286
-
287
- if (!response.ok) {
288
- const text = await response.text();
289
- return { error: true, message: `API error (${response.status}): ${text}` };
290
- }
291
-
292
- const data = await response.json();
293
- const content = data.choices?.[0]?.message?.content;
294
- if (!content) {
295
- return { error: true, message: 'Empty response from model.' };
296
- }
297
-
298
- return { success: true, content };
299
- } catch (err) {
300
- clearTimeout(timeout);
301
- if (err.name === 'AbortError') {
302
- return { error: true, retryable: true, message: 'Request timed out (30s).' };
303
- }
304
- return { error: true, message: `Network error: ${err.message}` };
305
- }
306
- }
307
-
308
- // ── Retry logic ────────────────────────────────────────────────────────
309
- async function callWithRetry(providerConfig, model, systemPrompt, userPrompt, maxRetries = 2) {
310
- let lastResult;
311
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
312
- const result = await callModelAPI(providerConfig, model, systemPrompt, userPrompt);
313
-
314
- if (result.success) return result;
315
- if (!result.retryable) return result;
316
-
317
- lastResult = result;
318
- if (attempt < maxRetries) {
319
- const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
320
- console.error(`[delphi-review] Retryable error: ${result.message}. Retrying in ${delay}ms...`);
321
- await new Promise(r => setTimeout(r, delay));
322
- }
323
- }
324
- return lastResult;
325
- }
326
-
327
- // ── Main execution ─────────────────────────────────────────────────────
328
- async function main() {
329
- checkNodeVersion();
330
-
331
- const args = parseArgs(process.argv.slice(2));
332
- const config = readConfig(args.config, args.profile);
333
-
334
- // Get expert config
335
- const expertConfig = config.experts[args.expert];
336
- if (!expertConfig) {
337
- console.error(`[delphi-review] ERROR: Expert "${args.expert}" not found in config.`);
338
- process.exit(1);
339
- }
340
-
341
- // Handle local fallback
342
- if (expertConfig.provider === 'local') {
343
- const fallback = resolveLocalFallback(args.expert);
344
- console.log(JSON.stringify(fallback));
345
- return;
346
- }
347
-
348
- // Validate cross-provider
349
- const validation = validateCrossProvider(config.experts, config.providers);
350
- if (!validation.valid) {
351
- console.error(`[delphi-review] ERROR: ${validation.reason}`);
352
- process.exit(1);
353
- }
354
- if (validation.warning) {
355
- console.error(`[delphi-review] WARNING: ${validation.warning}`);
356
- }
357
-
358
- // Get provider config
359
- const provider = config.providers[expertConfig.provider];
360
- if (!provider) {
361
- console.error(`[delphi-review] ERROR: Provider "${expertConfig.provider}" not found in config.`);
362
- process.exit(1);
363
- }
364
-
365
- // Resolve input
366
- const reviewContent = resolveInputContent(args);
367
-
368
- // Resolve other experts context
369
- let otherExpertsContent = null;
370
- if (args.otherExpertsFile && fs.existsSync(args.otherExpertsFile)) {
371
- otherExpertsContent = fs.readFileSync(args.otherExpertsFile, 'utf8');
372
- }
373
-
374
- // Build prompts
375
- const systemPrompt = buildSystemPrompt(args.expert);
376
- const userPrompt = buildUserPrompt(reviewContent, otherExpertsContent, args.round);
377
-
378
- // Call API with retry
379
- const result = await callWithRetry(provider, expertConfig.model, systemPrompt, userPrompt);
380
-
381
- if (result.error) {
382
- const errorOutput = {
383
- error: true,
384
- expert_role: args.expert,
385
- message: result.message,
386
- retryable: result.retryable || false,
387
- };
388
- console.log(JSON.stringify(errorOutput));
389
- process.exit(1);
390
- }
391
-
392
- // Extract JSON from response
393
- const verdict = extractJsonFromResponse(result.content);
394
-
395
- if (verdict.parse_error) {
396
- console.error(`[delphi-review] WARNING: Could not parse model response as JSON.`);
397
- }
398
-
399
- // Enrich output
400
- const output = {
401
- ...verdict,
402
- expert_id: { architecture: 'A', technical: 'B', feasibility: 'C' }[args.expert],
403
- expert_role: args.expert,
404
- model_used: `${expertConfig.provider}/${expertConfig.model}`,
405
- round: args.round,
406
- mode: args.mode,
407
- };
408
-
409
- console.log(JSON.stringify(output, null, 2));
410
- }
411
-
412
- // ── Module exports (for testing) ───────────────────────────────────────
413
- if (require.main !== module) {
414
- module.exports = {
415
- parseArgs,
416
- readConfig,
417
- validateCrossProvider,
418
- extractJsonFromResponse,
419
- buildSystemPrompt,
420
- buildUserPrompt,
421
- resolveInputContent,
422
- resolveLocalFallback,
423
- checkNodeVersion,
424
- callModelAPI,
425
- callWithRetry,
426
- };
427
- } else {
428
- main().catch(err => {
429
- console.error(`[delphi-review] FATAL: ${err.message}`);
430
- process.exit(1);
431
- });
432
- }
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+
8
+ // ── Node.js version check ──────────────────────────────────────────────
9
+ function checkNodeVersion() {
10
+ const major = parseInt(process.versions.node.split('.')[0]);
11
+ if (major < 18) {
12
+ console.error(`[delphi-review] ERROR: Node.js >= 18 required (current: ${process.versions.node})`);
13
+ process.exit(1);
14
+ }
15
+ return true;
16
+ }
17
+
18
+ // ── Argument parsing ───────────────────────────────────────────────────
19
+ const VALID_EXPERTS = ['architecture', 'technical', 'feasibility'];
20
+ const VALID_MODES = ['design', 'code-walkthrough'];
21
+
22
+ function parseArgs(argv) {
23
+ const args = {};
24
+ for (let i = 0; i < argv.length; i++) {
25
+ switch (argv[i]) {
26
+ case '--expert': args.expert = argv[++i]; break;
27
+ case '--input': args.input = argv[++i]; break;
28
+ case '--input-file': args.inputFile = argv[++i]; break;
29
+ case '--round': args.round = parseInt(argv[++i]); break;
30
+ case '--config': args.config = argv[++i]; break;
31
+ case '--mode': args.mode = argv[++i]; break;
32
+ case '--profile': args.profile = argv[++i]; break;
33
+ case '--other-experts-file': args.otherExpertsFile = argv[++i]; break;
34
+ case '--fallback-local': args.fallbackLocal = true; break;
35
+ default: break;
36
+ }
37
+ }
38
+
39
+ // Validate required args
40
+ const missing = [];
41
+ if (!args.expert) missing.push('--expert');
42
+ if (!args.input && !args.inputFile) missing.push('--input or --input-file');
43
+ if (!args.round) missing.push('--round');
44
+ if (!args.config) missing.push('--config');
45
+
46
+ if (missing.length > 0) {
47
+ console.error(`[delphi-review] ERROR: Missing required arguments: ${missing.join(', ')}`);
48
+ process.exit(1);
49
+ }
50
+
51
+ // Validate expert role
52
+ if (!VALID_EXPERTS.includes(args.expert)) {
53
+ console.error(`[delphi-review] ERROR: Invalid expert role "${args.expert}". Must be one of: ${VALID_EXPERTS.join(', ')}`);
54
+ process.exit(1);
55
+ }
56
+
57
+ // Validate mutual exclusivity of --input and --input-file
58
+ if (args.input && args.inputFile) {
59
+ console.error('[delphi-review] ERROR: --input and --input-file are mutually exclusive. Use only one.');
60
+ process.exit(1);
61
+ }
62
+
63
+ // Defaults
64
+ if (!args.mode) args.mode = 'design';
65
+ if (!VALID_MODES.includes(args.mode)) {
66
+ console.error(`[delphi-review] ERROR: Invalid mode "${args.mode}". Must be one of: ${VALID_MODES.join(', ')}`);
67
+ process.exit(1);
68
+ }
69
+
70
+ return args;
71
+ }
72
+
73
+ // ── Config reading ─────────────────────────────────────────────────────
74
+ function readConfig(configPath, profileOverride) {
75
+ if (!fs.existsSync(configPath)) {
76
+ console.error(`[delphi-review] ERROR: Config file not found: ${configPath}`);
77
+ console.error('[delphi-review] Copy .delphi-config.json.example to .delphi-config.json and fill in your API keys.');
78
+ process.exit(1);
79
+ }
80
+
81
+ let raw;
82
+ try {
83
+ raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
84
+ } catch (err) {
85
+ console.error(`[delphi-review] ERROR: Failed to parse config: ${err.message}`);
86
+ process.exit(1);
87
+ }
88
+
89
+ const profileName = profileOverride || raw.active_profile || 'default';
90
+ const profile = raw.profiles?.[profileName];
91
+
92
+ if (!profile) {
93
+ console.error(`[delphi-review] ERROR: Profile "${profileName}" not found in config.`);
94
+ console.error(`[delphi-review] Available profiles: ${Object.keys(raw.profiles || {}).join(', ')}`);
95
+ process.exit(1);
96
+ }
97
+
98
+ // Resolve ${ENV_VAR} references in provider api_key fields
99
+ const providers = profile.providers || {};
100
+ for (const [name, prov] of Object.entries(providers)) {
101
+ if (prov.api_key && prov.api_key.startsWith('${') && prov.api_key.endsWith('}')) {
102
+ const envName = prov.api_key.slice(2, -1);
103
+ const envVal = process.env[envName];
104
+ if (envVal) {
105
+ prov.api_key = envVal;
106
+ } else {
107
+ console.error(`[delphi-review] WARNING: Environment variable ${envName} not set (provider: ${name}). API calls will fail.`);
108
+ }
109
+ }
110
+ }
111
+
112
+ return {
113
+ active_profile: profileName,
114
+ providers,
115
+ experts: profile.experts || {},
116
+ consensus: raw.consensus || { threshold_percent: 90, max_review_rounds: 5 },
117
+ };
118
+ }
119
+
120
+ // ── Cross-provider validation ──────────────────────────────────────────
121
+ function validateCrossProvider(experts, providers) {
122
+ const nonLocalExperts = Object.values(experts).filter(e => e.provider !== 'local');
123
+
124
+ if (nonLocalExperts.length === 0) {
125
+ return { valid: true, warning: 'All experts are "local" — no cross-model diversity. Consider configuring external APIs.' };
126
+ }
127
+
128
+ const uniqueProviders = new Set(nonLocalExperts.map(e => e.provider));
129
+
130
+ if (uniqueProviders.size < 2 && nonLocalExperts.length >= 2) {
131
+ return {
132
+ valid: false,
133
+ reason: `All ${nonLocalExperts.length} non-local experts use the same provider. At least 2 different providers required for cross-model diversity.`,
134
+ };
135
+ }
136
+
137
+ return { valid: true };
138
+ }
139
+
140
+ // ── JSON extraction (4-layer fallback) ─────────────────────────────────
141
+ function extractJsonFromResponse(content) {
142
+ if (!content || typeof content !== 'string') {
143
+ return { parse_error: true, raw_content: String(content || '') };
144
+ }
145
+
146
+ const trimmed = content.trim();
147
+
148
+ // Layer 1: Direct parse
149
+ try {
150
+ return JSON.parse(trimmed);
151
+ } catch { /* continue */ }
152
+
153
+ // Layer 2: Strip markdown code block
154
+ const codeBlockMatch = trimmed.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
155
+ if (codeBlockMatch) {
156
+ try {
157
+ return JSON.parse(codeBlockMatch[1].trim());
158
+ } catch { /* continue */ }
159
+ }
160
+
161
+ // Layer 3: Extract first JSON object
162
+ const jsonMatch = trimmed.match(/\{[\s\S]*\}/);
163
+ if (jsonMatch) {
164
+ try {
165
+ return JSON.parse(jsonMatch[0]);
166
+ } catch { /* continue */ }
167
+ }
168
+
169
+ // Layer 4: Fallback
170
+ return { parse_error: true, raw_content: trimmed };
171
+ }
172
+
173
+ // ── System prompt templates ────────────────────────────────────────────
174
+ const SYSTEM_PROMPTS = {
175
+ architecture: `你是架构评审专家(Delphi Method - Architecture Expert)。
176
+
177
+ 你的评审关注维度:
178
+ 1) 需求对齐度 — 设计是否完整覆盖所有需求
179
+ 2) 系统一致性 — 与现有架构风格、模式是否一致
180
+ 3) 模块边界清晰度 — 职责划分是否明确,耦合度是否合理
181
+ 4) 架构演进性 — 是否为未来扩展留有空间
182
+ 5) 技术选型合理性 — 技术栈选择是否有充分依据
183
+
184
+ 输出要求:返回结构化 JSON,包含 verdict (APPROVED/REQUEST_CHANGES/REJECTED)、confidence (1-10)、critical_issues、major_concerns、minor_concerns、summary。`,
185
+
186
+ technical: `你是技术实现评审专家(Delphi Method - Technical Expert)。
187
+
188
+ 你的评审关注维度:
189
+ 1) 实现正确性 — 逻辑是否正确,边界条件是否处理
190
+ 2) 代码质量和设计模式 — 是否遵循 SOLID 原则,代码是否清晰
191
+ 3) 边界情况和错误处理 — 异常路径是否覆盖,错误处理是否健壮
192
+ 4) 性能影响 — 是否有性能瓶颈或资源泄漏风险
193
+ 5) 可测试性 — 代码是否易于编写单元测试
194
+
195
+ 输出要求:返回结构化 JSON,包含 verdict (APPROVED/REQUEST_CHANGES/REJECTED)、confidence (1-10)、critical_issues、major_concerns、minor_concerns、summary。`,
196
+
197
+ feasibility: `你是可行性分析专家(Delphi Method - Feasibility Expert)。
198
+
199
+ 你的评审关注维度:
200
+ 1) 实际约束(时间/资源/依赖)— 是否在现有约束下可行
201
+ 2) 风险识别和缓解 — 主要风险是否已识别,缓解措施是否充分
202
+ 3) 执行复杂度 — 实现难度是否被低估,依赖链是否清晰
203
+ 4) 替代方案 — 是否有更简单或更可靠的替代方案
204
+ 5) 回滚策略 — 如果实施失败,是否有退路
205
+
206
+ 输出要求:返回结构化 JSON,包含 verdict (APPROVED/REQUEST_CHANGES/REJECTED)、confidence (1-10)、critical_issues、major_concerns、minor_concerns、summary。`,
207
+ };
208
+
209
+ function buildSystemPrompt(role) {
210
+ return SYSTEM_PROMPTS[role] || SYSTEM_PROMPTS.architecture;
211
+ }
212
+
213
+ // ── User prompt construction ───────────────────────────────────────────
214
+ function buildUserPrompt(reviewContent, otherExpertsJson, round) {
215
+ let prompt = `请评审以下内容(Round ${round}):\n\n---\n${reviewContent}\n---`;
216
+
217
+ if (otherExpertsJson && round > 1) {
218
+ prompt += `\n\n以下是其他专家在 Round ${round - 1} 中的意见,请在考虑这些意见后重新评审:\n\n${otherExpertsJson}`;
219
+ }
220
+
221
+ return prompt;
222
+ }
223
+
224
+ // ── Input content resolution ───────────────────────────────────────────
225
+ function resolveInputContent(args) {
226
+ if (args.inputFile) {
227
+ if (!fs.existsSync(args.inputFile)) {
228
+ console.error(`[delphi-review] ERROR: Input file not found: ${args.inputFile}`);
229
+ process.exit(1);
230
+ }
231
+ return fs.readFileSync(args.inputFile, 'utf8');
232
+ }
233
+ return args.input || '';
234
+ }
235
+
236
+ // ── Local fallback ─────────────────────────────────────────────────────
237
+ function resolveLocalFallback(expertRole) {
238
+ return {
239
+ fallback: true,
240
+ expert_role: expertRole,
241
+ reason: 'local',
242
+ };
243
+ }
244
+
245
+ // ── API call ───────────────────────────────────────────────────────────
246
+ async function callModelAPI(providerConfig, model, systemPrompt, userPrompt) {
247
+ const url = `${providerConfig.base_url.replace(/\/$/, '')}/chat/completions`;
248
+
249
+ const body = {
250
+ model: model,
251
+ messages: [
252
+ { role: 'system', content: systemPrompt },
253
+ { role: 'user', content: userPrompt },
254
+ ],
255
+ temperature: 0.3,
256
+ response_format: { type: 'json_object' },
257
+ };
258
+
259
+ const controller = new AbortController();
260
+ const timeout = setTimeout(() => controller.abort(), 30000);
261
+
262
+ try {
263
+ const response = await fetch(url, {
264
+ method: 'POST',
265
+ headers: {
266
+ 'Content-Type': 'application/json',
267
+ 'Authorization': `Bearer ${providerConfig.api_key}`,
268
+ },
269
+ body: JSON.stringify(body),
270
+ signal: controller.signal,
271
+ });
272
+
273
+ clearTimeout(timeout);
274
+
275
+ if (response.status === 401 || response.status === 403) {
276
+ return { error: true, message: `Authentication failed (${response.status}). Check API key in .delphi-config.json.` };
277
+ }
278
+
279
+ if (response.status === 429) {
280
+ return { error: true, retryable: true, message: 'Rate limit exceeded (429).' };
281
+ }
282
+
283
+ if (response.status >= 500) {
284
+ return { error: true, retryable: true, message: `Server error (${response.status}).` };
285
+ }
286
+
287
+ if (!response.ok) {
288
+ const text = await response.text();
289
+ return { error: true, message: `API error (${response.status}): ${text}` };
290
+ }
291
+
292
+ const data = await response.json();
293
+ const content = data.choices?.[0]?.message?.content;
294
+ if (!content) {
295
+ return { error: true, message: 'Empty response from model.' };
296
+ }
297
+
298
+ return { success: true, content };
299
+ } catch (err) {
300
+ clearTimeout(timeout);
301
+ if (err.name === 'AbortError') {
302
+ return { error: true, retryable: true, message: 'Request timed out (30s).' };
303
+ }
304
+ return { error: true, message: `Network error: ${err.message}` };
305
+ }
306
+ }
307
+
308
+ // ── Retry logic ────────────────────────────────────────────────────────
309
+ async function callWithRetry(providerConfig, model, systemPrompt, userPrompt, maxRetries = 2) {
310
+ let lastResult;
311
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
312
+ const result = await callModelAPI(providerConfig, model, systemPrompt, userPrompt);
313
+
314
+ if (result.success) return result;
315
+ if (!result.retryable) return result;
316
+
317
+ lastResult = result;
318
+ if (attempt < maxRetries) {
319
+ const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
320
+ console.error(`[delphi-review] Retryable error: ${result.message}. Retrying in ${delay}ms...`);
321
+ await new Promise(r => setTimeout(r, delay));
322
+ }
323
+ }
324
+ return lastResult;
325
+ }
326
+
327
+ // ── Main execution ─────────────────────────────────────────────────────
328
+ async function main() {
329
+ checkNodeVersion();
330
+
331
+ const args = parseArgs(process.argv.slice(2));
332
+ const config = readConfig(args.config, args.profile);
333
+
334
+ // Get expert config
335
+ const expertConfig = config.experts[args.expert];
336
+ if (!expertConfig) {
337
+ console.error(`[delphi-review] ERROR: Expert "${args.expert}" not found in config.`);
338
+ process.exit(1);
339
+ }
340
+
341
+ // Handle local fallback
342
+ if (expertConfig.provider === 'local') {
343
+ const fallback = resolveLocalFallback(args.expert);
344
+ console.log(JSON.stringify(fallback));
345
+ return;
346
+ }
347
+
348
+ // Validate cross-provider
349
+ const validation = validateCrossProvider(config.experts, config.providers);
350
+ if (!validation.valid) {
351
+ console.error(`[delphi-review] ERROR: ${validation.reason}`);
352
+ process.exit(1);
353
+ }
354
+ if (validation.warning) {
355
+ console.error(`[delphi-review] WARNING: ${validation.warning}`);
356
+ }
357
+
358
+ // Get provider config
359
+ const provider = config.providers[expertConfig.provider];
360
+ if (!provider) {
361
+ console.error(`[delphi-review] ERROR: Provider "${expertConfig.provider}" not found in config.`);
362
+ process.exit(1);
363
+ }
364
+
365
+ // Resolve input
366
+ const reviewContent = resolveInputContent(args);
367
+
368
+ // Resolve other experts context
369
+ let otherExpertsContent = null;
370
+ if (args.otherExpertsFile && fs.existsSync(args.otherExpertsFile)) {
371
+ otherExpertsContent = fs.readFileSync(args.otherExpertsFile, 'utf8');
372
+ }
373
+
374
+ // Build prompts
375
+ const systemPrompt = buildSystemPrompt(args.expert);
376
+ const userPrompt = buildUserPrompt(reviewContent, otherExpertsContent, args.round);
377
+
378
+ // Call API with retry
379
+ const result = await callWithRetry(provider, expertConfig.model, systemPrompt, userPrompt);
380
+
381
+ if (result.error) {
382
+ const errorOutput = {
383
+ error: true,
384
+ expert_role: args.expert,
385
+ message: result.message,
386
+ retryable: result.retryable || false,
387
+ };
388
+ console.log(JSON.stringify(errorOutput));
389
+ process.exit(1);
390
+ }
391
+
392
+ // Extract JSON from response
393
+ const verdict = extractJsonFromResponse(result.content);
394
+
395
+ if (verdict.parse_error) {
396
+ console.error(`[delphi-review] WARNING: Could not parse model response as JSON.`);
397
+ }
398
+
399
+ // Enrich output
400
+ const output = {
401
+ ...verdict,
402
+ expert_id: { architecture: 'A', technical: 'B', feasibility: 'C' }[args.expert],
403
+ expert_role: args.expert,
404
+ model_used: `${expertConfig.provider}/${expertConfig.model}`,
405
+ round: args.round,
406
+ mode: args.mode,
407
+ };
408
+
409
+ console.log(JSON.stringify(output, null, 2));
410
+ }
411
+
412
+ // ── Module exports (for testing) ───────────────────────────────────────
413
+ if (require.main !== module) {
414
+ module.exports = {
415
+ parseArgs,
416
+ readConfig,
417
+ validateCrossProvider,
418
+ extractJsonFromResponse,
419
+ buildSystemPrompt,
420
+ buildUserPrompt,
421
+ resolveInputContent,
422
+ resolveLocalFallback,
423
+ checkNodeVersion,
424
+ callModelAPI,
425
+ callWithRetry,
426
+ };
427
+ } else {
428
+ main().catch(err => {
429
+ console.error(`[delphi-review] FATAL: ${err.message}`);
430
+ process.exit(1);
431
+ });
432
+ }