popeye-cli 2.1.0 → 2.7.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 (356) hide show
  1. package/dist/adapters/gemini.d.ts +14 -0
  2. package/dist/adapters/gemini.d.ts.map +1 -1
  3. package/dist/adapters/gemini.js +41 -6
  4. package/dist/adapters/gemini.js.map +1 -1
  5. package/dist/adapters/grok.d.ts +14 -0
  6. package/dist/adapters/grok.d.ts.map +1 -1
  7. package/dist/adapters/grok.js +42 -6
  8. package/dist/adapters/grok.js.map +1 -1
  9. package/dist/adapters/openai.d.ts +10 -0
  10. package/dist/adapters/openai.d.ts.map +1 -1
  11. package/dist/adapters/openai.js +44 -5
  12. package/dist/adapters/openai.js.map +1 -1
  13. package/dist/cli/commands/create.js +1 -1
  14. package/dist/cli/commands/create.js.map +1 -1
  15. package/dist/cli/interactive.d.ts.map +1 -1
  16. package/dist/cli/interactive.js +328 -21
  17. package/dist/cli/interactive.js.map +1 -1
  18. package/dist/generators/all.d.ts.map +1 -1
  19. package/dist/generators/all.js +25 -2
  20. package/dist/generators/all.js.map +1 -1
  21. package/dist/generators/doc-parser.d.ts +21 -6
  22. package/dist/generators/doc-parser.d.ts.map +1 -1
  23. package/dist/generators/doc-parser.js +55 -4
  24. package/dist/generators/doc-parser.js.map +1 -1
  25. package/dist/generators/templates/fullstack.js +1 -1
  26. package/dist/generators/templates/website-components.js +1 -1
  27. package/dist/generators/templates/website-components.js.map +1 -1
  28. package/dist/generators/templates/website-config.d.ts +4 -1
  29. package/dist/generators/templates/website-config.d.ts.map +1 -1
  30. package/dist/generators/templates/website-config.js +17 -11
  31. package/dist/generators/templates/website-config.js.map +1 -1
  32. package/dist/generators/templates/website-conversion.js +1 -1
  33. package/dist/generators/templates/website-conversion.js.map +1 -1
  34. package/dist/generators/templates/website-landing.js +1 -1
  35. package/dist/generators/templates/website-landing.js.map +1 -1
  36. package/dist/generators/templates/website-layout.d.ts +36 -4
  37. package/dist/generators/templates/website-layout.d.ts.map +1 -1
  38. package/dist/generators/templates/website-layout.js +466 -23
  39. package/dist/generators/templates/website-layout.js.map +1 -1
  40. package/dist/generators/templates/website-pricing.js +1 -1
  41. package/dist/generators/templates/website-pricing.js.map +1 -1
  42. package/dist/generators/templates/website-sections.js +1 -1
  43. package/dist/generators/templates/website-sections.js.map +1 -1
  44. package/dist/generators/templates/website-seo.d.ts.map +1 -1
  45. package/dist/generators/templates/website-seo.js +4 -1
  46. package/dist/generators/templates/website-seo.js.map +1 -1
  47. package/dist/generators/templates/website.d.ts +1 -1
  48. package/dist/generators/templates/website.d.ts.map +1 -1
  49. package/dist/generators/templates/website.js +1 -1
  50. package/dist/generators/templates/website.js.map +1 -1
  51. package/dist/generators/website-content-ai.d.ts +52 -0
  52. package/dist/generators/website-content-ai.d.ts.map +1 -0
  53. package/dist/generators/website-content-ai.js +141 -0
  54. package/dist/generators/website-content-ai.js.map +1 -0
  55. package/dist/generators/website-content-scanner.d.ts +1 -1
  56. package/dist/generators/website-content-scanner.d.ts.map +1 -1
  57. package/dist/generators/website-content-scanner.js +98 -1
  58. package/dist/generators/website-content-scanner.js.map +1 -1
  59. package/dist/generators/website-context.d.ts +34 -1
  60. package/dist/generators/website-context.d.ts.map +1 -1
  61. package/dist/generators/website-context.js +131 -9
  62. package/dist/generators/website-context.js.map +1 -1
  63. package/dist/generators/website-debug.d.ts +12 -0
  64. package/dist/generators/website-debug.d.ts.map +1 -1
  65. package/dist/generators/website-debug.js +16 -0
  66. package/dist/generators/website-debug.js.map +1 -1
  67. package/dist/generators/website.d.ts.map +1 -1
  68. package/dist/generators/website.js +26 -4
  69. package/dist/generators/website.js.map +1 -1
  70. package/dist/pipeline/artifact-manager.d.ts.map +1 -1
  71. package/dist/pipeline/artifact-manager.js +3 -0
  72. package/dist/pipeline/artifact-manager.js.map +1 -1
  73. package/dist/pipeline/auto-recovery.d.ts +56 -0
  74. package/dist/pipeline/auto-recovery.d.ts.map +1 -0
  75. package/dist/pipeline/auto-recovery.js +185 -0
  76. package/dist/pipeline/auto-recovery.js.map +1 -0
  77. package/dist/pipeline/change-request.d.ts +39 -0
  78. package/dist/pipeline/change-request.d.ts.map +1 -1
  79. package/dist/pipeline/change-request.js +40 -1
  80. package/dist/pipeline/change-request.js.map +1 -1
  81. package/dist/pipeline/check-runner.d.ts +30 -1
  82. package/dist/pipeline/check-runner.d.ts.map +1 -1
  83. package/dist/pipeline/check-runner.js +122 -1
  84. package/dist/pipeline/check-runner.js.map +1 -1
  85. package/dist/pipeline/command-resolver.d.ts.map +1 -1
  86. package/dist/pipeline/command-resolver.js +33 -2
  87. package/dist/pipeline/command-resolver.js.map +1 -1
  88. package/dist/pipeline/consensus/arbitrator-query.d.ts +22 -0
  89. package/dist/pipeline/consensus/arbitrator-query.d.ts.map +1 -0
  90. package/dist/pipeline/consensus/arbitrator-query.js +70 -0
  91. package/dist/pipeline/consensus/arbitrator-query.js.map +1 -0
  92. package/dist/pipeline/consensus/consensus-runner.d.ts +131 -7
  93. package/dist/pipeline/consensus/consensus-runner.d.ts.map +1 -1
  94. package/dist/pipeline/consensus/consensus-runner.js +809 -35
  95. package/dist/pipeline/consensus/consensus-runner.js.map +1 -1
  96. package/dist/pipeline/cr-lifecycle.d.ts +42 -0
  97. package/dist/pipeline/cr-lifecycle.d.ts.map +1 -0
  98. package/dist/pipeline/cr-lifecycle.js +89 -0
  99. package/dist/pipeline/cr-lifecycle.js.map +1 -0
  100. package/dist/pipeline/gate-engine.d.ts +1 -0
  101. package/dist/pipeline/gate-engine.d.ts.map +1 -1
  102. package/dist/pipeline/gate-engine.js +27 -8
  103. package/dist/pipeline/gate-engine.js.map +1 -1
  104. package/dist/pipeline/migration.d.ts.map +1 -1
  105. package/dist/pipeline/migration.js +3 -26
  106. package/dist/pipeline/migration.js.map +1 -1
  107. package/dist/pipeline/orchestrator.d.ts +1 -1
  108. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  109. package/dist/pipeline/orchestrator.js +311 -16
  110. package/dist/pipeline/orchestrator.js.map +1 -1
  111. package/dist/pipeline/packets/consensus-packet-builder.d.ts +15 -4
  112. package/dist/pipeline/packets/consensus-packet-builder.d.ts.map +1 -1
  113. package/dist/pipeline/packets/consensus-packet-builder.js +29 -17
  114. package/dist/pipeline/packets/consensus-packet-builder.js.map +1 -1
  115. package/dist/pipeline/phases/architecture.d.ts.map +1 -1
  116. package/dist/pipeline/phases/architecture.js +5 -3
  117. package/dist/pipeline/phases/architecture.js.map +1 -1
  118. package/dist/pipeline/phases/audit.d.ts.map +1 -1
  119. package/dist/pipeline/phases/audit.js +5 -3
  120. package/dist/pipeline/phases/audit.js.map +1 -1
  121. package/dist/pipeline/phases/consensus-architecture.d.ts.map +1 -1
  122. package/dist/pipeline/phases/consensus-architecture.js +10 -1
  123. package/dist/pipeline/phases/consensus-architecture.js.map +1 -1
  124. package/dist/pipeline/phases/consensus-master-plan.d.ts.map +1 -1
  125. package/dist/pipeline/phases/consensus-master-plan.js +10 -3
  126. package/dist/pipeline/phases/consensus-master-plan.js.map +1 -1
  127. package/dist/pipeline/phases/consensus-role-plans.d.ts.map +1 -1
  128. package/dist/pipeline/phases/consensus-role-plans.js +10 -1
  129. package/dist/pipeline/phases/consensus-role-plans.js.map +1 -1
  130. package/dist/pipeline/phases/done.d.ts.map +1 -1
  131. package/dist/pipeline/phases/done.js +9 -4
  132. package/dist/pipeline/phases/done.js.map +1 -1
  133. package/dist/pipeline/phases/intake.d.ts +1 -0
  134. package/dist/pipeline/phases/intake.d.ts.map +1 -1
  135. package/dist/pipeline/phases/intake.js +56 -13
  136. package/dist/pipeline/phases/intake.js.map +1 -1
  137. package/dist/pipeline/phases/phase-context.d.ts +2 -0
  138. package/dist/pipeline/phases/phase-context.d.ts.map +1 -1
  139. package/dist/pipeline/phases/phase-context.js +3 -1
  140. package/dist/pipeline/phases/phase-context.js.map +1 -1
  141. package/dist/pipeline/phases/production-gate.d.ts.map +1 -1
  142. package/dist/pipeline/phases/production-gate.js +28 -3
  143. package/dist/pipeline/phases/production-gate.js.map +1 -1
  144. package/dist/pipeline/phases/qa-validation.d.ts.map +1 -1
  145. package/dist/pipeline/phases/qa-validation.js +38 -5
  146. package/dist/pipeline/phases/qa-validation.js.map +1 -1
  147. package/dist/pipeline/phases/recovery-loop.d.ts +2 -0
  148. package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -1
  149. package/dist/pipeline/phases/recovery-loop.js +200 -6
  150. package/dist/pipeline/phases/recovery-loop.js.map +1 -1
  151. package/dist/pipeline/phases/review.d.ts.map +1 -1
  152. package/dist/pipeline/phases/review.js +58 -28
  153. package/dist/pipeline/phases/review.js.map +1 -1
  154. package/dist/pipeline/phases/role-planning.d.ts.map +1 -1
  155. package/dist/pipeline/phases/role-planning.js +20 -5
  156. package/dist/pipeline/phases/role-planning.js.map +1 -1
  157. package/dist/pipeline/phases/stuck.d.ts.map +1 -1
  158. package/dist/pipeline/phases/stuck.js +10 -0
  159. package/dist/pipeline/phases/stuck.js.map +1 -1
  160. package/dist/pipeline/repo-snapshot.d.ts.map +1 -1
  161. package/dist/pipeline/repo-snapshot.js +3 -0
  162. package/dist/pipeline/repo-snapshot.js.map +1 -1
  163. package/dist/pipeline/role-execution-adapter.d.ts +2 -1
  164. package/dist/pipeline/role-execution-adapter.d.ts.map +1 -1
  165. package/dist/pipeline/role-execution-adapter.js +22 -7
  166. package/dist/pipeline/role-execution-adapter.js.map +1 -1
  167. package/dist/pipeline/skill-loader.d.ts +19 -0
  168. package/dist/pipeline/skill-loader.d.ts.map +1 -1
  169. package/dist/pipeline/skill-loader.js +22 -0
  170. package/dist/pipeline/skill-loader.js.map +1 -1
  171. package/dist/pipeline/skills/constitution-generator.d.ts +51 -0
  172. package/dist/pipeline/skills/constitution-generator.d.ts.map +1 -0
  173. package/dist/pipeline/skills/constitution-generator.js +210 -0
  174. package/dist/pipeline/skills/constitution-generator.js.map +1 -0
  175. package/dist/pipeline/skills/coverage-gate.d.ts +44 -0
  176. package/dist/pipeline/skills/coverage-gate.d.ts.map +1 -0
  177. package/dist/pipeline/skills/coverage-gate.js +143 -0
  178. package/dist/pipeline/skills/coverage-gate.js.map +1 -0
  179. package/dist/pipeline/skills/generator.d.ts +65 -0
  180. package/dist/pipeline/skills/generator.d.ts.map +1 -0
  181. package/dist/pipeline/skills/generator.js +221 -0
  182. package/dist/pipeline/skills/generator.js.map +1 -0
  183. package/dist/pipeline/skills/role-map.d.ts +38 -0
  184. package/dist/pipeline/skills/role-map.d.ts.map +1 -0
  185. package/dist/pipeline/skills/role-map.js +234 -0
  186. package/dist/pipeline/skills/role-map.js.map +1 -0
  187. package/dist/pipeline/skills/types.d.ts +47 -0
  188. package/dist/pipeline/skills/types.d.ts.map +1 -0
  189. package/dist/pipeline/skills/types.js +5 -0
  190. package/dist/pipeline/skills/types.js.map +1 -0
  191. package/dist/pipeline/skills/usage-registry.d.ts +48 -0
  192. package/dist/pipeline/skills/usage-registry.d.ts.map +1 -0
  193. package/dist/pipeline/skills/usage-registry.js +55 -0
  194. package/dist/pipeline/skills/usage-registry.js.map +1 -0
  195. package/dist/pipeline/strategy-context.d.ts +20 -0
  196. package/dist/pipeline/strategy-context.d.ts.map +1 -0
  197. package/dist/pipeline/strategy-context.js +55 -0
  198. package/dist/pipeline/strategy-context.js.map +1 -0
  199. package/dist/pipeline/type-defs/artifacts.d.ts +30 -5
  200. package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -1
  201. package/dist/pipeline/type-defs/artifacts.js +5 -0
  202. package/dist/pipeline/type-defs/artifacts.js.map +1 -1
  203. package/dist/pipeline/type-defs/audit.d.ts +28 -13
  204. package/dist/pipeline/type-defs/audit.d.ts.map +1 -1
  205. package/dist/pipeline/type-defs/checks.d.ts +19 -8
  206. package/dist/pipeline/type-defs/checks.d.ts.map +1 -1
  207. package/dist/pipeline/type-defs/checks.js +4 -0
  208. package/dist/pipeline/type-defs/checks.js.map +1 -1
  209. package/dist/pipeline/type-defs/packets.d.ts +119 -18
  210. package/dist/pipeline/type-defs/packets.d.ts.map +1 -1
  211. package/dist/pipeline/type-defs/packets.js +17 -1
  212. package/dist/pipeline/type-defs/packets.js.map +1 -1
  213. package/dist/pipeline/type-defs/state.d.ts +165 -16
  214. package/dist/pipeline/type-defs/state.d.ts.map +1 -1
  215. package/dist/pipeline/type-defs/state.js +26 -1
  216. package/dist/pipeline/type-defs/state.js.map +1 -1
  217. package/dist/shared/text-utils.d.ts +23 -0
  218. package/dist/shared/text-utils.d.ts.map +1 -0
  219. package/dist/shared/text-utils.js +66 -0
  220. package/dist/shared/text-utils.js.map +1 -0
  221. package/dist/shared/website-strategy-format.d.ts +18 -0
  222. package/dist/shared/website-strategy-format.d.ts.map +1 -0
  223. package/dist/shared/website-strategy-format.js +47 -0
  224. package/dist/shared/website-strategy-format.js.map +1 -0
  225. package/dist/state/index.d.ts +2 -0
  226. package/dist/state/index.d.ts.map +1 -1
  227. package/dist/state/index.js +57 -8
  228. package/dist/state/index.js.map +1 -1
  229. package/dist/types/consensus.d.ts +1 -0
  230. package/dist/types/consensus.d.ts.map +1 -1
  231. package/dist/types/consensus.js.map +1 -1
  232. package/dist/types/website-strategy.d.ts +1 -1
  233. package/dist/types/workflow.d.ts +447 -0
  234. package/dist/types/workflow.d.ts.map +1 -1
  235. package/dist/types/workflow.js +3 -0
  236. package/dist/types/workflow.js.map +1 -1
  237. package/dist/upgrade/handlers.d.ts.map +1 -1
  238. package/dist/upgrade/handlers.js +6 -3
  239. package/dist/upgrade/handlers.js.map +1 -1
  240. package/dist/workflow/consensus.d.ts.map +1 -1
  241. package/dist/workflow/consensus.js +1 -0
  242. package/dist/workflow/consensus.js.map +1 -1
  243. package/dist/workflow/website-strategy.d.ts.map +1 -1
  244. package/dist/workflow/website-strategy.js +2 -29
  245. package/dist/workflow/website-strategy.js.map +1 -1
  246. package/dist/workflow/website-updater.d.ts.map +1 -1
  247. package/dist/workflow/website-updater.js +3 -2
  248. package/dist/workflow/website-updater.js.map +1 -1
  249. package/package.json +1 -1
  250. package/src/adapters/gemini.ts +51 -6
  251. package/src/adapters/grok.ts +51 -6
  252. package/src/adapters/openai.ts +53 -5
  253. package/src/cli/commands/create.ts +1 -1
  254. package/src/cli/interactive.ts +337 -20
  255. package/src/generators/all.ts +25 -2
  256. package/src/generators/doc-parser.ts +75 -15
  257. package/src/generators/templates/fullstack.ts +1 -1
  258. package/src/generators/templates/website-components.ts +1 -1
  259. package/src/generators/templates/website-config.ts +23 -11
  260. package/src/generators/templates/website-conversion.ts +1 -1
  261. package/src/generators/templates/website-landing.ts +1 -1
  262. package/src/generators/templates/website-layout.ts +491 -23
  263. package/src/generators/templates/website-pricing.ts +1 -1
  264. package/src/generators/templates/website-sections.ts +1 -1
  265. package/src/generators/templates/website-seo.ts +4 -1
  266. package/src/generators/templates/website.ts +3 -0
  267. package/src/generators/website-content-ai.ts +186 -0
  268. package/src/generators/website-content-scanner.ts +113 -1
  269. package/src/generators/website-context.ts +151 -12
  270. package/src/generators/website-debug.ts +26 -0
  271. package/src/generators/website.ts +28 -3
  272. package/src/pipeline/artifact-manager.ts +3 -0
  273. package/src/pipeline/auto-recovery.ts +283 -0
  274. package/src/pipeline/change-request.ts +63 -1
  275. package/src/pipeline/check-runner.ts +141 -2
  276. package/src/pipeline/command-resolver.ts +34 -2
  277. package/src/pipeline/consensus/arbitrator-query.ts +101 -0
  278. package/src/pipeline/consensus/consensus-runner.ts +1099 -42
  279. package/src/pipeline/cr-lifecycle.ts +103 -0
  280. package/src/pipeline/gate-engine.ts +36 -8
  281. package/src/pipeline/migration.ts +5 -30
  282. package/src/pipeline/orchestrator.ts +367 -16
  283. package/src/pipeline/packets/consensus-packet-builder.ts +44 -18
  284. package/src/pipeline/phases/architecture.ts +6 -3
  285. package/src/pipeline/phases/audit.ts +6 -3
  286. package/src/pipeline/phases/consensus-architecture.ts +10 -1
  287. package/src/pipeline/phases/consensus-master-plan.ts +10 -3
  288. package/src/pipeline/phases/consensus-role-plans.ts +10 -1
  289. package/src/pipeline/phases/done.ts +15 -4
  290. package/src/pipeline/phases/intake.ts +67 -14
  291. package/src/pipeline/phases/phase-context.ts +6 -1
  292. package/src/pipeline/phases/production-gate.ts +41 -3
  293. package/src/pipeline/phases/qa-validation.ts +51 -5
  294. package/src/pipeline/phases/recovery-loop.ts +229 -7
  295. package/src/pipeline/phases/review.ts +73 -30
  296. package/src/pipeline/phases/role-planning.ts +23 -5
  297. package/src/pipeline/phases/stuck.ts +10 -0
  298. package/src/pipeline/repo-snapshot.ts +3 -0
  299. package/src/pipeline/role-execution-adapter.ts +30 -4
  300. package/src/pipeline/skill-loader.ts +33 -0
  301. package/src/pipeline/skills/constitution-generator.ts +236 -0
  302. package/src/pipeline/skills/coverage-gate.ts +199 -0
  303. package/src/pipeline/skills/generator.ts +287 -0
  304. package/src/pipeline/skills/role-map.ts +248 -0
  305. package/src/pipeline/skills/types.ts +53 -0
  306. package/src/pipeline/skills/usage-registry.ts +87 -0
  307. package/src/pipeline/strategy-context.ts +60 -0
  308. package/src/pipeline/type-defs/artifacts.ts +5 -0
  309. package/src/pipeline/type-defs/checks.ts +4 -0
  310. package/src/pipeline/type-defs/packets.ts +18 -1
  311. package/src/pipeline/type-defs/state.ts +26 -1
  312. package/src/shared/text-utils.ts +70 -0
  313. package/src/shared/website-strategy-format.ts +56 -0
  314. package/src/state/index.ts +60 -8
  315. package/src/types/consensus.ts +1 -0
  316. package/src/types/workflow.ts +6 -0
  317. package/src/upgrade/handlers.ts +9 -3
  318. package/src/workflow/consensus.ts +1 -0
  319. package/src/workflow/website-strategy.ts +2 -36
  320. package/src/workflow/website-updater.ts +4 -2
  321. package/tests/adapters/gemini.test.ts +165 -0
  322. package/tests/adapters/grok.test.ts +137 -0
  323. package/tests/adapters/openai.test.ts +128 -0
  324. package/tests/generators/doc-parser.test.ts +88 -9
  325. package/tests/generators/quality-gate.test.ts +19 -3
  326. package/tests/generators/website-components.test.ts +34 -0
  327. package/tests/generators/website-content-ai.test.ts +308 -0
  328. package/tests/generators/website-content-scanner.test.ts +86 -0
  329. package/tests/generators/website-context.test.ts +3 -2
  330. package/tests/integration/smokestack-scaffold.test.ts +385 -0
  331. package/tests/pipeline/auto-recovery.test.ts +337 -0
  332. package/tests/pipeline/change-request.test.ts +70 -0
  333. package/tests/pipeline/command-resolver.test.ts +42 -0
  334. package/tests/pipeline/consensus/arbitrator-query.test.ts +107 -0
  335. package/tests/pipeline/consensus-runner.test.ts +1333 -10
  336. package/tests/pipeline/consensus-scoring.test.ts +602 -18
  337. package/tests/pipeline/gate-engine.test.ts +34 -0
  338. package/tests/pipeline/install-check.test.ts +261 -0
  339. package/tests/pipeline/migration.test.ts +4 -3
  340. package/tests/pipeline/orchestrator.test.ts +1506 -15
  341. package/tests/pipeline/packets/builders.test.ts +29 -6
  342. package/tests/pipeline/phases/role-planning.strategy.test.ts +204 -0
  343. package/tests/pipeline/pipeline-persistence.test.ts +230 -0
  344. package/tests/pipeline/recovery-loop-guidance.test.ts +280 -0
  345. package/tests/pipeline/role-execution-adapter.test.ts +88 -0
  346. package/tests/pipeline/skills/constitution-generator.test.ts +201 -0
  347. package/tests/pipeline/skills/coverage-gate.test.ts +370 -0
  348. package/tests/pipeline/skills/generator.test.ts +213 -0
  349. package/tests/pipeline/skills/role-map.test.ts +198 -0
  350. package/tests/pipeline/skills/usage-registry.test.ts +114 -0
  351. package/tests/pipeline/strategy-context.test.ts +148 -0
  352. package/tests/shared/text-utils.test.ts +155 -0
  353. package/tests/state/progress-analysis.test.ts +375 -0
  354. package/tests/upgrade/handlers.test.ts +33 -2
  355. package/tests/workflow/consensus.test.ts +6 -0
  356. package/tsconfig.json +1 -1
@@ -6,36 +6,336 @@
6
6
  * 1. Independent Review (DEFAULT): N reviewers review simultaneously,
7
7
  * no reviewer sees other reviewers' output.
8
8
  * 2. Iterative Consensus (optional): for recovery plan iteration.
9
+ *
10
+ * v2.1: Vote normalization pipeline, tag reclassification, hard-blocker
11
+ * detection, config-driven arbitration, reviewer rubric.
9
12
  */
10
13
  import { createHash } from 'node:crypto';
14
+ import logging from 'node:console';
15
+ import { existsSync, readFileSync } from 'node:fs';
16
+ import { resolve } from 'node:path';
17
+ import { z } from 'zod';
11
18
  import { buildConsensusPacket } from '../packets/consensus-packet-builder.js';
19
+ import { isNoneVariant } from '../../shared/text-utils.js';
20
+ import { queryProvider } from './arbitrator-query.js';
12
21
  // Re-use existing consensus infrastructure
13
22
  import { iterateUntilConsensus } from '../../workflow/consensus.js';
23
+ const logger = logging;
24
+ // ─── Hard Blocker Patterns ───────────────────────────────
25
+ // Module-level const so both containsHardBlockerPatterns() and
26
+ // the forced-REJECT block in normalizeVoteBlockers() can reference it.
27
+ const HARD_BLOCKER_PATTERNS = [
28
+ /\bsql injection\b/i,
29
+ /\bxss\b/i,
30
+ /\bsecurity vulnerabilit(?:y|ies)\b/i,
31
+ /\b(?:build|tests?)\s+(?:is|are\s+)?failing\b/i,
32
+ /\bfails?\s+(?:in\s+)?(?:ci|pipeline|compilation)\b/i,
33
+ /\bdata loss\b/i,
34
+ /\bcritical\s+(?:bug|defect|error)\b/i,
35
+ ];
36
+ function stripTag(s) {
37
+ return s.replace(/^\[(BLOCKER|REQUIRED|SUGGESTION)\]\s*/i, '');
38
+ }
39
+ /**
40
+ * Pool ALL issue lists, classify by tag prefix.
41
+ * Untagged items retain their origin field for downstream routing.
42
+ */
43
+ function parseTaggedIssues(blockingIssues, requiredChanges, suggestions) {
44
+ const result = {
45
+ blockers: [], required: [], suggestions: [], untagged: [],
46
+ };
47
+ function classify(items, origin) {
48
+ for (const issue of items) {
49
+ const trimmed = issue.trim();
50
+ if (!trimmed)
51
+ continue;
52
+ if (/^\[BLOCKER\]/i.test(trimmed))
53
+ result.blockers.push(stripTag(trimmed));
54
+ else if (/^\[REQUIRED\]/i.test(trimmed))
55
+ result.required.push(stripTag(trimmed));
56
+ else if (/^\[SUGGESTION\]/i.test(trimmed))
57
+ result.suggestions.push(stripTag(trimmed));
58
+ else
59
+ result.untagged.push({ text: trimmed, origin });
60
+ }
61
+ }
62
+ classify(blockingIssues, 'blocking');
63
+ classify(requiredChanges, 'required');
64
+ classify(suggestions, 'suggestion');
65
+ return result;
66
+ }
67
+ // ─── Normalization Helpers ───────────────────────────────
68
+ const cleanText = (s) => stripTag(s.trim());
69
+ const cleanList = (arr) => {
70
+ const out = [];
71
+ const seen = new Set();
72
+ for (const raw of arr) {
73
+ const s = cleanText(raw);
74
+ if (!s || isNoneVariant(s) || seen.has(s))
75
+ continue;
76
+ seen.add(s);
77
+ out.push(s);
78
+ }
79
+ return out;
80
+ };
81
+ function containsHardBlockerPatterns(issues) {
82
+ return issues.some(issue => HARD_BLOCKER_PATTERNS.some(p => p.test(issue)));
83
+ }
84
+ // ─── Vote Normalization Pipeline ─────────────────────────
85
+ /**
86
+ * Normalize votes: pool → classify by tag → detect hard blockers → route by vote → dedup.
87
+ * Called after collecting votes, before buildConsensusPacket().
88
+ * Idempotent: running twice produces the same result.
89
+ */
90
+ export function normalizeVoteBlockers(votes) {
91
+ const summary = {
92
+ tagged_blockers_demoted_to_suggestions: 0,
93
+ tagged_blockers_demoted_to_required: 0,
94
+ untagged_from_blocking_routed_to_required: 0,
95
+ forced_rejects: 0,
96
+ };
97
+ const normalized = votes.map((v) => {
98
+ // Step 1: Pool ALL issue lists, classify by tag
99
+ const tagged = parseTaggedIssues(v.blocking_issues.filter(i => !isNoneVariant(i)), (v.required_changes ?? []), v.suggestions);
100
+ // Step 2: Contradiction guard — scan ALL pooled text for hard blockers
101
+ const hasTaggedBlocker = tagged.blockers.length > 0;
102
+ const allPooledText = [
103
+ ...tagged.blockers, ...tagged.required, ...tagged.suggestions,
104
+ ...tagged.untagged.map(u => u.text),
105
+ ].map(cleanText);
106
+ const hasHardPattern = containsHardBlockerPatterns(allPooledText);
107
+ // v2.4.4: Vote-aware contradiction guard
108
+ // Principle:
109
+ // APPROVE + any hard pattern anywhere = genuinely inconsistent -> force REJECT
110
+ // CONDITIONAL = force REJECT only if [BLOCKER] tag OR hard pattern in blocker-origin text
111
+ // REJECT = already reject, no forcing needed
112
+ const hasHardPatternAnywhere = hasHardPattern; // already computed above (allPooledText)
113
+ const blockerOriginText = [
114
+ ...tagged.blockers,
115
+ ...tagged.untagged.filter(u => u.origin === 'blocking').map(u => u.text),
116
+ ].map(cleanText);
117
+ const hasHardPatternInBlockers = containsHardBlockerPatterns(blockerOriginText);
118
+ const forceReject = (v.vote === 'APPROVE' && (hasTaggedBlocker || hasHardPatternAnywhere)) ||
119
+ (v.vote === 'CONDITIONAL' && (hasTaggedBlocker || hasHardPatternInBlockers));
120
+ if (forceReject) {
121
+ summary.forced_rejects++;
122
+ // Debug logging for forced-reject diagnosis
123
+ logger.log(`[consensus] Forced REJECT: vote=${v.vote} reviewer=${v.reviewer_id} ` +
124
+ `hasTaggedBlocker=${hasTaggedBlocker} hasHardPatternAnywhere=${hasHardPatternAnywhere} ` +
125
+ `hasHardPatternInBlockers=${hasHardPatternInBlockers}`);
126
+ // Build minimal hard-blocker set: tagged blockers + any text matching patterns
127
+ const hardBlockers = [
128
+ ...tagged.blockers,
129
+ ...tagged.untagged.map(u => u.text).filter(t => HARD_BLOCKER_PATTERNS.some(p => p.test(t))),
130
+ ...tagged.required.filter(t => HARD_BLOCKER_PATTERNS.some(p => p.test(t))),
131
+ ...tagged.suggestions.filter(t => HARD_BLOCKER_PATTERNS.some(p => p.test(t))),
132
+ ];
133
+ // Non-hard items go to required_changes
134
+ const nonHard = [
135
+ ...tagged.required.filter(t => !HARD_BLOCKER_PATTERNS.some(p => p.test(t))),
136
+ ...tagged.untagged.filter(u => u.origin === 'required').map(u => u.text),
137
+ ...tagged.untagged.filter(u => u.origin === 'blocking').map(u => u.text)
138
+ .filter(t => !HARD_BLOCKER_PATTERNS.some(p => p.test(t))),
139
+ ];
140
+ const nonHardSuggestions = [
141
+ ...tagged.suggestions.filter(t => !HARD_BLOCKER_PATTERNS.some(p => p.test(t))),
142
+ ...tagged.untagged.filter(u => u.origin === 'suggestion').map(u => u.text),
143
+ ];
144
+ return {
145
+ ...v,
146
+ vote: 'REJECT',
147
+ blocking_issues: cleanList(hardBlockers),
148
+ required_changes: cleanList(nonHard),
149
+ suggestions: cleanList(nonHardSuggestions),
150
+ reviewer_inconsistency: true,
151
+ };
152
+ }
153
+ // Step 3: Vote-consistent routing for untagged items
154
+ switch (v.vote) {
155
+ case 'APPROVE': {
156
+ // APPROVE = execution-ready. Tagged blockers → suggestions. All untagged → suggestions.
157
+ summary.tagged_blockers_demoted_to_suggestions += tagged.blockers.length;
158
+ return {
159
+ ...v,
160
+ blocking_issues: [],
161
+ required_changes: cleanList([...tagged.required]),
162
+ suggestions: cleanList([
163
+ ...tagged.suggestions,
164
+ ...tagged.blockers,
165
+ ...tagged.untagged.map(u => u.text),
166
+ ]),
167
+ };
168
+ }
169
+ case 'CONDITIONAL': {
170
+ // CONDITIONAL: tagged blockers → required_changes, untagged-from-blocking → required_changes
171
+ summary.tagged_blockers_demoted_to_required += tagged.blockers.length;
172
+ summary.untagged_from_blocking_routed_to_required += tagged.untagged.filter(u => u.origin === 'blocking').length;
173
+ return {
174
+ ...v,
175
+ blocking_issues: [],
176
+ required_changes: cleanList([
177
+ ...tagged.required,
178
+ ...tagged.blockers,
179
+ ...tagged.untagged.filter(u => u.origin === 'blocking').map(u => u.text),
180
+ ...tagged.untagged.filter(u => u.origin === 'required').map(u => u.text),
181
+ ]),
182
+ suggestions: cleanList([
183
+ ...tagged.suggestions,
184
+ ...tagged.untagged.filter(u => u.origin === 'suggestion').map(u => u.text),
185
+ ]),
186
+ };
187
+ }
188
+ case 'REJECT': {
189
+ // REJECT: untagged-from-blocking stays as blockers
190
+ return {
191
+ ...v,
192
+ blocking_issues: cleanList([
193
+ ...tagged.blockers,
194
+ ...tagged.untagged.filter(u => u.origin === 'blocking').map(u => u.text),
195
+ ]),
196
+ required_changes: cleanList([
197
+ ...tagged.required,
198
+ ...tagged.untagged.filter(u => u.origin === 'required').map(u => u.text),
199
+ ]),
200
+ suggestions: cleanList([
201
+ ...tagged.suggestions,
202
+ ...tagged.untagged.filter(u => u.origin === 'suggestion').map(u => u.text),
203
+ ]),
204
+ };
205
+ }
206
+ }
207
+ });
208
+ return { votes: normalized, summary };
209
+ }
210
+ // ─── Vote Mapping ────────────────────────────────────────
211
+ /**
212
+ * Floor confidence score for CONDITIONAL votes.
213
+ * Matches adapter rubric: 80-94% = "minor revisions needed".
214
+ */
215
+ export const DEFAULT_CONDITIONAL_FLOOR = 0.80;
216
+ /**
217
+ * Map a reviewer's confidence score (0-1) to a structured vote.
218
+ * Threshold-aware: APPROVE = meets gate bar, CONDITIONAL = iterate, REJECT = major rework.
219
+ */
220
+ export function mapVote(confidence, threshold, conditionalFloor = DEFAULT_CONDITIONAL_FLOOR) {
221
+ const c = Math.max(0, Math.min(1, confidence));
222
+ const t = Math.max(0, Math.min(1, threshold));
223
+ const f = Math.max(0, Math.min(t, conditionalFloor));
224
+ if (c >= t)
225
+ return 'APPROVE';
226
+ if (c >= f)
227
+ return 'CONDITIONAL';
228
+ return 'REJECT';
229
+ }
230
+ // ─── Vote Disagreement Detection ─────────────────────────
231
+ /**
232
+ * Check if votes have meaningful disagreement (not unanimous).
233
+ */
234
+ export function hasVoteDisagreement(votes) {
235
+ if (votes.length <= 1)
236
+ return false;
237
+ const uniqueVotes = new Set(votes.map(v => v.vote));
238
+ return uniqueVotes.size > 1;
239
+ }
14
240
  const DEFAULT_PROVIDERS = [
15
241
  { provider: 'openai', model: 'gpt-4.1', temperature: 0.3 },
16
242
  { provider: 'gemini', model: 'gemini-2.5-flash', temperature: 0.3 },
17
243
  ];
244
+ // ─── Plan Content Loader ─────────────────────────────────
245
+ /** Max plan content chars to embed in prompt (50K ~ safe for all providers). */
246
+ const MAX_PLAN_CONTENT_CHARS = 50_000;
247
+ /**
248
+ * Safely load plan content from disk.
249
+ * - Path traversal guard: resolved path must start with projectDir.
250
+ * - Size cap: truncates with marker if content exceeds MAX_PLAN_CONTENT_CHARS.
251
+ */
252
+ export function loadPlanContent(projectDir, artifactPath) {
253
+ if (!artifactPath) {
254
+ logger.warn('[consensus] No master plan path in packet references');
255
+ return { content: '', truncated: false };
256
+ }
257
+ const resolvedProject = resolve(projectDir);
258
+ const fullPath = resolve(projectDir, artifactPath);
259
+ // Path traversal guard: resolved path must be inside projectDir
260
+ if (!fullPath.startsWith(resolvedProject + '/') && fullPath !== resolvedProject) {
261
+ logger.warn(`[consensus] Path traversal blocked: ${artifactPath} resolved to ${fullPath}`);
262
+ return { content: '', truncated: false };
263
+ }
264
+ if (!existsSync(fullPath)) {
265
+ logger.warn(`[consensus] Plan artifact not found at ${fullPath}`);
266
+ return { content: '', truncated: false };
267
+ }
268
+ let content = readFileSync(fullPath, 'utf-8');
269
+ let truncated = false;
270
+ if (content.length > MAX_PLAN_CONTENT_CHARS) {
271
+ content = content.slice(0, MAX_PLAN_CONTENT_CHARS)
272
+ + '\n\n[TRUNCATED -- plan exceeds 50K chars. Review based on visible content.]';
273
+ truncated = true;
274
+ logger.warn(`[consensus] Plan content truncated to ${MAX_PLAN_CONTENT_CHARS} chars`);
275
+ }
276
+ logger.log(`[consensus] Loaded plan content from ${artifactPath} (${content.length} chars${truncated ? ', truncated' : ''})`);
277
+ return { content, truncated };
278
+ }
279
+ /**
280
+ * Determine whether arbitration should be triggered and why.
281
+ * Pure function — no side effects, easily unit-testable.
282
+ */
283
+ export function getArbitrationTrigger(votes, weightedScore, threshold) {
284
+ if (hasVoteDisagreement(votes))
285
+ return 'DISAGREEMENT';
286
+ if (weightedScore >= (threshold - 0.10))
287
+ return 'BORDERLINE_SCORE';
288
+ const avgConfidence = votes.reduce((s, v) => s + v.confidence, 0) / votes.length;
289
+ const allConditional = votes.every(v => v.vote === 'CONDITIONAL');
290
+ const totalRequired = votes.reduce((sum, v) => sum + (v.required_changes?.length ?? 0), 0);
291
+ if (allConditional && avgConfidence >= 0.94 && totalRequired <= 3)
292
+ return 'ALL_CONDITIONAL';
293
+ return 'NONE';
294
+ }
18
295
  // ─── Consensus Runner ────────────────────────────────────
19
296
  export class ConsensusRunner {
20
297
  config;
298
+ arbitrationAttempted = new Set();
21
299
  constructor(config) {
22
300
  this.config = config;
23
301
  }
24
302
  /** Run structured consensus on a plan packet */
25
- async runStructuredConsensus(planPacket, gateDefinition) {
303
+ async runStructuredConsensus(planPacket, gateDefinition, options) {
26
304
  const rules = {
27
305
  threshold: gateDefinition.consensusThreshold ?? this.config.threshold,
28
306
  quorum: this.config.quorum,
29
307
  min_reviewers: gateDefinition.minReviewers ?? this.config.minReviewers,
30
308
  };
309
+ // v2.4.4: Dev-time warning when version is missing or stuck at 1
310
+ if (planPacket.metadata.version === undefined || planPacket.metadata.version <= 1) {
311
+ logger.warn(`[consensus] Phase ${planPacket.metadata.phase}: version=${planPacket.metadata.version ?? 'undefined'} ` +
312
+ `— ensure this is intentional (not a missing recoveryCount passthrough)`);
313
+ }
314
+ // v2.2.1: Record REVIEWER skill usage if loader available
315
+ if (this.config.skillLoader && this.config.skillUsageRegistry) {
316
+ const { meta } = this.config.skillLoader.loadSkillWithMeta('REVIEWER');
317
+ this.config.skillUsageRegistry.record('REVIEWER', planPacket.metadata.phase, 'review_prompt', meta.source, meta.version);
318
+ }
319
+ // Load actual plan content from disk for inclusion in review prompt
320
+ const { content: planContent } = loadPlanContent(this.config.projectDir, planPacket.references.master_plan?.path);
321
+ const revisionDirective = options?.revisionDirective;
31
322
  let votes;
32
323
  if (this.config.mode === 'independent') {
33
- votes = await this.runIndependentReview(planPacket);
324
+ votes = await this.runIndependentReview(planPacket, planContent, revisionDirective);
34
325
  }
35
326
  else {
36
- votes = await this.runIterativeReview(planPacket);
327
+ votes = await this.runIterativeReview(planPacket, planContent, revisionDirective);
328
+ }
329
+ // v2.1: Normalize votes before scoring
330
+ const { votes: normalizedVotes, summary: normSummary } = normalizeVoteBlockers(votes);
331
+ if (normSummary.forced_rejects > 0) {
332
+ logger.warn(`[consensus] Normalization forced ${normSummary.forced_rejects} vote(s) to REJECT due to blocker/pattern contradiction`);
37
333
  }
38
- // Build consensus packet from votes
334
+ logger.log(`[consensus] Normalization: ${JSON.stringify(normSummary)}`);
335
+ for (const v of normalizedVotes) {
336
+ logger.log(`[consensus] Normalized: ${v.reviewer_id} vote=${v.vote} conf=${v.confidence.toFixed(3)} blockers=${v.blocking_issues.length}`);
337
+ }
338
+ // Build consensus packet from normalized votes
39
339
  const packet = buildConsensusPacket({
40
340
  planPacketRef: {
41
341
  artifact_id: planPacket.metadata.packet_id,
@@ -44,19 +344,91 @@ export class ConsensusRunner {
44
344
  version: planPacket.metadata.version,
45
345
  type: 'consensus',
46
346
  },
47
- votes,
347
+ votes: normalizedVotes,
48
348
  rules,
349
+ normalizationMoves: normSummary,
49
350
  });
351
+ logger.log(`[consensus] Result: weighted_score=${packet.consensus_result.weighted_score.toFixed(3)} score=${packet.consensus_result.score.toFixed(3)} status=${packet.final_status}`);
352
+ // v2.4.2: Attempt arbitration for REJECTED packets if enabled
353
+ if (packet.final_status === 'REJECTED'
354
+ && this.config.enableArbitration
355
+ && !this.arbitrationAttempted.has(`${planPacket.metadata.phase}@v${planPacket.metadata.version}`)) {
356
+ const arbitrationTrigger = getArbitrationTrigger(normalizedVotes, packet.consensus_result.weighted_score, rules.threshold);
357
+ const shouldArbitrate = arbitrationTrigger !== 'NONE';
358
+ if (shouldArbitrate) {
359
+ logger.log(`[consensus] Arbitration triggered: reason=${arbitrationTrigger} weighted_score=${packet.consensus_result.weighted_score.toFixed(3)}`);
360
+ this.arbitrationAttempted.add(`${planPacket.metadata.phase}@v${planPacket.metadata.version}`);
361
+ const arbResult = await this.callArbitrator(planPacket, normalizedVotes, rules, planContent);
362
+ if (arbResult?.approved) {
363
+ // v2.2.1: Record ARBITRATOR skill usage
364
+ if (this.config.skillLoader && this.config.skillUsageRegistry) {
365
+ const { meta } = this.config.skillLoader.loadSkillWithMeta('ARBITRATOR');
366
+ this.config.skillUsageRegistry.record('ARBITRATOR', planPacket.metadata.phase, 'arbitration_prompt', meta.source, meta.version);
367
+ }
368
+ // Rebuild with arbitration
369
+ return buildConsensusPacket({
370
+ planPacketRef: {
371
+ artifact_id: planPacket.metadata.packet_id,
372
+ path: '',
373
+ sha256: '',
374
+ version: planPacket.metadata.version,
375
+ type: 'consensus',
376
+ },
377
+ votes: normalizedVotes,
378
+ rules,
379
+ arbitratorResult: {
380
+ decision: arbResult.reasoning,
381
+ merged_patch: arbResult.suggestedChanges?.join('\n'),
382
+ },
383
+ normalizationMoves: normSummary,
384
+ });
385
+ }
386
+ }
387
+ }
388
+ // v2.2.1: Record ARBITRATOR skill usage if arbitration occurred (legacy path)
389
+ if (packet.final_status === 'ARBITRATED' && this.config.skillLoader && this.config.skillUsageRegistry) {
390
+ const { meta } = this.config.skillLoader.loadSkillWithMeta('ARBITRATOR');
391
+ this.config.skillUsageRegistry.record('ARBITRATOR', planPacket.metadata.phase, 'arbitration_prompt', meta.source, meta.version);
392
+ }
393
+ // v2.4.2: Diagnostic logging at high version counts
394
+ if (planPacket.metadata.version >= 3) {
395
+ logger.warn(`[consensus] High iteration count: phase=${planPacket.metadata.phase} version=${planPacket.metadata.version} `
396
+ + `weighted_score=${packet.consensus_result.weighted_score.toFixed(3)} `
397
+ + `has_true_blockers=${packet.consensus_result.has_true_blockers} `
398
+ + `status=${packet.final_status}`);
399
+ }
50
400
  return packet;
51
401
  }
52
402
  /** Independent review: spawn N reviewers, each reviews independently */
53
- async runIndependentReview(planPacket) {
54
- const providers = this.config.reviewerProviders ?? DEFAULT_PROVIDERS;
403
+ async runIndependentReview(planPacket, planContent, revisionDirective) {
404
+ let providers = [...(this.config.reviewerProviders ?? DEFAULT_PROVIDERS)];
405
+ // v2.4.2: Escalation — add tie-breaking reviewer on high iteration count.
406
+ // Only select from configured providers (arbitrator config is a valid source).
407
+ if (planPacket.metadata.version >= 3 && providers.length < 3) {
408
+ const existingNames = new Set(providers.map(p => p.provider));
409
+ // Build candidates from: arbitrator provider + all configured reviewers (deduplicated)
410
+ const candidates = new Set();
411
+ if (this.config.arbitratorProvider)
412
+ candidates.add(this.config.arbitratorProvider.provider);
413
+ for (const p of this.config.reviewerProviders ?? DEFAULT_PROVIDERS)
414
+ candidates.add(p.provider);
415
+ // Pick first configured provider not already reviewing
416
+ const PREFERRED_ORDER = ['grok', 'openai', 'gemini'];
417
+ const tieBreaker = PREFERRED_ORDER.find(p => candidates.has(p) && !existingNames.has(p));
418
+ if (tieBreaker) {
419
+ const model = getModelForProvider(this.config.consensusConfig, tieBreaker);
420
+ providers.push({ provider: tieBreaker, model, temperature: 0.3 });
421
+ logger.log(`[consensus] Escalation: added ${tieBreaker}/${model} as tie-breaking reviewer (v${planPacket.metadata.version})`);
422
+ }
423
+ else {
424
+ logger.warn(`[consensus] Escalation: no additional provider available. ` +
425
+ `configured=${[...candidates].join(',')} ` +
426
+ `in_use=${[...existingNames].join(',')}`);
427
+ }
428
+ }
55
429
  const numReviewers = Math.max(this.config.minReviewers, providers.length);
56
- // Build the review prompt from the plan packet
57
- const prompt = buildReviewPrompt(planPacket);
430
+ const prompt = buildReviewPrompt(planPacket, planContent, revisionDirective);
58
431
  const promptHash = createHash('sha256').update(prompt).digest('hex');
59
- // Spawn reviewers in parallel
60
432
  const reviewPromises = [];
61
433
  for (let i = 0; i < numReviewers; i++) {
62
434
  const provider = providers[i % providers.length];
@@ -65,22 +437,22 @@ export class ConsensusRunner {
65
437
  return Promise.all(reviewPromises);
66
438
  }
67
439
  /** Iterative review: wraps existing iterateUntilConsensus */
68
- async runIterativeReview(planPacket) {
69
- const prompt = buildReviewPrompt(planPacket);
440
+ async runIterativeReview(planPacket, planContent, revisionDirective) {
441
+ const prompt = buildReviewPrompt(planPacket, planContent, revisionDirective);
70
442
  try {
71
443
  const result = await iterateUntilConsensus(prompt, `Phase: ${planPacket.metadata.phase}`, {
72
444
  projectDir: this.config.projectDir,
73
445
  config: this.config.consensusConfig,
74
446
  });
75
- // Convert legacy result to ReviewerVote format
447
+ const iterativeConfidence = (result.finalScore ?? 50) / 100;
76
448
  const vote = {
77
449
  reviewer_id: 'iterative-reviewer',
78
450
  provider: 'openai',
79
451
  model: this.config.consensusConfig?.openaiModel ?? 'gpt-4.1',
80
452
  temperature: this.config.consensusConfig?.temperature ?? 0.3,
81
453
  prompt_hash: createHash('sha256').update(prompt).digest('hex'),
82
- vote: result.approved ? 'APPROVE' : 'REJECT',
83
- confidence: result.finalScore ?? 0.5,
454
+ vote: mapVote(iterativeConfidence, this.config.threshold),
455
+ confidence: iterativeConfidence,
84
456
  blocking_issues: result.finalConcerns ?? [],
85
457
  suggestions: result.finalRecommendations ?? [],
86
458
  evidence_refs: [],
@@ -102,21 +474,35 @@ export class ConsensusRunner {
102
474
  }];
103
475
  }
104
476
  }
105
- /** Spawn a single independent reviewer */
477
+ /**
478
+ * Spawn a single independent reviewer.
479
+ * Governance rule: vote is ALWAYS derived from confidence via mapVote().
480
+ * The LLM's explicit vote is advisory only — logged for debugging.
481
+ */
106
482
  async spawnSingleReviewer(prompt, promptHash, provider, reviewerId) {
107
483
  try {
108
484
  const result = await this.callProviderForReview(prompt, provider);
485
+ // Governance: always derive vote from confidence, never trust LLM's explicit vote
486
+ const derived = mapVote(result.confidence, this.config.threshold);
487
+ const modelVote = result.modelVote ?? null;
488
+ const reviewer_inconsistency = modelVote !== null && modelVote !== derived;
489
+ if (reviewer_inconsistency) {
490
+ logger.log(`[consensus] ${provider.provider}: model said ${modelVote} but confidence ${result.confidence.toFixed(3)} -> derived ${derived}`);
491
+ }
492
+ logger.log(`[consensus] ${provider.provider}/${provider.model}: vote=${derived} confidence=${result.confidence.toFixed(3)} modelVote=${modelVote ?? 'none'} blockers=${result.blockingIssues.length}`);
109
493
  return {
110
494
  reviewer_id: reviewerId,
111
495
  provider: provider.provider,
112
496
  model: provider.model,
113
497
  temperature: provider.temperature,
114
498
  prompt_hash: promptHash,
115
- vote: result.approved ? 'APPROVE' : 'REJECT',
499
+ vote: derived,
116
500
  confidence: result.confidence,
117
501
  blocking_issues: result.blockingIssues,
502
+ required_changes: result.requiredChanges ?? [],
118
503
  suggestions: result.suggestions,
119
504
  evidence_refs: [],
505
+ reviewer_inconsistency,
120
506
  };
121
507
  }
122
508
  catch {
@@ -134,48 +520,349 @@ export class ConsensusRunner {
134
520
  };
135
521
  }
136
522
  }
137
- /** Call the appropriate provider adapter for a review */
523
+ /**
524
+ * Call the appropriate provider adapter for a review.
525
+ * Uses requestRawReview() to bypass adapter prompt wrapping/parsing —
526
+ * the runner owns the prompt and parses the raw LLM response itself.
527
+ */
138
528
  async callProviderForReview(prompt, provider) {
529
+ let raw;
139
530
  switch (provider.provider) {
140
531
  case 'openai': {
141
- const { requestConsensus } = await import('../../adapters/openai.js');
142
- const result = await requestConsensus(prompt, '', {
532
+ const { requestRawReview } = await import('../../adapters/openai.js');
533
+ raw = await requestRawReview(prompt, {
143
534
  openaiModel: provider.model,
144
535
  temperature: provider.temperature,
145
536
  });
146
- return parseConsensusResult(result);
537
+ break;
147
538
  }
148
539
  case 'gemini': {
149
- const { requestConsensus } = await import('../../adapters/gemini.js');
150
- const result = await requestConsensus(prompt, '', {
540
+ const { requestRawReview } = await import('../../adapters/gemini.js');
541
+ raw = await requestRawReview(prompt, {
151
542
  model: provider.model,
152
543
  temperature: provider.temperature,
153
544
  });
154
- return parseConsensusResult(result);
545
+ break;
155
546
  }
156
547
  case 'grok': {
157
- const { requestConsensus } = await import('../../adapters/grok.js');
158
- const result = await requestConsensus(prompt, '', {
548
+ const { requestRawReview } = await import('../../adapters/grok.js');
549
+ raw = await requestRawReview(prompt, {
159
550
  model: provider.model,
160
551
  temperature: provider.temperature,
161
552
  });
162
- return parseConsensusResult(result);
553
+ break;
163
554
  }
164
555
  default:
165
556
  throw new Error(`Unknown provider: ${provider.provider}`);
166
557
  }
558
+ logger.log(`[consensus] raw(${provider.provider}/${provider.model}): ${raw.slice(0, 500)}`);
559
+ return parseRawReviewResponse(raw);
560
+ }
561
+ /**
562
+ * Call arbitrator provider for tie-breaking (v2.1).
563
+ * v2.4.2: Rotates arbitrator away from dissenting reviewers to prevent
564
+ * systematic failure (e.g., Gemini rejects as reviewer + as arbitrator).
565
+ */
566
+ async callArbitrator(planPacket, votes, _rules, planContent) {
567
+ let provider = this.config.arbitratorProvider;
568
+ if (!provider)
569
+ return null;
570
+ // v2.4.2: Rotate arbitrator away from dissenting reviewers
571
+ const dissentingProviders = new Set(votes.filter(v => v.vote === 'REJECT').map(v => v.provider));
572
+ if (dissentingProviders.has(provider.provider)) {
573
+ const configuredProviders = new Set((this.config.reviewerProviders ?? DEFAULT_PROVIDERS).map(p => p.provider));
574
+ if (this.config.arbitratorProvider)
575
+ configuredProviders.add(this.config.arbitratorProvider.provider);
576
+ const ARBITRATOR_FALLBACK_ORDER = ['openai', 'grok', 'gemini'];
577
+ const alternate = ARBITRATOR_FALLBACK_ORDER.find(p => !dissentingProviders.has(p) && configuredProviders.has(p));
578
+ if (alternate && alternate !== provider.provider) {
579
+ const model = getModelForProvider(this.config.consensusConfig, alternate);
580
+ logger.log(`[consensus] Arbitrator rotation: ${provider.provider} is a dissenter, switching to ${alternate}/${model}`);
581
+ provider = { provider: alternate, model, temperature: 0.2 };
582
+ }
583
+ else {
584
+ logger.warn(`[consensus] Arbitrator rotation: no configured non-dissenter provider available, keeping ${provider.provider}`);
585
+ }
586
+ }
587
+ try {
588
+ const prompt = buildArbitrationPrompt(planPacket, votes, planContent);
589
+ // v2.6.0: Use shared queryProvider for adapter wiring + timeout
590
+ const raw = await queryProvider(prompt, provider);
591
+ if (!raw)
592
+ return null;
593
+ logger.log(`[consensus] arbitrator raw(${provider.provider}/${provider.model}): ${raw.slice(0, 500)}`);
594
+ // v2.4.3: Dedicated arbitrator response parser (not reviewer schema)
595
+ const parsed = parseArbitratorResponse(raw);
596
+ logger.log(`[consensus] Arbitrator decision: approved=${parsed.approved} ` +
597
+ `suggestedChanges=${parsed.suggestedChanges.length}`);
598
+ return {
599
+ approved: parsed.approved,
600
+ score: parsed.approved ? 90 : 10,
601
+ analysis: raw.slice(0, 2000),
602
+ criticalConcerns: [],
603
+ minorConcerns: [],
604
+ subjectiveConcerns: [],
605
+ reasoning: parsed.reasoning || raw.slice(0, 2000),
606
+ suggestedChanges: parsed.suggestedChanges,
607
+ rawResponse: raw,
608
+ };
609
+ }
610
+ catch (err) {
611
+ logger.warn(`[consensus] Arbitration call failed: ${err instanceof Error ? err.message : 'unknown'}`);
612
+ return null;
613
+ }
614
+ }
615
+ }
616
+ // ─── JSON-first Response Parsing ─────────────────────────
617
+ /**
618
+ * Zod schema for structured JSON review responses from the LLM.
619
+ */
620
+ const ReviewResponseSchema = z.object({
621
+ vote: z.enum(['APPROVE', 'CONDITIONAL', 'REJECT']),
622
+ confidence: z.number().min(0).max(1),
623
+ blocking_issues: z.array(z.string()).default([]),
624
+ required_changes: z.array(z.string()).default([]),
625
+ suggestions: z.array(z.string()).default([]),
626
+ analysis: z.string().optional(),
627
+ });
628
+ /**
629
+ * Parse raw LLM response text into a ProviderReviewResult.
630
+ * Strategy 1: Try JSON parse first (expected format).
631
+ * Strategy 2: Regex fallback for free-form text responses.
632
+ *
633
+ * @param raw - Raw text from the LLM
634
+ * @returns Parsed review result with confidence, issues, and advisory vote
635
+ */
636
+ export function parseRawReviewResponse(raw) {
637
+ const jsonResult = tryParseJSON(raw);
638
+ const result = jsonResult ?? parseRegexFallback(raw);
639
+ // Correct confidence if vote and confidence are semantically contradictory
640
+ const { confidence, wasContradiction, original } = correctConfidenceContradiction(result.modelVote ?? null, result.confidence);
641
+ if (wasContradiction) {
642
+ logger.warn(`[consensus] Confidence contradiction corrected: vote=${result.modelVote} `
643
+ + `conf=${original.toFixed(3)} -> corrected=${confidence.toFixed(3)}`);
167
644
  }
645
+ return { ...result, confidence };
168
646
  }
169
- function parseConsensusResult(result) {
647
+ /**
648
+ * Attempt to parse a JSON response, optionally wrapped in markdown code fences.
649
+ */
650
+ function tryParseJSON(raw) {
651
+ // Extract JSON from response (may be wrapped in markdown code fences)
652
+ const jsonMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
653
+ const candidate = (jsonMatch ? jsonMatch[1] : raw).trim();
654
+ if (!candidate)
655
+ return null;
656
+ try {
657
+ const parsed = JSON.parse(candidate);
658
+ const validated = ReviewResponseSchema.safeParse(parsed);
659
+ if (!validated.success)
660
+ return null;
661
+ const d = validated.data;
662
+ return {
663
+ confidence: d.confidence,
664
+ blockingIssues: d.blocking_issues,
665
+ suggestions: d.suggestions,
666
+ requiredChanges: d.required_changes,
667
+ modelVote: d.vote,
668
+ };
669
+ }
670
+ catch {
671
+ return null;
672
+ }
673
+ }
674
+ /**
675
+ * Regex fallback parser for free-form text responses.
676
+ * Extracts vote, confidence, and issue lists from unstructured text.
677
+ */
678
+ function parseRegexFallback(raw) {
679
+ // Extract vote (advisory only)
680
+ const voteMatch = raw.match(/\bVOTE:\s*(APPROVE|REJECT|CONDITIONAL)\b/i)
681
+ || raw.match(/\b(APPROVE|REJECT|CONDITIONAL)\b/i);
682
+ const modelVote = voteMatch
683
+ ? voteMatch[1].toUpperCase()
684
+ : null;
685
+ // Extract confidence (0-1 scale) — try multiple patterns
686
+ // Note: JSON keys have quotes ("confidence": 0.88), so patterns must handle optional quotes
687
+ let confidence = 0;
688
+ const confPatterns = [
689
+ /"?CONFIDENCE"?\s*:\s*(\d+\.?\d*)/i,
690
+ /"?[Cc]onfidence"?\s*(?:score)?[:\s]+(\d+\.?\d*)/,
691
+ /(\d+\.?\d*)\s*\/\s*1(?:\.0)?/,
692
+ ];
693
+ for (const pattern of confPatterns) {
694
+ const match = raw.match(pattern);
695
+ if (match) {
696
+ const val = parseFloat(match[1]);
697
+ confidence = val > 1 ? val / 100 : val;
698
+ break;
699
+ }
700
+ }
701
+ // Fallback: CONSENSUS: XX% format (legacy adapter format)
702
+ if (confidence === 0) {
703
+ const consensusMatch = raw.match(/CONSENSUS:\s*(\d+)%/i);
704
+ if (consensusMatch)
705
+ confidence = parseInt(consensusMatch[1], 10) / 100;
706
+ }
707
+ // Extract issues — handle flexible section headings and tagged items
708
+ const blockingIssues = extractTaggedList(raw, 'BLOCKER')
709
+ .concat(extractSectionList(raw, 'BLOCKING.?ISSUES'));
710
+ const requiredChanges = extractTaggedList(raw, 'REQUIRED')
711
+ .concat(extractSectionList(raw, 'REQUIRED.?CHANGES'));
712
+ const suggestions = extractTaggedList(raw, 'SUGGESTION')
713
+ .concat(extractSectionList(raw, 'SUGGESTIONS', 'CONCERNS', 'RECOMMENDATIONS'));
170
714
  return {
171
- approved: result.approved,
172
- confidence: result.score / 100, // score is 0-100, confidence is 0-1
173
- blockingIssues: result.concerns ?? [],
174
- suggestions: result.recommendations ?? [],
715
+ confidence: Math.max(0, Math.min(1, confidence)),
716
+ blockingIssues: dedup(blockingIssues),
717
+ suggestions: dedup(suggestions),
718
+ requiredChanges: dedup(requiredChanges),
719
+ modelVote,
175
720
  };
176
721
  }
722
+ /**
723
+ * Extract items prefixed with [TAG] from raw text.
724
+ * E.g. "[BLOCKER] SQL injection vulnerability" → "SQL injection vulnerability"
725
+ */
726
+ function extractTaggedList(raw, tag) {
727
+ const regex = new RegExp(`\\[${tag}\\]\\s*:?\\s*(.+)`, 'gi');
728
+ const items = [];
729
+ let m;
730
+ while ((m = regex.exec(raw)) !== null)
731
+ items.push(m[1].trim());
732
+ return items;
733
+ }
734
+ /**
735
+ * Extract bullet items from a named section (flexible headings).
736
+ * Handles "BLOCKING ISSUES:", "BLOCKING_ISSUES:", "Blocking Issues:", etc.
737
+ */
738
+ function extractSectionList(raw, ...patterns) {
739
+ for (const pat of patterns) {
740
+ const regex = new RegExp(`${pat}[:\\s]*\\n([\\s\\S]*?)(?=\\n(?:[A-Z][A-Z_\\s]+:|##)|$)`, 'i');
741
+ const match = raw.match(regex);
742
+ if (match) {
743
+ return match[1]
744
+ .split('\n')
745
+ .map(l => l.replace(/^[\s]*[-*]\s*/, '').replace(/^\d+\.\s*/, '').trim())
746
+ .filter(l => l.length > 0 && !/^none$/i.test(l));
747
+ }
748
+ }
749
+ return [];
750
+ }
751
+ /**
752
+ * Deduplicate a string array (case-insensitive).
753
+ */
754
+ function dedup(items) {
755
+ const seen = new Set();
756
+ return items.filter(i => {
757
+ const key = i.toLowerCase().trim();
758
+ if (seen.has(key))
759
+ return false;
760
+ seen.add(key);
761
+ return true;
762
+ });
763
+ }
764
+ // ─── Confidence Contradiction Correction ─────────────────
765
+ /**
766
+ * Correct confidence when it contradicts the model's explicit vote.
767
+ *
768
+ * The prompt defines confidence as "plan quality score" (0-1) and
769
+ * assigns ranges: REJECT < 0.80, CONDITIONAL 0.80-0.94, APPROVE >= 0.95.
770
+ * Some models confuse this with "assessment certainty" and return e.g.
771
+ * REJECT + 0.99 ("99% sure it's bad"). This function inverts such
772
+ * contradictions so mapVote() receives a semantically correct input.
773
+ *
774
+ * Correction is SYMMETRIC across all three bands:
775
+ * - REJECT + conf >= 0.80 -> invert: min(0.79, 1 - conf)
776
+ * - CONDITIONAL + conf >= 0.95 -> snap to midpoint 0.87
777
+ * - CONDITIONAL + conf < 0.80 -> snap to midpoint 0.87
778
+ * - APPROVE + conf < 0.80 -> invert: max(0.95, 1 - conf)
779
+ * - APPROVE + conf in [0.80, 0.95) -> snap to 0.95
780
+ *
781
+ * If modelVote is null (regex fallback couldn't find a vote), no correction.
782
+ */
783
+ export function correctConfidenceContradiction(modelVote, rawConfidence) {
784
+ if (modelVote === null) {
785
+ return { confidence: rawConfidence, wasContradiction: false, original: rawConfidence };
786
+ }
787
+ const c = Math.max(0, Math.min(1, rawConfidence));
788
+ // REJECT + confidence >= 0.80: model confused "certainty" with "quality"
789
+ // Invert, cap at 0.79 (top of REJECT range)
790
+ if (modelVote === 'REJECT' && c >= 0.80) {
791
+ const corrected = Math.min(0.79, 1.0 - c);
792
+ return { confidence: corrected, wasContradiction: true, original: c };
793
+ }
794
+ // CONDITIONAL outside its range [0.80, 0.95): snap to midpoint 0.87
795
+ if (modelVote === 'CONDITIONAL' && (c >= 0.95 || c < 0.80)) {
796
+ return { confidence: 0.87, wasContradiction: true, original: c };
797
+ }
798
+ // APPROVE + confidence < 0.80: model confused semantics
799
+ // Invert, floor at 0.95 (bottom of APPROVE range)
800
+ if (modelVote === 'APPROVE' && c < 0.80) {
801
+ const corrected = Math.max(0.95, 1.0 - c);
802
+ return { confidence: corrected, wasContradiction: true, original: c };
803
+ }
804
+ // APPROVE + confidence in [0.80, 0.95): slightly off, snap to 0.95
805
+ if (modelVote === 'APPROVE' && c < 0.95) {
806
+ return { confidence: 0.95, wasContradiction: true, original: c };
807
+ }
808
+ return { confidence: c, wasContradiction: false, original: c };
809
+ }
810
+ // ─── Arbitrator Response Parser (v2.4.3) ─────────────────
811
+ /**
812
+ * Zod schema for arbitrator JSON responses.
813
+ * Accepts both camelCase and snake_case for suggestedChanges.
814
+ */
815
+ const ArbitratorResponseSchema = z.object({
816
+ approved: z.boolean(),
817
+ reasoning: z.string().optional(),
818
+ suggestedChanges: z.array(z.string()).default([]),
819
+ suggested_changes: z.array(z.string()).default([]),
820
+ });
821
+ /**
822
+ * Parse raw arbitrator response into a structured result.
823
+ * Strategy 1: JSON parse (optionally wrapped in code fences).
824
+ * Strategy 2: Regex fallback for free-form text.
825
+ *
826
+ * @param raw - Raw text from the arbitrator LLM
827
+ * @returns Parsed result with approved boolean, reasoning, and suggested changes
828
+ */
829
+ export function parseArbitratorResponse(raw) {
830
+ // Strategy 1: JSON parse (with optional code fence wrapping)
831
+ const jsonMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
832
+ const candidate = (jsonMatch ? jsonMatch[1] : raw).trim();
833
+ try {
834
+ const parsed = JSON.parse(candidate);
835
+ const validated = ArbitratorResponseSchema.safeParse(parsed);
836
+ if (validated.success) {
837
+ const data = validated.data;
838
+ return {
839
+ approved: data.approved,
840
+ reasoning: data.reasoning ?? '',
841
+ suggestedChanges: [
842
+ ...(data.suggestedChanges ?? []),
843
+ ...(data.suggested_changes ?? []),
844
+ ],
845
+ };
846
+ }
847
+ }
848
+ catch { /* fall through to regex */ }
849
+ // Strategy 2: Regex fallback for free-form text
850
+ let approved = false;
851
+ const approvedMatch = raw.match(/approved\s*[:=]\s*(true|false)/i) ??
852
+ raw.match(/\b(approve|approved|accept|accepted)\b/i) ??
853
+ raw.match(/\b(reject|rejected|deny|denied)\b/i);
854
+ if (approvedMatch) {
855
+ const val = approvedMatch[1].toLowerCase();
856
+ approved = ['true', 'approve', 'approved', 'accept', 'accepted'].includes(val);
857
+ }
858
+ const changes = [];
859
+ const changeMatches = raw.matchAll(/(?:^|\n)\s*[-*\d.]+\s+(.+)/g);
860
+ for (const m of changeMatches)
861
+ changes.push(m[1].trim());
862
+ return { approved, reasoning: raw.slice(0, 2000), suggestedChanges: changes };
863
+ }
177
864
  // ─── Prompt Builder ──────────────────────────────────────
178
- export function buildReviewPrompt(planPacket) {
865
+ export function buildReviewPrompt(planPacket, planContent, revisionDirective) {
179
866
  const lines = [
180
867
  `# Independent Plan Review`,
181
868
  ``,
@@ -190,16 +877,99 @@ export function buildReviewPrompt(planPacket) {
190
877
  ...planPacket.constraints.map((c) => `- [${c.type}] ${c.description}`),
191
878
  ``,
192
879
  ];
880
+ // Render plan content (loaded from disk by caller)
881
+ if (planContent && planContent.trim().length > 0) {
882
+ lines.push(`## Plan Content`, ``, planContent, ``);
883
+ }
884
+ else {
885
+ lines.push(`## Plan Content`, ``, `[WARNING: Plan content could not be loaded. Review based on metadata only.]`, ``);
886
+ }
193
887
  if (planPacket.open_questions?.length) {
194
888
  lines.push(`## Open Questions`);
195
889
  lines.push(...planPacket.open_questions.map((q) => `- ${q}`));
196
890
  lines.push('');
197
891
  }
198
- lines.push(`## Review Instructions`, ``, `You are an independent reviewer. Evaluate this plan for:`, `1. Completeness — are all required artifacts defined?`, `2. Consistency — do acceptance criteria match constraints?`, `3. Feasibility — can this be implemented as described?`, `4. Constitution compliance — does it follow governance rules?`, ``, `Respond with:`, `- APPROVE, REJECT, or CONDITIONAL`, `- Confidence score (0-1)`, `- Blocking issues (if any)`, `- Suggestions for improvement`);
892
+ lines.push(`## Review Instructions`, ``, `You are an independent reviewer. Evaluate this plan for:`, `1. Completeness — are all required artifacts defined?`, `2. Consistency — do acceptance criteria match constraints?`, `3. Feasibility — can this be implemented as described?`, `4. Constitution compliance — does it follow governance rules?`, ``, `## Scoring Guide`, ``, `The "confidence" field represents your assessment of PLAN QUALITY, NOT how certain you are about your review.`, `It answers: "How ready is this plan for execution on a scale of 0.00 to 1.00?"`, ``, `- confidence 0.95-1.00 (vote APPROVE): The plan is EXECUTION-READY as-is.`, `- confidence 0.80-0.94 (vote CONDITIONAL): The plan needs specific changes before execution.`, `- confidence below 0.80 (vote REJECT): The plan has fundamental issues.`, ``, `CRITICAL: Your vote and confidence MUST be consistent:`, ` - REJECT requires confidence below 0.80`, ` - CONDITIONAL requires confidence between 0.80 and 0.94`, ` - APPROVE requires confidence 0.95 or above`, `Do NOT use confidence to express how certain you are of your assessment.`, `A REJECT with confidence 0.99 is INVALID -- it implies the plan is 99% ready while rejecting it.`, `Mismatched vote+confidence will be auto-corrected by the system.`, ``, `IMPORTANT: "Execution-ready" means a competent developer could implement this plan successfully, not that the plan is theoretically perfect. Reserve CONDITIONAL for changes that would cause implementation to fail or produce incorrect results, not style preferences.`, ``, `## Output Format for Issues`, `- Prefix blocking issues with [BLOCKER]: items that MUST be fixed before approval`, `- Prefix required changes with [REQUIRED]: items that need changes but are not deal-breakers`, `- Prefix suggestions with [SUGGESTION]: nice-to-have improvements`, ``, `IMPORTANT: If your vote is APPROVE or CONDITIONAL, do NOT list [BLOCKER] items.`, `[BLOCKER] items are only valid with a REJECT vote.`, ``, `## Response Format`, ``, `Return ONLY a JSON object matching this schema:`, ``, '```json', `{`, ` "vote": "APPROVE" | "CONDITIONAL" | "REJECT",`, ` "confidence": 0.00, // Plan quality score, NOT review certainty`, ` "blocking_issues": ["[BLOCKER] ..."],`, ` "required_changes": ["[REQUIRED] ..."],`, ` "suggestions": ["[SUGGESTION] ..."],`, ` "analysis": "Your detailed analysis here"`, `}`, '```', ``, `### Examples of VALID responses:`, `- APPROVE with confidence 0.97: "Plan is solid, minor style nits only"`, `- CONDITIONAL with confidence 0.85: "Need to add error handling for X"`, `- REJECT with confidence 0.45: "Missing entire auth layer, unclear data model"`, ``, `### Examples of INVALID responses (will be auto-corrected):`, `- REJECT with confidence 0.99: This means "plan is 99% ready" while rejecting it`, `- APPROVE with confidence 0.60: This means "plan has issues" while approving it`, ``, `Confidence = plan quality score (NOT review certainty):`, `- 0.95-1.00: APPROVE range -- plan is execution-ready`, `- 0.80-0.94: CONDITIONAL range -- specific changes needed`, `- Below 0.80: REJECT range -- fundamental issues`, ``, `Your vote and confidence MUST fall in the same range. Mismatches will be auto-corrected.`, ``, `If vote is APPROVE: blocking_issues and required_changes must be empty arrays.`, `If vote is CONDITIONAL: blocking_issues must be empty, use required_changes.`, `If vote is REJECT: use blocking_issues for critical issues.`);
893
+ // v2.4.2: Add revision notice + prior feedback for plan revisions
894
+ if (planPacket.metadata.version > 1) {
895
+ lines.push(``, `## Revision Notice`, ``, `This is revision ${planPacket.metadata.version} of the plan.`, `Prioritize verifying whether prior issues have been adequately addressed.`, `Also flag any new *critical* issues you discover.`, ``);
896
+ }
897
+ if (revisionDirective && revisionDirective.trim().length > 0) {
898
+ const trimmed = revisionDirective.trim();
899
+ const capped = trimmed.length > 2000
900
+ ? trimmed.slice(0, 2000) + '\n\n[TRUNCATED -- full directive exceeds 2000 chars]'
901
+ : trimmed;
902
+ lines.push(`## Prior Feedback (Must Address)`, ``, capped, ``, `Confirm each item above is addressed or explain why it is not applicable.`, ``);
903
+ }
199
904
  return lines.join('\n');
200
905
  }
906
+ /**
907
+ * Build arbitration prompt with reviewer feedback context.
908
+ */
909
+ function buildArbitrationPrompt(planPacket, votes, planContent) {
910
+ const voteSummary = votes.map((v, i) => {
911
+ const parts = [
912
+ `### Reviewer ${i + 1} (${v.provider}/${v.model})`,
913
+ `Vote: ${v.vote} (confidence: ${v.confidence.toFixed(2)})`,
914
+ ];
915
+ if (v.blocking_issues.length > 0) {
916
+ parts.push(`Blocking: ${v.blocking_issues.join('; ')}`);
917
+ }
918
+ if (v.required_changes?.length) {
919
+ parts.push(`Required changes: ${v.required_changes.join('; ')}`);
920
+ }
921
+ if (v.suggestions.length > 0) {
922
+ parts.push(`Suggestions: ${v.suggestions.join('; ')}`);
923
+ }
924
+ return parts.join('\n');
925
+ }).join('\n\n');
926
+ const planSection = (planContent && planContent.trim().length > 0)
927
+ ? [`## Plan Content`, ``, planContent, ``]
928
+ : [`## Plan Content`, ``, `[WARNING: Plan content could not be loaded.]`, ``];
929
+ return [
930
+ `# Arbitration Request`,
931
+ ``,
932
+ `## Phase: ${planPacket.metadata.phase}`,
933
+ `## Plan Version: ${planPacket.metadata.version}`,
934
+ ``,
935
+ ...planSection,
936
+ `## Reviewer Votes`,
937
+ voteSummary,
938
+ ``,
939
+ `## Instructions`,
940
+ `The reviewers above could not reach consensus. As arbitrator:`,
941
+ `1. Analyze the disagreement points`,
942
+ `2. Determine if the plan is execution-ready with minor amendments`,
943
+ `3. If approving, provide specific suggestedChanges that address each required_change`,
944
+ `4. If the issues are fundamental, do NOT approve`,
945
+ ``,
946
+ `Provide your decision as: approved (true/false), reasoning, and suggestedChanges array.`,
947
+ ].join('\n');
948
+ }
201
949
  // ─── Factory ─────────────────────────────────────────────
202
- export function createConsensusRunner(projectDir, consensusConfig) {
950
+ /**
951
+ * Helper to resolve model string for a given provider from consensus config.
952
+ */
953
+ export function getModelForProvider(config, provider) {
954
+ if (!config)
955
+ return provider === 'openai' ? 'gpt-4.1' : provider === 'gemini' ? 'gemini-2.5-flash' : 'grok-3';
956
+ switch (provider) {
957
+ case 'openai': return config.openaiModel ?? 'gpt-4.1';
958
+ case 'gemini': return config.geminiModel ?? 'gemini-2.5-flash';
959
+ case 'grok': return config.grokModel ?? 'grok-3';
960
+ default: return 'gpt-4.1';
961
+ }
962
+ }
963
+ export function createConsensusRunner(projectDir, consensusConfig, skillLoader, skillUsageRegistry) {
964
+ // Wire arbitration from consensus config
965
+ const enableArbitration = consensusConfig?.enableArbitration !== false;
966
+ const arbitratorProvider = enableArbitration
967
+ ? {
968
+ provider: consensusConfig?.arbitrator ?? 'gemini',
969
+ model: getModelForProvider(consensusConfig, consensusConfig?.arbitrator ?? 'gemini'),
970
+ temperature: 0.2,
971
+ }
972
+ : undefined;
203
973
  return new ConsensusRunner({
204
974
  mode: 'independent',
205
975
  minReviewers: 2,
@@ -207,6 +977,10 @@ export function createConsensusRunner(projectDir, consensusConfig) {
207
977
  quorum: 2,
208
978
  projectDir,
209
979
  consensusConfig,
980
+ arbitratorProvider,
981
+ enableArbitration,
982
+ skillLoader,
983
+ skillUsageRegistry,
210
984
  });
211
985
  }
212
986
  //# sourceMappingURL=consensus-runner.js.map