popeye-cli 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +521 -125
  2. package/dist/adapters/claude.d.ts +16 -4
  3. package/dist/adapters/claude.d.ts.map +1 -1
  4. package/dist/adapters/claude.js +679 -33
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/adapters/gemini.d.ts +55 -0
  7. package/dist/adapters/gemini.d.ts.map +1 -0
  8. package/dist/adapters/gemini.js +318 -0
  9. package/dist/adapters/gemini.js.map +1 -0
  10. package/dist/adapters/openai.d.ts.map +1 -1
  11. package/dist/adapters/openai.js +41 -7
  12. package/dist/adapters/openai.js.map +1 -1
  13. package/dist/auth/claude.d.ts +11 -9
  14. package/dist/auth/claude.d.ts.map +1 -1
  15. package/dist/auth/claude.js +107 -71
  16. package/dist/auth/claude.js.map +1 -1
  17. package/dist/auth/gemini.d.ts +58 -0
  18. package/dist/auth/gemini.d.ts.map +1 -0
  19. package/dist/auth/gemini.js +172 -0
  20. package/dist/auth/gemini.js.map +1 -0
  21. package/dist/auth/index.d.ts +11 -7
  22. package/dist/auth/index.d.ts.map +1 -1
  23. package/dist/auth/index.js +23 -5
  24. package/dist/auth/index.js.map +1 -1
  25. package/dist/auth/keychain.d.ts +20 -7
  26. package/dist/auth/keychain.d.ts.map +1 -1
  27. package/dist/auth/keychain.js +85 -29
  28. package/dist/auth/keychain.js.map +1 -1
  29. package/dist/auth/openai.d.ts +2 -2
  30. package/dist/auth/openai.d.ts.map +1 -1
  31. package/dist/auth/openai.js +30 -32
  32. package/dist/auth/openai.js.map +1 -1
  33. package/dist/cli/interactive.d.ts.map +1 -1
  34. package/dist/cli/interactive.js +1151 -110
  35. package/dist/cli/interactive.js.map +1 -1
  36. package/dist/config/defaults.d.ts +6 -1
  37. package/dist/config/defaults.d.ts.map +1 -1
  38. package/dist/config/defaults.js +10 -2
  39. package/dist/config/defaults.js.map +1 -1
  40. package/dist/config/index.d.ts +10 -0
  41. package/dist/config/index.d.ts.map +1 -1
  42. package/dist/config/index.js +19 -0
  43. package/dist/config/index.js.map +1 -1
  44. package/dist/config/schema.d.ts +20 -0
  45. package/dist/config/schema.d.ts.map +1 -1
  46. package/dist/config/schema.js +7 -0
  47. package/dist/config/schema.js.map +1 -1
  48. package/dist/generators/python.d.ts.map +1 -1
  49. package/dist/generators/python.js +1 -0
  50. package/dist/generators/python.js.map +1 -1
  51. package/dist/generators/typescript.d.ts.map +1 -1
  52. package/dist/generators/typescript.js +1 -0
  53. package/dist/generators/typescript.js.map +1 -1
  54. package/dist/state/index.d.ts +108 -0
  55. package/dist/state/index.d.ts.map +1 -1
  56. package/dist/state/index.js +551 -4
  57. package/dist/state/index.js.map +1 -1
  58. package/dist/state/registry.d.ts +52 -0
  59. package/dist/state/registry.d.ts.map +1 -0
  60. package/dist/state/registry.js +215 -0
  61. package/dist/state/registry.js.map +1 -0
  62. package/dist/types/cli.d.ts +4 -0
  63. package/dist/types/cli.d.ts.map +1 -1
  64. package/dist/types/cli.js.map +1 -1
  65. package/dist/types/consensus.d.ts +69 -4
  66. package/dist/types/consensus.d.ts.map +1 -1
  67. package/dist/types/consensus.js +24 -3
  68. package/dist/types/consensus.js.map +1 -1
  69. package/dist/types/workflow.d.ts +55 -0
  70. package/dist/types/workflow.d.ts.map +1 -1
  71. package/dist/types/workflow.js +16 -0
  72. package/dist/types/workflow.js.map +1 -1
  73. package/dist/workflow/auto-fix.d.ts +45 -0
  74. package/dist/workflow/auto-fix.d.ts.map +1 -0
  75. package/dist/workflow/auto-fix.js +274 -0
  76. package/dist/workflow/auto-fix.js.map +1 -0
  77. package/dist/workflow/consensus.d.ts +44 -2
  78. package/dist/workflow/consensus.d.ts.map +1 -1
  79. package/dist/workflow/consensus.js +565 -17
  80. package/dist/workflow/consensus.js.map +1 -1
  81. package/dist/workflow/execution-mode.d.ts +10 -4
  82. package/dist/workflow/execution-mode.d.ts.map +1 -1
  83. package/dist/workflow/execution-mode.js +547 -58
  84. package/dist/workflow/execution-mode.js.map +1 -1
  85. package/dist/workflow/index.d.ts +14 -2
  86. package/dist/workflow/index.d.ts.map +1 -1
  87. package/dist/workflow/index.js +69 -6
  88. package/dist/workflow/index.js.map +1 -1
  89. package/dist/workflow/milestone-workflow.d.ts +34 -0
  90. package/dist/workflow/milestone-workflow.d.ts.map +1 -0
  91. package/dist/workflow/milestone-workflow.js +414 -0
  92. package/dist/workflow/milestone-workflow.js.map +1 -0
  93. package/dist/workflow/plan-mode.d.ts +14 -1
  94. package/dist/workflow/plan-mode.d.ts.map +1 -1
  95. package/dist/workflow/plan-mode.js +589 -47
  96. package/dist/workflow/plan-mode.js.map +1 -1
  97. package/dist/workflow/plan-storage.d.ts +142 -0
  98. package/dist/workflow/plan-storage.d.ts.map +1 -0
  99. package/dist/workflow/plan-storage.js +331 -0
  100. package/dist/workflow/plan-storage.js.map +1 -0
  101. package/dist/workflow/project-verification.d.ts +37 -0
  102. package/dist/workflow/project-verification.d.ts.map +1 -0
  103. package/dist/workflow/project-verification.js +381 -0
  104. package/dist/workflow/project-verification.js.map +1 -0
  105. package/dist/workflow/task-workflow.d.ts +37 -0
  106. package/dist/workflow/task-workflow.d.ts.map +1 -0
  107. package/dist/workflow/task-workflow.js +383 -0
  108. package/dist/workflow/task-workflow.js.map +1 -0
  109. package/dist/workflow/test-runner.d.ts +1 -0
  110. package/dist/workflow/test-runner.d.ts.map +1 -1
  111. package/dist/workflow/test-runner.js +9 -5
  112. package/dist/workflow/test-runner.js.map +1 -1
  113. package/dist/workflow/ui-designer.d.ts +82 -0
  114. package/dist/workflow/ui-designer.d.ts.map +1 -0
  115. package/dist/workflow/ui-designer.js +234 -0
  116. package/dist/workflow/ui-designer.js.map +1 -0
  117. package/dist/workflow/ui-setup.d.ts +58 -0
  118. package/dist/workflow/ui-setup.d.ts.map +1 -0
  119. package/dist/workflow/ui-setup.js +685 -0
  120. package/dist/workflow/ui-setup.js.map +1 -0
  121. package/dist/workflow/ui-verification.d.ts +114 -0
  122. package/dist/workflow/ui-verification.d.ts.map +1 -0
  123. package/dist/workflow/ui-verification.js +258 -0
  124. package/dist/workflow/ui-verification.js.map +1 -0
  125. package/dist/workflow/workflow-logger.d.ts +110 -0
  126. package/dist/workflow/workflow-logger.d.ts.map +1 -0
  127. package/dist/workflow/workflow-logger.js +267 -0
  128. package/dist/workflow/workflow-logger.js.map +1 -0
  129. package/package.json +2 -2
  130. package/src/adapters/claude.ts +815 -34
  131. package/src/adapters/gemini.ts +373 -0
  132. package/src/adapters/openai.ts +40 -7
  133. package/src/auth/claude.ts +120 -78
  134. package/src/auth/gemini.ts +207 -0
  135. package/src/auth/index.ts +28 -8
  136. package/src/auth/keychain.ts +95 -28
  137. package/src/auth/openai.ts +29 -36
  138. package/src/cli/interactive.ts +1357 -115
  139. package/src/config/defaults.ts +10 -2
  140. package/src/config/index.ts +21 -0
  141. package/src/config/schema.ts +7 -0
  142. package/src/generators/python.ts +1 -0
  143. package/src/generators/typescript.ts +1 -0
  144. package/src/state/index.ts +713 -4
  145. package/src/state/registry.ts +278 -0
  146. package/src/types/cli.ts +4 -0
  147. package/src/types/consensus.ts +65 -6
  148. package/src/types/workflow.ts +35 -0
  149. package/src/workflow/auto-fix.ts +340 -0
  150. package/src/workflow/consensus.ts +750 -16
  151. package/src/workflow/execution-mode.ts +673 -74
  152. package/src/workflow/index.ts +95 -6
  153. package/src/workflow/milestone-workflow.ts +576 -0
  154. package/src/workflow/plan-mode.ts +696 -50
  155. package/src/workflow/plan-storage.ts +482 -0
  156. package/src/workflow/project-verification.ts +471 -0
  157. package/src/workflow/task-workflow.ts +525 -0
  158. package/src/workflow/test-runner.ts +10 -5
  159. package/src/workflow/ui-designer.ts +337 -0
  160. package/src/workflow/ui-setup.ts +797 -0
  161. package/src/workflow/ui-verification.ts +357 -0
  162. package/src/workflow/workflow-logger.ts +353 -0
  163. package/tests/config/config.test.ts +1 -1
  164. package/tests/types/consensus.test.ts +3 -3
  165. package/tests/workflow/plan-mode.test.ts +213 -0
  166. package/tests/workflow/test-runner.test.ts +5 -3
@@ -1,11 +1,67 @@
1
1
  /**
2
2
  * Consensus workflow module
3
- * Handles the iterative consensus-building process between Claude and OpenAI
3
+ * Handles the iterative consensus-building process between Claude and OpenAI/Gemini
4
+ * with arbitration support when consensus cannot be reached
4
5
  */
5
6
  import { DEFAULT_CONSENSUS_CONFIG } from '../types/consensus.js';
6
- import { requestConsensus } from '../adapters/openai.js';
7
+ import { requestConsensus as requestOpenAIConsensus } from '../adapters/openai.js';
8
+ import { requestConsensus as requestGeminiConsensus, requestArbitration as requestGeminiArbitration } from '../adapters/gemini.js';
7
9
  import { revisePlan } from '../adapters/claude.js';
8
10
  import { recordConsensusIteration } from '../state/index.js';
11
+ import { createPlanStorage } from './plan-storage.js';
12
+ /**
13
+ * Request consensus from the configured reviewer (OpenAI or Gemini)
14
+ */
15
+ async function requestReviewerConsensus(plan, context, reviewer, config) {
16
+ if (reviewer === 'gemini') {
17
+ return requestGeminiConsensus(plan, context, {
18
+ model: config.geminiModel,
19
+ temperature: config.temperature,
20
+ maxTokens: config.maxTokens,
21
+ });
22
+ }
23
+ return requestOpenAIConsensus(plan, context, config);
24
+ }
25
+ /**
26
+ * Check if the consensus process is "stuck" (not improving)
27
+ * Detects both:
28
+ * 1. Stagnation: scores within 5% of each other
29
+ * 2. Oscillation: scores going up and down without progress
30
+ */
31
+ function isStuck(scores, stuckIterations) {
32
+ if (scores.length < stuckIterations)
33
+ return false;
34
+ const recentScores = scores.slice(-stuckIterations);
35
+ const maxRecent = Math.max(...recentScores);
36
+ const minRecent = Math.min(...recentScores);
37
+ // Check 1: Stagnation - all recent scores are within 5% of each other
38
+ if ((maxRecent - minRecent) <= 5) {
39
+ return true;
40
+ }
41
+ // Check 2: Oscillation - detect if we're going up and down without making progress
42
+ // e.g., 70 -> 85 -> 75 -> 80 (oscillating around ~77.5)
43
+ if (recentScores.length >= 3) {
44
+ const avg = recentScores.reduce((a, b) => a + b, 0) / recentScores.length;
45
+ const deviations = recentScores.map(s => Math.abs(s - avg));
46
+ const avgDeviation = deviations.reduce((a, b) => a + b, 0) / deviations.length;
47
+ // If scores are oscillating around an average (avg deviation > 3% but range < 20%)
48
+ // and we're not trending upward, consider it stuck
49
+ if (avgDeviation > 3 && (maxRecent - minRecent) < 20) {
50
+ // Check if we're trending upward (last score should be close to max)
51
+ const lastScore = recentScores[recentScores.length - 1];
52
+ const firstScore = recentScores[0];
53
+ // Not improving if last score is not better than first
54
+ if (lastScore <= firstScore + 2) {
55
+ return true;
56
+ }
57
+ }
58
+ }
59
+ return false;
60
+ }
61
+ /**
62
+ * Default consensus timeout (15 minutes total)
63
+ */
64
+ const DEFAULT_CONSENSUS_TIMEOUT_MS = 15 * 60 * 1000;
9
65
  /**
10
66
  * Format a plan for consensus review
11
67
  * Structures the plan in a way that's optimal for review
@@ -55,6 +111,7 @@ export function meetsThreshold(score, threshold = DEFAULT_CONSENSUS_CONFIG.thres
55
111
  }
56
112
  /**
57
113
  * Iterate until consensus is reached
114
+ * Supports configurable reviewer and arbitration when stuck
58
115
  *
59
116
  * @param initialPlan - The initial plan to review
60
117
  * @param context - Project context
@@ -62,15 +119,89 @@ export function meetsThreshold(score, threshold = DEFAULT_CONSENSUS_CONFIG.thres
62
119
  * @returns The consensus process result
63
120
  */
64
121
  export async function iterateUntilConsensus(initialPlan, context, options) {
65
- const { projectDir, config = {}, onIteration, onRevision, } = options;
66
- const { threshold = DEFAULT_CONSENSUS_CONFIG.threshold, maxIterations = DEFAULT_CONSENSUS_CONFIG.maxIterations, } = config;
122
+ const { projectDir, config = {}, onIteration, onRevision, onConcerns, onArbitration, onProgress, } = options;
123
+ const { threshold = DEFAULT_CONSENSUS_CONFIG.threshold, maxIterations = DEFAULT_CONSENSUS_CONFIG.maxIterations, reviewer = DEFAULT_CONSENSUS_CONFIG.reviewer, arbitrator = DEFAULT_CONSENSUS_CONFIG.arbitrator, enableArbitration = DEFAULT_CONSENSUS_CONFIG.enableArbitration, arbitrationThreshold = DEFAULT_CONSENSUS_CONFIG.arbitrationThreshold, stuckIterations = DEFAULT_CONSENSUS_CONFIG.stuckIterations, } = config;
67
124
  const iterations = [];
125
+ const scores = [];
68
126
  let currentPlan = initialPlan;
69
127
  let iteration = 0;
128
+ // Track the best plan throughout the process
129
+ let bestPlan = initialPlan;
130
+ let bestScore = 0;
131
+ let bestIteration = 0;
132
+ let lastConcerns = [];
133
+ let lastRecommendations = [];
134
+ let lastAnalysis = '';
135
+ // Track arbitration attempts to prevent infinite loops
136
+ let arbitrationAttempts = 0;
137
+ // Track elapsed time to detect stuck processes
138
+ const startTime = Date.now();
139
+ const maxArbitrationAttempts = 2;
140
+ onProgress?.('consensus', `Using ${reviewer} as reviewer${enableArbitration ? `, ${arbitrator} as arbitrator` : ''}`);
70
141
  while (iteration < maxIterations) {
71
142
  iteration++;
72
- // Request consensus review from OpenAI
73
- const consensusResult = await requestConsensus(currentPlan, context, config);
143
+ // Check total elapsed time - if timing out, try arbitration before giving up
144
+ const totalElapsed = Date.now() - startTime;
145
+ if (totalElapsed > DEFAULT_CONSENSUS_TIMEOUT_MS && enableArbitration && arbitrationAttempts < maxArbitrationAttempts) {
146
+ onProgress?.('consensus', `Consensus timeout after ${Math.round(totalElapsed / 60000)} minutes - invoking arbitrator before accepting`);
147
+ try {
148
+ arbitrationAttempts++;
149
+ const arbitrationResult = await requestGeminiArbitration(bestPlan, lastAnalysis, `Consensus timed out after ${Math.round(totalElapsed / 60000)} minutes. Best score: ${bestScore}%. Main concerns: ${lastConcerns.slice(0, 3).join('; ')}`, iteration, scores);
150
+ if (onArbitration) {
151
+ onArbitration(arbitrationResult);
152
+ }
153
+ // Accept arbitration result (we're out of time)
154
+ onProgress?.('arbitration', `Arbitrator decision: ${arbitrationResult.approved ? 'APPROVED' : 'REVISE'} with ${arbitrationResult.score}%`);
155
+ return {
156
+ approved: arbitrationResult.approved || arbitrationResult.score >= 80,
157
+ finalPlan: bestPlan,
158
+ finalScore: arbitrationResult.score,
159
+ bestPlan,
160
+ bestScore: arbitrationResult.score,
161
+ bestIteration,
162
+ iterations,
163
+ totalIterations: iteration - 1,
164
+ finalConcerns: arbitrationResult.minorConcerns || lastConcerns,
165
+ finalRecommendations: arbitrationResult.suggestedChanges || lastRecommendations,
166
+ arbitrated: true,
167
+ arbitrationResult,
168
+ timedOut: true,
169
+ };
170
+ }
171
+ catch (arbError) {
172
+ onProgress?.('arbitration', `Arbitration failed on timeout: ${arbError instanceof Error ? arbError.message : 'Unknown error'}`);
173
+ // Fall through to accept best plan
174
+ }
175
+ }
176
+ // Hard timeout - no more arbitration attempts left
177
+ if (totalElapsed > DEFAULT_CONSENSUS_TIMEOUT_MS) {
178
+ onProgress?.('consensus', `Consensus timeout - accepting best plan with ${bestScore}%`);
179
+ return {
180
+ approved: bestScore >= arbitrationThreshold,
181
+ finalPlan: bestPlan,
182
+ finalScore: bestScore,
183
+ bestPlan,
184
+ bestScore,
185
+ bestIteration,
186
+ iterations,
187
+ totalIterations: iteration - 1,
188
+ finalConcerns: lastConcerns,
189
+ finalRecommendations: lastRecommendations,
190
+ arbitrated: false,
191
+ timedOut: true,
192
+ };
193
+ }
194
+ // Log iteration timing
195
+ const iterationStart = Date.now();
196
+ const elapsedMinutes = Math.round((iterationStart - startTime) / 60000);
197
+ onProgress?.('consensus', `Iteration ${iteration} starting (${elapsedMinutes}min elapsed)`);
198
+ // Request consensus review from configured reviewer
199
+ onProgress?.('consensus', `Requesting review from ${reviewer}...`);
200
+ const consensusResult = await requestReviewerConsensus(currentPlan, context, reviewer, config);
201
+ // Log iteration duration
202
+ const iterationDuration = Math.round((Date.now() - iterationStart) / 1000);
203
+ onProgress?.('consensus', `Review completed in ${iterationDuration}s - score: ${consensusResult.score}%`);
204
+ scores.push(consensusResult.score);
74
205
  // Record the iteration
75
206
  const iterationRecord = {
76
207
  iteration,
@@ -81,45 +212,155 @@ export async function iterateUntilConsensus(initialPlan, context, options) {
81
212
  iterations.push(iterationRecord);
82
213
  // Save to project state
83
214
  await recordConsensusIteration(projectDir, iterationRecord);
84
- // Notify callback
215
+ // Track best plan - only update if this score is better
216
+ if (consensusResult.score > bestScore) {
217
+ bestPlan = currentPlan;
218
+ bestScore = consensusResult.score;
219
+ bestIteration = iteration;
220
+ }
221
+ // Track concerns for output
222
+ lastConcerns = consensusResult.concerns || [];
223
+ lastRecommendations = consensusResult.recommendations || [];
224
+ lastAnalysis = consensusResult.analysis || '';
225
+ // Notify callbacks
85
226
  if (onIteration) {
86
227
  onIteration(iteration, consensusResult);
87
228
  }
229
+ if (onConcerns && (lastConcerns.length > 0 || lastRecommendations.length > 0)) {
230
+ onConcerns(lastConcerns, lastRecommendations);
231
+ }
88
232
  // Check if we've reached consensus
89
233
  if (meetsThreshold(consensusResult.score, threshold)) {
90
234
  return {
91
235
  approved: true,
92
236
  finalPlan: currentPlan,
93
237
  finalScore: consensusResult.score,
238
+ bestPlan: currentPlan,
239
+ bestScore: consensusResult.score,
240
+ bestIteration: iteration,
94
241
  iterations,
95
242
  totalIterations: iteration,
243
+ finalConcerns: [],
244
+ finalRecommendations: [],
245
+ arbitrated: false,
96
246
  };
97
247
  }
248
+ // Check if we're stuck and should trigger arbitration
249
+ if (enableArbitration &&
250
+ bestScore >= arbitrationThreshold &&
251
+ isStuck(scores, stuckIterations) &&
252
+ arbitrationAttempts < maxArbitrationAttempts) {
253
+ arbitrationAttempts++;
254
+ onProgress?.('arbitration', `Consensus stuck at ${bestScore}%, invoking ${arbitrator} arbitrator (attempt ${arbitrationAttempts}/${maxArbitrationAttempts})...`);
255
+ try {
256
+ const arbitrationResult = await requestGeminiArbitration(bestPlan, lastAnalysis, `The plan has been revised ${iteration} times. Best score achieved: ${bestScore}%. The reviewer's main concerns are: ${lastConcerns.slice(0, 3).join('; ')}`, iteration, scores);
257
+ if (onArbitration) {
258
+ onArbitration(arbitrationResult);
259
+ }
260
+ // Accept if arbitrator approves OR if arbitrator gives a high score (>= 88%)
261
+ // This prevents infinite REVISE loops when the arbitrator is happy enough
262
+ const acceptArbitration = arbitrationResult.approved ||
263
+ arbitrationResult.score >= 88 ||
264
+ (arbitrationAttempts >= maxArbitrationAttempts && arbitrationResult.score >= 80);
265
+ if (acceptArbitration) {
266
+ const reason = arbitrationResult.approved
267
+ ? `Arbitrator approved plan with ${arbitrationResult.score}% confidence`
268
+ : `Arbitrator score ${arbitrationResult.score}% is acceptable - proceeding with best plan`;
269
+ onProgress?.('arbitration', reason);
270
+ return {
271
+ approved: true,
272
+ finalPlan: bestPlan,
273
+ finalScore: arbitrationResult.score,
274
+ bestPlan,
275
+ bestScore: arbitrationResult.score,
276
+ bestIteration,
277
+ iterations,
278
+ totalIterations: iteration,
279
+ finalConcerns: arbitrationResult.minorConcerns || [],
280
+ finalRecommendations: arbitrationResult.suggestedChanges || [],
281
+ arbitrated: true,
282
+ arbitrationResult,
283
+ };
284
+ }
285
+ else {
286
+ onProgress?.('arbitration', `Arbitrator requests changes: ${arbitrationResult.suggestedChanges.slice(0, 2).join('; ')}`);
287
+ // Apply arbitrator's suggested changes
288
+ if (arbitrationResult.suggestedChanges.length > 0) {
289
+ onProgress?.('consensus', 'Applying arbitrator suggestions...');
290
+ const revisionResult = await revisePlan(bestPlan, arbitrationResult.reasoning, arbitrationResult.suggestedChanges);
291
+ if (revisionResult.success && revisionResult.response) {
292
+ currentPlan = revisionResult.response;
293
+ // Reset stuck detection after arbitration revision
294
+ scores.length = 0;
295
+ scores.push(arbitrationResult.score);
296
+ onProgress?.('consensus', 'Plan revised based on arbitrator feedback');
297
+ }
298
+ else {
299
+ onProgress?.('consensus', 'Revision failed, continuing with current plan');
300
+ }
301
+ }
302
+ }
303
+ }
304
+ catch (error) {
305
+ onProgress?.('arbitration', `Arbitration failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
306
+ // If we've tried arbitration and it failed, accept the best plan we have
307
+ if (arbitrationAttempts >= maxArbitrationAttempts && bestScore >= arbitrationThreshold) {
308
+ onProgress?.('arbitration', `Max arbitration attempts reached, accepting best plan with ${bestScore}%`);
309
+ return {
310
+ approved: true,
311
+ finalPlan: bestPlan,
312
+ finalScore: bestScore,
313
+ bestPlan,
314
+ bestScore,
315
+ bestIteration,
316
+ iterations,
317
+ totalIterations: iteration,
318
+ finalConcerns: lastConcerns,
319
+ finalRecommendations: lastRecommendations,
320
+ arbitrated: true,
321
+ };
322
+ }
323
+ }
324
+ }
98
325
  // If not at max iterations, revise the plan
99
326
  if (iteration < maxIterations) {
100
327
  const concerns = extractConcerns(consensusResult);
328
+ onProgress?.('consensus', 'Revising plan based on feedback...');
329
+ // Create a progress handler for revision
330
+ const revisionProgress = onProgress
331
+ ? (msg) => onProgress('consensus', `[revision] ${msg}`)
332
+ : undefined;
101
333
  // Use Claude to revise the plan
102
- const revisionResult = await revisePlan(currentPlan, consensusResult.analysis, concerns);
334
+ const revisionResult = await revisePlan(currentPlan, consensusResult.analysis, concerns, revisionProgress);
103
335
  if (revisionResult.success && revisionResult.response) {
336
+ // Only use the revised plan for the next iteration
337
+ // The best plan tracking above will decide if it's actually better
104
338
  currentPlan = revisionResult.response;
105
339
  if (onRevision) {
106
340
  onRevision(iteration, currentPlan);
107
341
  }
108
342
  }
109
343
  else {
110
- // If revision fails, try to continue with current plan
344
+ // If revision fails, try to continue with best plan
111
345
  console.warn(`Plan revision failed at iteration ${iteration}:`, revisionResult.error);
346
+ currentPlan = bestPlan;
112
347
  }
113
348
  }
114
349
  }
115
350
  // Max iterations reached without consensus
116
- const lastIteration = iterations[iterations.length - 1];
351
+ // Return the BEST plan we found, not the last one
117
352
  return {
118
353
  approved: false,
119
- finalPlan: currentPlan,
120
- finalScore: lastIteration?.result.score || 0,
354
+ finalPlan: bestPlan,
355
+ finalScore: bestScore,
356
+ bestPlan,
357
+ bestScore,
358
+ bestIteration,
121
359
  iterations,
122
360
  totalIterations: iteration,
361
+ finalConcerns: lastConcerns,
362
+ finalRecommendations: lastRecommendations,
363
+ arbitrated: false,
123
364
  };
124
365
  }
125
366
  /**
@@ -132,27 +373,48 @@ export function summarizeConsensusProcess(result) {
132
373
  const lines = [];
133
374
  lines.push(`## Consensus Summary`);
134
375
  lines.push('');
135
- lines.push(`**Status:** ${result.approved ? 'APPROVED' : 'NOT APPROVED'}`);
376
+ lines.push(`**Status:** ${result.approved ? 'APPROVED' : 'NOT APPROVED'}${result.arbitrated ? ' (via arbitration)' : ''}`);
136
377
  lines.push(`**Final Score:** ${result.finalScore}%`);
378
+ lines.push(`**Best Score:** ${result.bestScore}% (iteration ${result.bestIteration})`);
137
379
  lines.push(`**Total Iterations:** ${result.totalIterations}`);
380
+ if (result.arbitrated && result.arbitrationResult) {
381
+ lines.push('');
382
+ lines.push(`### Arbitration Decision`);
383
+ lines.push(`- Decision: ${result.arbitrationResult.approved ? 'APPROVED' : 'REVISE'}`);
384
+ lines.push(`- Confidence: ${result.arbitrationResult.score}%`);
385
+ if (result.arbitrationResult.criticalConcerns.length > 0) {
386
+ lines.push(`- Critical Concerns: ${result.arbitrationResult.criticalConcerns.length}`);
387
+ }
388
+ if (result.arbitrationResult.minorConcerns.length > 0) {
389
+ lines.push(`- Minor Concerns: ${result.arbitrationResult.minorConcerns.length}`);
390
+ }
391
+ }
138
392
  lines.push('');
139
393
  lines.push(`### Iteration History`);
140
394
  lines.push('');
141
395
  for (const iteration of result.iterations) {
142
- lines.push(`#### Iteration ${iteration.iteration}`);
396
+ const isBest = iteration.iteration === result.bestIteration;
397
+ lines.push(`#### Iteration ${iteration.iteration}${isBest ? ' (BEST)' : ''}`);
143
398
  lines.push(`- Score: ${iteration.result.score}%`);
144
399
  lines.push(`- Strengths: ${iteration.result.strengths?.length || 0}`);
145
400
  lines.push(`- Concerns: ${iteration.result.concerns?.length || 0}`);
146
401
  lines.push('');
147
402
  }
148
403
  if (!result.approved) {
149
- const lastResult = result.iterations[result.iterations.length - 1]?.result;
150
- if (lastResult?.concerns && lastResult.concerns.length > 0) {
404
+ if (result.finalConcerns && result.finalConcerns.length > 0) {
151
405
  lines.push(`### Remaining Concerns`);
152
406
  lines.push('');
153
- for (const concern of lastResult.concerns) {
407
+ for (const concern of result.finalConcerns) {
154
408
  lines.push(`- ${concern}`);
155
409
  }
410
+ lines.push('');
411
+ }
412
+ if (result.finalRecommendations && result.finalRecommendations.length > 0) {
413
+ lines.push(`### Recommendations`);
414
+ lines.push('');
415
+ for (const rec of result.finalRecommendations) {
416
+ lines.push(`- ${rec}`);
417
+ }
156
418
  }
157
419
  }
158
420
  return lines.join('\n');
@@ -217,4 +479,290 @@ export function getScoreTrend(iterations) {
217
479
  return 'declining';
218
480
  return 'stable';
219
481
  }
482
+ /**
483
+ * Collect feedback from a single reviewer
484
+ */
485
+ async function collectReviewerFeedback(plan, context, reviewer, config, onProgress) {
486
+ onProgress?.('consensus', `Requesting review from ${reviewer}...`);
487
+ const startTime = Date.now();
488
+ const result = await requestReviewerConsensus(plan, context, reviewer, config);
489
+ const duration = Math.round((Date.now() - startTime) / 1000);
490
+ onProgress?.('consensus', `${reviewer} review completed in ${duration}s - score: ${result.score}%`);
491
+ return {
492
+ reviewer,
493
+ score: result.score,
494
+ timestamp: new Date().toISOString(),
495
+ concerns: result.concerns || [],
496
+ recommendations: result.recommendations || [],
497
+ analysis: result.analysis || '',
498
+ };
499
+ }
500
+ /**
501
+ * Collect feedback from multiple reviewers in parallel
502
+ */
503
+ async function collectAllFeedback(plan, context, reviewers, config, onProgress) {
504
+ onProgress?.('consensus', `Collecting feedback from ${reviewers.length} reviewer(s) in parallel...`);
505
+ const feedbackPromises = reviewers.map(reviewer => collectReviewerFeedback(plan, context, reviewer, config, onProgress)
506
+ .catch(error => {
507
+ onProgress?.('consensus', `${reviewer} review failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
508
+ return null;
509
+ }));
510
+ const results = await Promise.all(feedbackPromises);
511
+ return results.filter((f) => f !== null);
512
+ }
513
+ /**
514
+ * Optimized consensus process that batches feedback and reduces API calls
515
+ *
516
+ * Key optimizations:
517
+ * 1. Plans stored in files, not regenerated from scratch
518
+ * 2. Collects ALL reviewer feedback before revision
519
+ * 3. Claude revises ONCE per round with combined feedback
520
+ * 4. Parallel reviews when multiple reviewers configured
521
+ *
522
+ * @param initialPlan - The initial plan to seek consensus on
523
+ * @param context - Project context for review
524
+ * @param options - Consensus options including tracking info
525
+ * @returns Consensus process result
526
+ */
527
+ export async function runOptimizedConsensusProcess(initialPlan, context, options) {
528
+ const { projectDir, config = {}, onIteration, onRevision, onConcerns, onArbitration, onProgress, milestoneId, milestoneName, taskId, taskName, parallelReviews = true, additionalReviewers = [], } = options;
529
+ const { threshold = DEFAULT_CONSENSUS_CONFIG.threshold, maxIterations = DEFAULT_CONSENSUS_CONFIG.maxIterations, reviewer = DEFAULT_CONSENSUS_CONFIG.reviewer, arbitrator = DEFAULT_CONSENSUS_CONFIG.arbitrator, enableArbitration = DEFAULT_CONSENSUS_CONFIG.enableArbitration, arbitrationThreshold = DEFAULT_CONSENSUS_CONFIG.arbitrationThreshold, stuckIterations = DEFAULT_CONSENSUS_CONFIG.stuckIterations, } = config;
530
+ // Initialize plan storage
531
+ const planStorage = createPlanStorage(projectDir);
532
+ await planStorage.initialize();
533
+ // Determine all reviewers
534
+ const allReviewers = [reviewer, ...additionalReviewers.filter(r => r !== reviewer)];
535
+ const iterations = [];
536
+ const scores = [];
537
+ let currentPlan = initialPlan;
538
+ let iteration = 0;
539
+ // Track the best plan
540
+ let bestPlan = initialPlan;
541
+ let bestScore = 0;
542
+ let bestIteration = 0;
543
+ let lastConcerns = [];
544
+ let lastRecommendations = [];
545
+ let lastAnalysis = '';
546
+ const startTime = Date.now();
547
+ onProgress?.('consensus', `Using optimized consensus with ${allReviewers.join(', ')} as reviewer(s)`);
548
+ onProgress?.('consensus', `Plan tracking: milestone=${milestoneId}${taskId ? `, task=${taskId}` : ''}`);
549
+ // Save initial plan to storage
550
+ await planStorage.savePlan(currentPlan, taskId ? 'task' : 'milestone', {
551
+ milestoneId,
552
+ milestoneName,
553
+ taskId,
554
+ taskName,
555
+ });
556
+ while (iteration < maxIterations) {
557
+ iteration++;
558
+ // Check timeout
559
+ const totalElapsed = Date.now() - startTime;
560
+ if (totalElapsed > DEFAULT_CONSENSUS_TIMEOUT_MS) {
561
+ onProgress?.('consensus', `Consensus timeout after ${Math.round(totalElapsed / 60000)} minutes`);
562
+ if (enableArbitration) {
563
+ try {
564
+ const arbitrationResult = await requestGeminiArbitration(bestPlan, lastAnalysis, `Timeout. Best score: ${bestScore}%. Concerns: ${lastConcerns.slice(0, 3).join('; ')}`, iteration, scores);
565
+ if (onArbitration)
566
+ onArbitration(arbitrationResult);
567
+ return {
568
+ approved: arbitrationResult.approved || arbitrationResult.score >= 80,
569
+ finalPlan: bestPlan,
570
+ finalScore: arbitrationResult.score,
571
+ bestPlan,
572
+ bestScore: arbitrationResult.score,
573
+ bestIteration,
574
+ iterations,
575
+ totalIterations: iteration - 1,
576
+ finalConcerns: arbitrationResult.minorConcerns || lastConcerns,
577
+ finalRecommendations: arbitrationResult.suggestedChanges || lastRecommendations,
578
+ arbitrated: true,
579
+ arbitrationResult,
580
+ timedOut: true,
581
+ };
582
+ }
583
+ catch {
584
+ // Fall through to accept best plan
585
+ }
586
+ }
587
+ return {
588
+ approved: bestScore >= arbitrationThreshold,
589
+ finalPlan: bestPlan,
590
+ finalScore: bestScore,
591
+ bestPlan,
592
+ bestScore,
593
+ bestIteration,
594
+ iterations,
595
+ totalIterations: iteration - 1,
596
+ finalConcerns: lastConcerns,
597
+ finalRecommendations: lastRecommendations,
598
+ arbitrated: false,
599
+ timedOut: true,
600
+ };
601
+ }
602
+ const elapsedMinutes = Math.round((Date.now() - startTime) / 60000);
603
+ onProgress?.('consensus', `Iteration ${iteration} starting (${elapsedMinutes}min elapsed)`);
604
+ // Clear previous feedback for this round
605
+ await planStorage.clearFeedback(milestoneId, taskId);
606
+ // ============================================
607
+ // OPTIMIZATION: Collect ALL feedback in parallel
608
+ // ============================================
609
+ let allFeedback;
610
+ if (parallelReviews && allReviewers.length > 1) {
611
+ allFeedback = await collectAllFeedback(currentPlan, context, allReviewers, config, onProgress);
612
+ }
613
+ else {
614
+ // Sequential fallback
615
+ allFeedback = [];
616
+ for (const rev of allReviewers) {
617
+ const feedback = await collectReviewerFeedback(currentPlan, context, rev, config, onProgress);
618
+ allFeedback.push(feedback);
619
+ }
620
+ }
621
+ // Save all feedback
622
+ for (const feedback of allFeedback) {
623
+ await planStorage.saveFeedback(feedback, milestoneId, taskId);
624
+ }
625
+ // Calculate combined score (average of all reviewers)
626
+ const combinedScore = allFeedback.length > 0
627
+ ? Math.round(allFeedback.reduce((sum, f) => sum + f.score, 0) / allFeedback.length)
628
+ : 0;
629
+ scores.push(combinedScore);
630
+ // Combine all concerns and recommendations
631
+ const allConcerns = [...new Set(allFeedback.flatMap(f => f.concerns))];
632
+ const allRecommendations = [...new Set(allFeedback.flatMap(f => f.recommendations))];
633
+ const combinedAnalysis = allFeedback.map(f => `[${f.reviewer}] ${f.analysis}`).join('\n\n');
634
+ lastConcerns = allConcerns;
635
+ lastRecommendations = allRecommendations;
636
+ lastAnalysis = combinedAnalysis;
637
+ // Create consensus result for tracking
638
+ const consensusResult = {
639
+ score: combinedScore,
640
+ analysis: combinedAnalysis,
641
+ concerns: allConcerns,
642
+ recommendations: allRecommendations,
643
+ approved: combinedScore >= threshold,
644
+ strengths: [],
645
+ rawResponse: combinedAnalysis,
646
+ };
647
+ // Record iteration
648
+ const iterationRecord = {
649
+ iteration,
650
+ plan: currentPlan,
651
+ timestamp: new Date().toISOString(),
652
+ result: consensusResult,
653
+ };
654
+ iterations.push(iterationRecord);
655
+ if (onIteration)
656
+ onIteration(iteration, consensusResult);
657
+ if (onConcerns)
658
+ onConcerns(allConcerns, allRecommendations);
659
+ // Update best plan tracking
660
+ if (combinedScore > bestScore) {
661
+ bestScore = combinedScore;
662
+ bestPlan = currentPlan;
663
+ bestIteration = iteration;
664
+ }
665
+ // Save plan with updated score
666
+ await planStorage.savePlan(currentPlan, taskId ? 'task' : 'milestone', {
667
+ milestoneId,
668
+ milestoneName,
669
+ taskId,
670
+ taskName,
671
+ score: combinedScore,
672
+ });
673
+ // Record in project state
674
+ await recordConsensusIteration(projectDir, iterationRecord);
675
+ onProgress?.('consensus', `Combined score: ${combinedScore}% (from ${allFeedback.length} reviewer(s))`);
676
+ // Check if consensus reached
677
+ if (combinedScore >= threshold) {
678
+ onProgress?.('consensus', `Consensus reached at ${combinedScore}%`);
679
+ await planStorage.updateStatus('approved', milestoneId, taskId);
680
+ return {
681
+ approved: true,
682
+ finalPlan: currentPlan,
683
+ finalScore: combinedScore,
684
+ bestPlan: currentPlan,
685
+ bestScore: combinedScore,
686
+ bestIteration: iteration,
687
+ iterations,
688
+ totalIterations: iteration,
689
+ finalConcerns: allConcerns,
690
+ finalRecommendations: allRecommendations,
691
+ arbitrated: false,
692
+ };
693
+ }
694
+ // Check if stuck
695
+ if (isStuck(scores, stuckIterations) && enableArbitration) {
696
+ onProgress?.('consensus', `Consensus stuck - invoking ${arbitrator} for arbitration`);
697
+ try {
698
+ const arbitrationResult = await requestGeminiArbitration(bestPlan, combinedAnalysis, `Stuck after ${iteration} iterations. Scores: ${scores.slice(-stuckIterations).join(', ')}`, iteration, scores);
699
+ if (onArbitration)
700
+ onArbitration(arbitrationResult);
701
+ if (arbitrationResult.approved || arbitrationResult.score >= arbitrationThreshold) {
702
+ onProgress?.('arbitration', `Arbitrator approved with ${arbitrationResult.score}%`);
703
+ await planStorage.updateStatus('approved', milestoneId, taskId);
704
+ return {
705
+ approved: true,
706
+ finalPlan: bestPlan,
707
+ finalScore: arbitrationResult.score,
708
+ bestPlan,
709
+ bestScore: arbitrationResult.score,
710
+ bestIteration,
711
+ iterations,
712
+ totalIterations: iteration,
713
+ finalConcerns: arbitrationResult.minorConcerns || allConcerns,
714
+ finalRecommendations: arbitrationResult.suggestedChanges || allRecommendations,
715
+ arbitrated: true,
716
+ arbitrationResult,
717
+ };
718
+ }
719
+ }
720
+ catch (arbError) {
721
+ onProgress?.('arbitration', `Arbitration failed: ${arbError instanceof Error ? arbError.message : 'Unknown error'}`);
722
+ }
723
+ }
724
+ // ============================================
725
+ // OPTIMIZATION: Single revision with ALL feedback
726
+ // ============================================
727
+ if (iteration < maxIterations) {
728
+ onProgress?.('consensus', `Revising plan with combined feedback from ${allFeedback.length} reviewer(s)...`);
729
+ const revisionProgress = onProgress
730
+ ? (msg) => onProgress('consensus', `[revision] ${msg}`)
731
+ : undefined;
732
+ // Use Claude to revise with ALL combined feedback (single API call)
733
+ const revisionResult = await revisePlan(currentPlan, combinedAnalysis, allConcerns, revisionProgress);
734
+ if (revisionResult.success && revisionResult.response) {
735
+ currentPlan = revisionResult.response;
736
+ // Save revised plan
737
+ await planStorage.savePlan(currentPlan, taskId ? 'task' : 'milestone', {
738
+ milestoneId,
739
+ milestoneName,
740
+ taskId,
741
+ taskName,
742
+ });
743
+ if (onRevision)
744
+ onRevision(iteration, currentPlan);
745
+ }
746
+ else {
747
+ onProgress?.('consensus', `Revision failed, continuing with best plan`);
748
+ currentPlan = bestPlan;
749
+ }
750
+ }
751
+ }
752
+ // Max iterations reached
753
+ await planStorage.updateStatus('reviewing', milestoneId, taskId);
754
+ return {
755
+ approved: false,
756
+ finalPlan: bestPlan,
757
+ finalScore: bestScore,
758
+ bestPlan,
759
+ bestScore,
760
+ bestIteration,
761
+ iterations,
762
+ totalIterations: iteration,
763
+ finalConcerns: lastConcerns,
764
+ finalRecommendations: lastRecommendations,
765
+ arbitrated: false,
766
+ };
767
+ }
220
768
  //# sourceMappingURL=consensus.js.map