popeye-cli 2.2.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 (323) 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 +324 -20
  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 +3 -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/auto-recovery.d.ts +56 -0
  71. package/dist/pipeline/auto-recovery.d.ts.map +1 -0
  72. package/dist/pipeline/auto-recovery.js +185 -0
  73. package/dist/pipeline/auto-recovery.js.map +1 -0
  74. package/dist/pipeline/change-request.d.ts +39 -0
  75. package/dist/pipeline/change-request.d.ts.map +1 -1
  76. package/dist/pipeline/change-request.js +40 -1
  77. package/dist/pipeline/change-request.js.map +1 -1
  78. package/dist/pipeline/check-runner.d.ts +30 -1
  79. package/dist/pipeline/check-runner.d.ts.map +1 -1
  80. package/dist/pipeline/check-runner.js +122 -1
  81. package/dist/pipeline/check-runner.js.map +1 -1
  82. package/dist/pipeline/command-resolver.d.ts.map +1 -1
  83. package/dist/pipeline/command-resolver.js +33 -2
  84. package/dist/pipeline/command-resolver.js.map +1 -1
  85. package/dist/pipeline/consensus/arbitrator-query.d.ts +22 -0
  86. package/dist/pipeline/consensus/arbitrator-query.d.ts.map +1 -0
  87. package/dist/pipeline/consensus/arbitrator-query.js +70 -0
  88. package/dist/pipeline/consensus/arbitrator-query.js.map +1 -0
  89. package/dist/pipeline/consensus/consensus-runner.d.ts +131 -7
  90. package/dist/pipeline/consensus/consensus-runner.d.ts.map +1 -1
  91. package/dist/pipeline/consensus/consensus-runner.js +809 -35
  92. package/dist/pipeline/consensus/consensus-runner.js.map +1 -1
  93. package/dist/pipeline/cr-lifecycle.d.ts +42 -0
  94. package/dist/pipeline/cr-lifecycle.d.ts.map +1 -0
  95. package/dist/pipeline/cr-lifecycle.js +89 -0
  96. package/dist/pipeline/cr-lifecycle.js.map +1 -0
  97. package/dist/pipeline/gate-engine.d.ts +1 -0
  98. package/dist/pipeline/gate-engine.d.ts.map +1 -1
  99. package/dist/pipeline/gate-engine.js +26 -7
  100. package/dist/pipeline/gate-engine.js.map +1 -1
  101. package/dist/pipeline/orchestrator.d.ts +1 -1
  102. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  103. package/dist/pipeline/orchestrator.js +306 -16
  104. package/dist/pipeline/orchestrator.js.map +1 -1
  105. package/dist/pipeline/packets/consensus-packet-builder.d.ts +15 -4
  106. package/dist/pipeline/packets/consensus-packet-builder.d.ts.map +1 -1
  107. package/dist/pipeline/packets/consensus-packet-builder.js +29 -17
  108. package/dist/pipeline/packets/consensus-packet-builder.js.map +1 -1
  109. package/dist/pipeline/phases/architecture.d.ts.map +1 -1
  110. package/dist/pipeline/phases/architecture.js +5 -3
  111. package/dist/pipeline/phases/architecture.js.map +1 -1
  112. package/dist/pipeline/phases/audit.d.ts.map +1 -1
  113. package/dist/pipeline/phases/audit.js +5 -3
  114. package/dist/pipeline/phases/audit.js.map +1 -1
  115. package/dist/pipeline/phases/consensus-architecture.d.ts.map +1 -1
  116. package/dist/pipeline/phases/consensus-architecture.js +10 -1
  117. package/dist/pipeline/phases/consensus-architecture.js.map +1 -1
  118. package/dist/pipeline/phases/consensus-master-plan.d.ts.map +1 -1
  119. package/dist/pipeline/phases/consensus-master-plan.js +10 -3
  120. package/dist/pipeline/phases/consensus-master-plan.js.map +1 -1
  121. package/dist/pipeline/phases/consensus-role-plans.d.ts.map +1 -1
  122. package/dist/pipeline/phases/consensus-role-plans.js +10 -1
  123. package/dist/pipeline/phases/consensus-role-plans.js.map +1 -1
  124. package/dist/pipeline/phases/done.d.ts.map +1 -1
  125. package/dist/pipeline/phases/done.js +9 -4
  126. package/dist/pipeline/phases/done.js.map +1 -1
  127. package/dist/pipeline/phases/intake.d.ts.map +1 -1
  128. package/dist/pipeline/phases/intake.js +7 -3
  129. package/dist/pipeline/phases/intake.js.map +1 -1
  130. package/dist/pipeline/phases/phase-context.d.ts +2 -0
  131. package/dist/pipeline/phases/phase-context.d.ts.map +1 -1
  132. package/dist/pipeline/phases/phase-context.js +3 -1
  133. package/dist/pipeline/phases/phase-context.js.map +1 -1
  134. package/dist/pipeline/phases/production-gate.d.ts.map +1 -1
  135. package/dist/pipeline/phases/production-gate.js +28 -3
  136. package/dist/pipeline/phases/production-gate.js.map +1 -1
  137. package/dist/pipeline/phases/qa-validation.d.ts.map +1 -1
  138. package/dist/pipeline/phases/qa-validation.js +38 -5
  139. package/dist/pipeline/phases/qa-validation.js.map +1 -1
  140. package/dist/pipeline/phases/recovery-loop.d.ts +2 -0
  141. package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -1
  142. package/dist/pipeline/phases/recovery-loop.js +200 -6
  143. package/dist/pipeline/phases/recovery-loop.js.map +1 -1
  144. package/dist/pipeline/phases/review.d.ts.map +1 -1
  145. package/dist/pipeline/phases/review.js +58 -28
  146. package/dist/pipeline/phases/review.js.map +1 -1
  147. package/dist/pipeline/phases/role-planning.d.ts.map +1 -1
  148. package/dist/pipeline/phases/role-planning.js +18 -2
  149. package/dist/pipeline/phases/role-planning.js.map +1 -1
  150. package/dist/pipeline/phases/stuck.d.ts.map +1 -1
  151. package/dist/pipeline/phases/stuck.js +10 -0
  152. package/dist/pipeline/phases/stuck.js.map +1 -1
  153. package/dist/pipeline/repo-snapshot.d.ts.map +1 -1
  154. package/dist/pipeline/repo-snapshot.js +3 -0
  155. package/dist/pipeline/repo-snapshot.js.map +1 -1
  156. package/dist/pipeline/role-execution-adapter.d.ts +2 -1
  157. package/dist/pipeline/role-execution-adapter.d.ts.map +1 -1
  158. package/dist/pipeline/role-execution-adapter.js +22 -7
  159. package/dist/pipeline/role-execution-adapter.js.map +1 -1
  160. package/dist/pipeline/skill-loader.d.ts +19 -0
  161. package/dist/pipeline/skill-loader.d.ts.map +1 -1
  162. package/dist/pipeline/skill-loader.js +22 -0
  163. package/dist/pipeline/skill-loader.js.map +1 -1
  164. package/dist/pipeline/skills/coverage-gate.d.ts +44 -0
  165. package/dist/pipeline/skills/coverage-gate.d.ts.map +1 -0
  166. package/dist/pipeline/skills/coverage-gate.js +143 -0
  167. package/dist/pipeline/skills/coverage-gate.js.map +1 -0
  168. package/dist/pipeline/skills/usage-registry.d.ts +48 -0
  169. package/dist/pipeline/skills/usage-registry.d.ts.map +1 -0
  170. package/dist/pipeline/skills/usage-registry.js +55 -0
  171. package/dist/pipeline/skills/usage-registry.js.map +1 -0
  172. package/dist/pipeline/strategy-context.d.ts +20 -0
  173. package/dist/pipeline/strategy-context.d.ts.map +1 -0
  174. package/dist/pipeline/strategy-context.js +55 -0
  175. package/dist/pipeline/strategy-context.js.map +1 -0
  176. package/dist/pipeline/type-defs/artifacts.d.ts +25 -5
  177. package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -1
  178. package/dist/pipeline/type-defs/artifacts.js +4 -0
  179. package/dist/pipeline/type-defs/artifacts.js.map +1 -1
  180. package/dist/pipeline/type-defs/audit.d.ts +25 -13
  181. package/dist/pipeline/type-defs/audit.d.ts.map +1 -1
  182. package/dist/pipeline/type-defs/checks.d.ts +18 -8
  183. package/dist/pipeline/type-defs/checks.d.ts.map +1 -1
  184. package/dist/pipeline/type-defs/checks.js +4 -0
  185. package/dist/pipeline/type-defs/checks.js.map +1 -1
  186. package/dist/pipeline/type-defs/packets.d.ts +104 -18
  187. package/dist/pipeline/type-defs/packets.d.ts.map +1 -1
  188. package/dist/pipeline/type-defs/packets.js +17 -1
  189. package/dist/pipeline/type-defs/packets.js.map +1 -1
  190. package/dist/pipeline/type-defs/state.d.ts +160 -16
  191. package/dist/pipeline/type-defs/state.d.ts.map +1 -1
  192. package/dist/pipeline/type-defs/state.js +26 -1
  193. package/dist/pipeline/type-defs/state.js.map +1 -1
  194. package/dist/shared/text-utils.d.ts +23 -0
  195. package/dist/shared/text-utils.d.ts.map +1 -0
  196. package/dist/shared/text-utils.js +66 -0
  197. package/dist/shared/text-utils.js.map +1 -0
  198. package/dist/shared/website-strategy-format.d.ts +18 -0
  199. package/dist/shared/website-strategy-format.d.ts.map +1 -0
  200. package/dist/shared/website-strategy-format.js +47 -0
  201. package/dist/shared/website-strategy-format.js.map +1 -0
  202. package/dist/state/index.d.ts +2 -0
  203. package/dist/state/index.d.ts.map +1 -1
  204. package/dist/state/index.js +57 -8
  205. package/dist/state/index.js.map +1 -1
  206. package/dist/types/consensus.d.ts +1 -0
  207. package/dist/types/consensus.d.ts.map +1 -1
  208. package/dist/types/consensus.js.map +1 -1
  209. package/dist/types/website-strategy.d.ts +1 -1
  210. package/dist/types/workflow.d.ts +447 -0
  211. package/dist/types/workflow.d.ts.map +1 -1
  212. package/dist/types/workflow.js +3 -0
  213. package/dist/types/workflow.js.map +1 -1
  214. package/dist/upgrade/handlers.d.ts.map +1 -1
  215. package/dist/upgrade/handlers.js +6 -3
  216. package/dist/upgrade/handlers.js.map +1 -1
  217. package/dist/workflow/consensus.d.ts.map +1 -1
  218. package/dist/workflow/consensus.js +1 -0
  219. package/dist/workflow/consensus.js.map +1 -1
  220. package/dist/workflow/website-strategy.d.ts.map +1 -1
  221. package/dist/workflow/website-strategy.js +2 -29
  222. package/dist/workflow/website-strategy.js.map +1 -1
  223. package/dist/workflow/website-updater.d.ts.map +1 -1
  224. package/dist/workflow/website-updater.js +3 -2
  225. package/dist/workflow/website-updater.js.map +1 -1
  226. package/package.json +1 -1
  227. package/src/adapters/gemini.ts +51 -6
  228. package/src/adapters/grok.ts +51 -6
  229. package/src/adapters/openai.ts +53 -5
  230. package/src/cli/commands/create.ts +1 -1
  231. package/src/cli/interactive.ts +333 -19
  232. package/src/generators/all.ts +3 -2
  233. package/src/generators/doc-parser.ts +75 -15
  234. package/src/generators/templates/fullstack.ts +1 -1
  235. package/src/generators/templates/website-components.ts +1 -1
  236. package/src/generators/templates/website-config.ts +23 -11
  237. package/src/generators/templates/website-conversion.ts +1 -1
  238. package/src/generators/templates/website-landing.ts +1 -1
  239. package/src/generators/templates/website-layout.ts +491 -23
  240. package/src/generators/templates/website-pricing.ts +1 -1
  241. package/src/generators/templates/website-sections.ts +1 -1
  242. package/src/generators/templates/website-seo.ts +4 -1
  243. package/src/generators/templates/website.ts +3 -0
  244. package/src/generators/website-content-ai.ts +186 -0
  245. package/src/generators/website-content-scanner.ts +113 -1
  246. package/src/generators/website-context.ts +151 -12
  247. package/src/generators/website-debug.ts +26 -0
  248. package/src/generators/website.ts +28 -3
  249. package/src/pipeline/auto-recovery.ts +283 -0
  250. package/src/pipeline/change-request.ts +63 -1
  251. package/src/pipeline/check-runner.ts +141 -2
  252. package/src/pipeline/command-resolver.ts +34 -2
  253. package/src/pipeline/consensus/arbitrator-query.ts +101 -0
  254. package/src/pipeline/consensus/consensus-runner.ts +1099 -42
  255. package/src/pipeline/cr-lifecycle.ts +103 -0
  256. package/src/pipeline/gate-engine.ts +35 -7
  257. package/src/pipeline/orchestrator.ts +361 -16
  258. package/src/pipeline/packets/consensus-packet-builder.ts +44 -18
  259. package/src/pipeline/phases/architecture.ts +6 -3
  260. package/src/pipeline/phases/audit.ts +6 -3
  261. package/src/pipeline/phases/consensus-architecture.ts +10 -1
  262. package/src/pipeline/phases/consensus-master-plan.ts +10 -3
  263. package/src/pipeline/phases/consensus-role-plans.ts +10 -1
  264. package/src/pipeline/phases/done.ts +15 -4
  265. package/src/pipeline/phases/intake.ts +7 -3
  266. package/src/pipeline/phases/phase-context.ts +6 -1
  267. package/src/pipeline/phases/production-gate.ts +41 -3
  268. package/src/pipeline/phases/qa-validation.ts +51 -5
  269. package/src/pipeline/phases/recovery-loop.ts +229 -7
  270. package/src/pipeline/phases/review.ts +73 -30
  271. package/src/pipeline/phases/role-planning.ts +21 -2
  272. package/src/pipeline/phases/stuck.ts +10 -0
  273. package/src/pipeline/repo-snapshot.ts +3 -0
  274. package/src/pipeline/role-execution-adapter.ts +30 -4
  275. package/src/pipeline/skill-loader.ts +33 -0
  276. package/src/pipeline/skills/coverage-gate.ts +199 -0
  277. package/src/pipeline/skills/usage-registry.ts +87 -0
  278. package/src/pipeline/strategy-context.ts +60 -0
  279. package/src/pipeline/type-defs/artifacts.ts +4 -0
  280. package/src/pipeline/type-defs/checks.ts +4 -0
  281. package/src/pipeline/type-defs/packets.ts +18 -1
  282. package/src/pipeline/type-defs/state.ts +26 -1
  283. package/src/shared/text-utils.ts +70 -0
  284. package/src/shared/website-strategy-format.ts +56 -0
  285. package/src/state/index.ts +60 -8
  286. package/src/types/consensus.ts +1 -0
  287. package/src/types/workflow.ts +6 -0
  288. package/src/upgrade/handlers.ts +9 -3
  289. package/src/workflow/consensus.ts +1 -0
  290. package/src/workflow/website-strategy.ts +2 -36
  291. package/src/workflow/website-updater.ts +4 -2
  292. package/tests/adapters/gemini.test.ts +165 -0
  293. package/tests/adapters/grok.test.ts +137 -0
  294. package/tests/adapters/openai.test.ts +128 -0
  295. package/tests/generators/doc-parser.test.ts +88 -9
  296. package/tests/generators/quality-gate.test.ts +19 -3
  297. package/tests/generators/website-components.test.ts +34 -0
  298. package/tests/generators/website-content-ai.test.ts +308 -0
  299. package/tests/generators/website-content-scanner.test.ts +86 -0
  300. package/tests/generators/website-context.test.ts +3 -2
  301. package/tests/integration/smokestack-scaffold.test.ts +385 -0
  302. package/tests/pipeline/auto-recovery.test.ts +337 -0
  303. package/tests/pipeline/change-request.test.ts +70 -0
  304. package/tests/pipeline/command-resolver.test.ts +42 -0
  305. package/tests/pipeline/consensus/arbitrator-query.test.ts +107 -0
  306. package/tests/pipeline/consensus-runner.test.ts +1333 -10
  307. package/tests/pipeline/consensus-scoring.test.ts +602 -18
  308. package/tests/pipeline/gate-engine.test.ts +34 -0
  309. package/tests/pipeline/install-check.test.ts +261 -0
  310. package/tests/pipeline/orchestrator.test.ts +1506 -15
  311. package/tests/pipeline/packets/builders.test.ts +29 -6
  312. package/tests/pipeline/phases/role-planning.strategy.test.ts +204 -0
  313. package/tests/pipeline/pipeline-persistence.test.ts +230 -0
  314. package/tests/pipeline/recovery-loop-guidance.test.ts +280 -0
  315. package/tests/pipeline/role-execution-adapter.test.ts +88 -0
  316. package/tests/pipeline/skills/coverage-gate.test.ts +370 -0
  317. package/tests/pipeline/skills/usage-registry.test.ts +114 -0
  318. package/tests/pipeline/strategy-context.test.ts +148 -0
  319. package/tests/shared/text-utils.test.ts +155 -0
  320. package/tests/state/progress-analysis.test.ts +375 -0
  321. package/tests/upgrade/handlers.test.ts +33 -2
  322. package/tests/workflow/consensus.test.ts +6 -0
  323. package/tsconfig.json +1 -1
@@ -19,13 +19,16 @@ import type {
19
19
  PipelineResult,
20
20
  RCAPacket,
21
21
  } from './types.js';
22
- import { createDefaultPipelineState } from './types.js';
22
+ import { createDefaultPipelineState, toLegacyPhase } from './types.js';
23
23
  import { createGateEngine } from './gate-engine.js';
24
24
  import type { GateResult } from './gate-engine.js';
25
25
  import { createArtifactManager } from './artifact-manager.js';
26
26
  import { createSkillLoader } from './skill-loader.js';
27
27
  import { createConsensusRunner } from './consensus/consensus-runner.js';
28
28
  import { verifyConstitution } from './constitution.js';
29
+ import { SkillUsageRegistry } from './skills/usage-registry.js';
30
+ import { runSkillCoverageCheck } from './check-runner.js';
31
+ import { resolveActiveCR, checkStagnation } from './cr-lifecycle.js';
29
32
 
30
33
  import {
31
34
  runIntake,
@@ -46,6 +49,7 @@ import {
46
49
  import type { PhaseContext, PhaseResult } from './phases/index.js';
47
50
  import type { ProjectState } from '../types/workflow.js';
48
51
  import type { ConsensusConfig } from '../types/consensus.js';
52
+ import { updateState } from '../state/index.js';
49
53
 
50
54
  // ─── Types ───────────────────────────────────────────────
51
55
 
@@ -97,19 +101,24 @@ export async function runPipeline(options: PipelineOptions): Promise<PipelineRes
97
101
  } = options;
98
102
 
99
103
  // Initialize pipeline state if needed
100
- const pipeline: PipelineState = (state as unknown as { pipeline?: PipelineState }).pipeline
101
- ?? createDefaultPipelineState();
104
+ const pipeline: PipelineState = state.pipeline ?? createDefaultPipelineState();
105
+ // Attach pipeline to state so persistence includes it
106
+ state.pipeline = pipeline;
102
107
 
103
108
  // Persist user guidance in pipeline state so it survives resume
104
109
  if (additionalContext && !pipeline.sessionGuidance) {
105
110
  pipeline.sessionGuidance = additionalContext;
106
111
  }
107
112
 
113
+ // Initialize skill usage events array (v2.2.1)
114
+ pipeline.skillUsageEvents = pipeline.skillUsageEvents ?? [];
115
+ const skillUsageRegistry = new SkillUsageRegistry(pipeline.skillUsageEvents);
116
+
108
117
  // Create context dependencies
109
118
  const gateEngine = createGateEngine();
110
119
  const artifactManager = createArtifactManager(projectDir);
111
120
  const skillLoader = createSkillLoader(projectDir);
112
- const consensusRunner = createConsensusRunner(projectDir, consensusConfig);
121
+ const consensusRunner = createConsensusRunner(projectDir, consensusConfig, skillLoader, skillUsageRegistry);
113
122
 
114
123
  // Ensure docs structure
115
124
  artifactManager.ensureDocsStructure();
@@ -122,6 +131,7 @@ export async function runPipeline(options: PipelineOptions): Promise<PipelineRes
122
131
  artifactManager,
123
132
  gateEngine,
124
133
  consensusRunner,
134
+ skillUsageRegistry,
125
135
  };
126
136
 
127
137
  let phase = pipeline.pipelinePhase;
@@ -164,6 +174,16 @@ export async function runPipeline(options: PipelineOptions): Promise<PipelineRes
164
174
  onProgress?.(`Phase ${phase} FAILED: ${result.message}${result.error ? ` — ${result.error}` : ''}`);
165
175
  }
166
176
 
177
+ // v2.2.1: Run skill coverage check before gates that require it
178
+ if (phase === 'CONSENSUS_ROLE_PLANS' || phase === 'PRODUCTION_GATE') {
179
+ const coverageResult = runSkillCoverageCheck(pipeline, phase);
180
+ const phaseChecks = pipeline.gateChecks[phase] ?? [];
181
+ // Replace any existing skill_coverage check result
182
+ const filtered = phaseChecks.filter((c) => c.check_type !== 'skill_coverage');
183
+ filtered.push(coverageResult);
184
+ pipeline.gateChecks[phase] = filtered;
185
+ }
186
+
167
187
  // v1.1: Verify constitution integrity before evaluating gate
168
188
  const constitutionCheck = verifyConstitution(pipeline, projectDir);
169
189
 
@@ -179,13 +199,42 @@ export async function runPipeline(options: PipelineOptions): Promise<PipelineRes
179
199
  if (gateResult.pass) {
180
200
  // ─── PASS ────────────────────────────────────────
181
201
 
202
+ // v2.7.0: Clear failedPhase when the originally-failed phase now passes.
203
+ // INVARIANT: This is the ONLY place failedPhase is cleared during the main loop.
204
+ // It must remain set throughout the entire recovery traversal to guard budget resets.
205
+ if (pipeline.failedPhase === phase) {
206
+ pipeline.failedPhase = undefined;
207
+ pipeline.recoveryBaselineFailedCheckCount = undefined;
208
+ }
209
+
210
+ // v2.4.9: Resolve the active CR after its routed phase passes
211
+ if (pipeline.activeChangeRequestId) {
212
+ resolveActiveCR(pipeline, onProgress);
213
+ }
214
+
182
215
  // v1.1: Check for pending CRs after REVIEW/AUDIT — route to consensus before continuing
183
216
  if (CR_CHECK_PHASES.has(phase)) {
184
217
  const crRoute = getNextCRRoute(pipeline);
185
218
  if (crRoute) {
186
219
  onProgress?.(`CR ${crRoute.cr_id} routing to ${crRoute.target_phase}`);
220
+ // v2.5.4: CR routing to a new phase — reset recovery budget
221
+ if (crRoute.target_phase !== phase && pipeline.recoveryCount > 0) {
222
+ onProgress?.(`Recovery budget reset: ${pipeline.recoveryCount} -> 0 (CR routing ${phase} -> ${crRoute.target_phase})`);
223
+ pipeline.recoveryCount = 0;
224
+ pipeline.lastRewindTarget = undefined;
225
+ }
226
+ pipeline.activeChangeRequestId = crRoute.cr_id;
187
227
  phase = crRoute.target_phase;
188
228
  pipeline.pipelinePhase = phase;
229
+ // v2.5.0: Stagnation check before continue (otherwise skipped by the continue)
230
+ if (checkStagnation(pipeline, onProgress)) {
231
+ phase = 'STUCK';
232
+ pipeline.pipelinePhase = phase;
233
+ }
234
+ // Persist before continue (otherwise CR-routed phase is lost on crash)
235
+ try {
236
+ await updateState(projectDir, { pipeline, phase: toLegacyPhase(phase) });
237
+ } catch { /* non-fatal */ }
189
238
  continue;
190
239
  }
191
240
  }
@@ -193,23 +242,121 @@ export async function runPipeline(options: PipelineOptions): Promise<PipelineRes
193
242
  if (phase === 'RECOVERY_LOOP') {
194
243
  // Recovery succeeded. RCA may specify rewind target.
195
244
  const rca = getLatestRCA(pipeline, projectDir);
196
- if (rca?.requires_phase_rewind_to) {
197
- phase = rca.requires_phase_rewind_to;
198
- } else {
199
- // Retest the phase that failed
200
- phase = failedPhase ?? 'QA_VALIDATION';
245
+ let rewindTarget = rca?.requires_phase_rewind_to;
246
+
247
+ // v2.4.6: Detect repeated same-target rewind — if we already rewound
248
+ // to this target last iteration and it didn't help, skip the rewind
249
+ // and re-test the failed phase directly.
250
+ if (rewindTarget && rewindTarget === pipeline.lastRewindTarget) {
251
+ onProgress?.(
252
+ `Repeated rewind to ${rewindTarget} detected ` +
253
+ `(recovery #${pipeline.recoveryCount}) — re-testing ` +
254
+ `${failedPhase ?? 'QA_VALIDATION'} directly`,
255
+ );
256
+ rewindTarget = undefined;
201
257
  }
258
+
259
+ // Determine effective target
260
+ const effectiveTarget = rewindTarget ?? failedPhase ?? 'QA_VALIDATION';
261
+ phase = effectiveTarget;
262
+
263
+ // Record only the actual rewind target taken (not undefined when skipped)
264
+ pipeline.lastRewindTarget = rewindTarget;
265
+
266
+ // v2.4.6: Clear stale gate data so the re-entered phase evaluates fresh
267
+ if (failedPhase) {
268
+ delete pipeline.gateResults[failedPhase];
269
+ delete pipeline.gateChecks[failedPhase];
270
+ }
271
+
202
272
  failedPhase = null;
203
273
  } else {
204
274
  // Normal progression
205
- phase = gateEngine.getNextPhase(phase, gateResult);
275
+ const nextPhase = gateEngine.getNextPhase(phase, gateResult);
276
+
277
+ // v2.5.4: Reset recovery budget on forward phase change.
278
+ // Each phase gets a fresh budget — prevents a single contentious consensus
279
+ // from consuming all iterations, leaving later phases with zero.
280
+ // v2.7.0: Don't reset budget during recovery traversal.
281
+ // failedPhase stays set until the originally-failed phase passes, so
282
+ // intermediate forward transitions (IMPL→QA→REVIEW→AUDIT) are skipped.
283
+ if (nextPhase !== phase && pipeline.recoveryCount > 0 && !pipeline.failedPhase) {
284
+ onProgress?.(`Recovery budget reset: ${pipeline.recoveryCount} -> 0 (advancing ${phase} -> ${nextPhase})`);
285
+ pipeline.recoveryCount = 0;
286
+ pipeline.lastRewindTarget = undefined;
287
+ }
288
+
289
+ phase = nextPhase;
206
290
  }
207
291
  } else {
208
292
  // ─── FAIL ────────────────────────────────────────
209
293
  onProgress?.(`Gate FAILED for ${phase}: ${gateResult.blockers.join('; ')}`);
294
+
295
+ // v2.7.0: Regression detection — recovery made things worse than the original failure
296
+ if (
297
+ pipeline.failedPhase === phase &&
298
+ pipeline.recoveryBaselineFailedCheckCount !== undefined &&
299
+ (gateResult.failedChecks.length + gateResult.missingArtifacts.length) > pipeline.recoveryBaselineFailedCheckCount
300
+ ) {
301
+ onProgress?.(
302
+ `[regression] Recovery worsened ${phase}: ` +
303
+ `${pipeline.recoveryBaselineFailedCheckCount} -> ` +
304
+ `${gateResult.failedChecks.length + gateResult.missingArtifacts.length} failing checks. ` +
305
+ `Treating budget as exhausted.`
306
+ );
307
+ pipeline.recoveryCount = pipeline.maxRecoveryIterations;
308
+ }
309
+
210
310
  if (pipeline.recoveryCount >= pipeline.maxRecoveryIterations) {
211
- phase = 'STUCK';
311
+ const exhaustedPhase = phase; // capture before any reassignment
312
+
313
+ // v2.6.0: One auto-recovery attempt before STUCK
314
+ const arbitratorConfigured = !!(consensusConfig?.arbitrator);
315
+ if (!pipeline.autoRecoveryResult && arbitratorConfigured) {
316
+ onProgress?.(`[auto-recovery] Budget exhausted at ${exhaustedPhase}. Consulting arbitrator...`);
317
+
318
+ try {
319
+ const { attemptAutoRecovery } = await import('./auto-recovery.js');
320
+ const result = await attemptAutoRecovery({
321
+ pipeline, projectDir, artifactManager, consensusConfig,
322
+ });
323
+
324
+ pipeline.autoRecoveryResult = result.success ? 'success' : 'invalid';
325
+ if (result.artifact) pipeline.artifacts.push(result.artifact);
326
+
327
+ if (result.success && result.guidance) {
328
+ onProgress?.(`[auto-recovery] Strategic guidance received (${result.guidance.length} chars). Resetting budget.`);
329
+ pipeline.recoveryCount = 0;
330
+ pipeline.lastRewindTarget = undefined;
331
+ // Stay on the failed phase — let the normal loop re-execute it
332
+ // with the injected strategic guidance
333
+ phase = exhaustedPhase;
334
+ pipeline.pipelinePhase = phase;
335
+ pipeline.failedPhase = exhaustedPhase;
336
+ // Clear stale gate data so re-entry evaluates fresh
337
+ delete pipeline.gateResults[exhaustedPhase];
338
+ delete pipeline.gateChecks[exhaustedPhase];
339
+ // Continue main loop (don't fall through to STUCK)
340
+ } else {
341
+ onProgress?.(`[auto-recovery] No useful guidance. Entering STUCK.`);
342
+ phase = 'STUCK';
343
+ }
344
+ } catch (err) {
345
+ pipeline.autoRecoveryResult = (err as Error)?.message?.includes('timeout') ? 'timeout' : 'error';
346
+ onProgress?.(`[auto-recovery] Failed: ${err instanceof Error ? err.message : 'unknown'}. Entering STUCK.`);
347
+ phase = 'STUCK';
348
+ }
349
+ } else {
350
+ phase = 'STUCK';
351
+ }
212
352
  } else {
353
+ // v2.7.0: Capture baseline for fresh failure or when failure origin changes
354
+ if (
355
+ pipeline.recoveryBaselineFailedCheckCount === undefined ||
356
+ pipeline.failedPhase !== phase // Different phase failing — new recovery origin
357
+ ) {
358
+ pipeline.recoveryBaselineFailedCheckCount = gateResult.failedChecks.length + gateResult.missingArtifacts.length;
359
+ }
213
360
  failedPhase = phase;
214
361
  pipeline.failedPhase = phase;
215
362
  phase = 'RECOVERY_LOOP';
@@ -217,8 +364,22 @@ export async function runPipeline(options: PipelineOptions): Promise<PipelineRes
217
364
  }
218
365
  }
219
366
 
367
+ // v2.5.0: Stagnation detection — shared helper checks rolling signature window
368
+ if (checkStagnation(pipeline, onProgress)) {
369
+ phase = 'STUCK';
370
+ }
371
+
220
372
  // Update pipeline phase in state
221
373
  pipeline.pipelinePhase = phase;
374
+
375
+ // v2.4.5b: Persist pipeline state after each phase transition (crash safety)
376
+ try {
377
+ await updateState(projectDir, { pipeline, phase: toLegacyPhase(phase) });
378
+ } catch (persistErr) {
379
+ onProgress?.(`Warning: Failed to persist pipeline state: ${
380
+ persistErr instanceof Error ? persistErr.message : String(persistErr)}`);
381
+ }
382
+
222
383
  onProgress?.(`Transitioning to: ${phase}`);
223
384
  }
224
385
 
@@ -235,6 +396,13 @@ export async function runPipeline(options: PipelineOptions): Promise<PipelineRes
235
396
  }
236
397
  }
237
398
 
399
+ // Persist final pipeline state (DONE or STUCK)
400
+ try {
401
+ await updateState(projectDir, { pipeline, phase: toLegacyPhase(phase) });
402
+ } catch {
403
+ // Best-effort for terminal persistence
404
+ }
405
+
238
406
  return {
239
407
  success: phase === 'DONE',
240
408
  finalPhase: phase,
@@ -246,9 +414,185 @@ export async function runPipeline(options: PipelineOptions): Promise<PipelineRes
246
414
  };
247
415
  }
248
416
 
249
- /** Resume pipeline from saved state */
417
+ /** Resume pipeline from saved state — with STUCK recovery (v2.4.5) */
250
418
  export async function resumePipeline(options: PipelineOptions): Promise<PipelineResult> {
251
- // Resume is the same as run — it picks up from pipeline.pipelinePhase
419
+ const pipeline: PipelineState | undefined = options.state.pipeline;
420
+
421
+ // v2.5.3: Normalize stale legacy status when pipeline is STUCK.
422
+ // completeProject() may have set status='complete' before the pipeline entered STUCK,
423
+ // leaving inconsistent state. Fix on load so the display layer never sees the lie.
424
+ if (
425
+ pipeline &&
426
+ (pipeline.pipelinePhase === 'STUCK' || pipeline.pipelinePhase === 'RECOVERY_LOOP') &&
427
+ options.state.status === 'complete'
428
+ ) {
429
+ options.state.status = 'in-progress';
430
+ try {
431
+ await updateState(options.projectDir, { status: 'in-progress' });
432
+ } catch {
433
+ // Best-effort — display layer override in interactive.ts handles it regardless
434
+ }
435
+ }
436
+
437
+ // v2.4.8: RECOVERY_LOOP with remaining attempts — auto-resume without guidance.
438
+ // Must check this BEFORE the STUCK branch because RECOVERY_LOOP with exhausted
439
+ // attempts falls through to the guidance-required path below.
440
+ if (
441
+ pipeline?.pipelinePhase === 'RECOVERY_LOOP' &&
442
+ pipeline?.failedPhase &&
443
+ pipeline.recoveryCount < (pipeline.maxRecoveryIterations ?? 5)
444
+ ) {
445
+ // Reset to the failed phase so the main loop re-enters. Do NOT reset
446
+ // recoveryCount — the pipeline retains its remaining iteration budget.
447
+ // Do NOT clear lastRewindTarget — preserve repeated-rewind detection.
448
+ pipeline.pipelinePhase = pipeline.failedPhase;
449
+ delete pipeline.gateResults[pipeline.failedPhase];
450
+ delete pipeline.gateChecks[pipeline.failedPhase];
451
+ options.onProgress?.(
452
+ `[resume] Auto-resuming from RECOVERY_LOOP: resetting to ${pipeline.failedPhase}, ` +
453
+ `recoveryCount ${pipeline.recoveryCount}/${pipeline.maxRecoveryIterations}`,
454
+ );
455
+ } else if (
456
+ // v2.4.7: STUCK (or RECOVERY_LOOP with exhausted attempts) requires user guidance.
457
+ (pipeline?.pipelinePhase === 'STUCK' ||
458
+ (pipeline?.pipelinePhase === 'RECOVERY_LOOP' &&
459
+ pipeline.recoveryCount >= (pipeline.maxRecoveryIterations ?? 5))) &&
460
+ pipeline?.failedPhase
461
+ ) {
462
+ // v2.5.2: Purge legacy CRs without drift_key (from pre-v2.5.0 infinite loop).
463
+ // These are orphaned — no drift_key means they can never be deduplicated or resolved.
464
+ // Runs unconditionally (before guidance check) so cleanup persists even without guidance.
465
+ if (pipeline.pendingChangeRequests) {
466
+ const before = pipeline.pendingChangeRequests.length;
467
+ pipeline.pendingChangeRequests = pipeline.pendingChangeRequests.filter(
468
+ (cr) => cr.drift_key != null || cr.status === 'proposed',
469
+ );
470
+ const purged = before - pipeline.pendingChangeRequests.length;
471
+ if (purged > 0) {
472
+ options.onProgress?.(`[resume] Purged ${purged} legacy CRs without drift_key`);
473
+ }
474
+ }
475
+
476
+ // Clear stale activeChangeRequestId if the referenced CR was purged
477
+ if (
478
+ pipeline.activeChangeRequestId &&
479
+ !pipeline.pendingChangeRequests?.some((cr) => cr.cr_id === pipeline.activeChangeRequestId)
480
+ ) {
481
+ options.onProgress?.(`[resume] Cleared stale activeChangeRequestId: ${pipeline.activeChangeRequestId}`);
482
+ pipeline.activeChangeRequestId = undefined;
483
+ }
484
+
485
+ const guidance = options.additionalContext?.trim() ?? '';
486
+ if (guidance.length > 0) {
487
+ // User provided guidance — allow one more retry
488
+ const prevRecovery = pipeline.recoveryCount;
489
+ pipeline.pipelinePhase = pipeline.failedPhase;
490
+ pipeline.recoveryCount = 0;
491
+ pipeline.lastRewindTarget = undefined;
492
+ pipeline.autoRecoveryResult = undefined; // v2.6.0: Fresh auto-recovery budget after user guidance
493
+
494
+ // Clear stale gate results and checks for the failed phase
495
+ delete pipeline.gateResults[pipeline.failedPhase];
496
+ delete pipeline.gateChecks[pipeline.failedPhase];
497
+
498
+ options.onProgress?.(
499
+ `[resume] Recovering from ${pipeline.pipelinePhase}: resetting to ${pipeline.failedPhase}, ` +
500
+ `recoveryCount ${prevRecovery} -> 0 (user provided guidance)`,
501
+ );
502
+ } else {
503
+ // No guidance — v2.6.0: attempt auto-recovery before giving up
504
+ const arbitratorConfigured = !!(options.consensusConfig?.arbitrator);
505
+ if (!pipeline.autoRecoveryResult && arbitratorConfigured) {
506
+ options.onProgress?.(
507
+ `[resume] No guidance provided. Attempting auto-recovery via arbitrator...`,
508
+ );
509
+
510
+ try {
511
+ const artifactManager = createArtifactManager(options.projectDir);
512
+ const { attemptAutoRecovery } = await import('./auto-recovery.js');
513
+ const result = await attemptAutoRecovery({
514
+ pipeline,
515
+ projectDir: options.projectDir,
516
+ artifactManager,
517
+ consensusConfig: options.consensusConfig,
518
+ });
519
+
520
+ pipeline.autoRecoveryResult = result.success ? 'success' : 'invalid';
521
+ if (result.artifact) pipeline.artifacts.push(result.artifact);
522
+
523
+ if (result.success && result.guidance) {
524
+ options.onProgress?.(
525
+ `[resume] Auto-recovery guidance received (${result.guidance.length} chars). Resetting budget.`,
526
+ );
527
+ const prevRecovery = pipeline.recoveryCount;
528
+ pipeline.pipelinePhase = pipeline.failedPhase;
529
+ pipeline.recoveryCount = 0;
530
+ pipeline.lastRewindTarget = undefined;
531
+ delete pipeline.gateResults[pipeline.failedPhase];
532
+ delete pipeline.gateChecks[pipeline.failedPhase];
533
+
534
+ options.onProgress?.(
535
+ `[resume] Recovering from STUCK: resetting to ${pipeline.failedPhase}, ` +
536
+ `recoveryCount ${prevRecovery} -> 0 (auto-recovery guidance)`,
537
+ );
538
+ // Fall through to runPipeline() below
539
+ } else {
540
+ options.onProgress?.(`[resume] Auto-recovery produced no useful guidance.`);
541
+ // Fall through to return STUCK below
542
+ }
543
+ } catch (err) {
544
+ pipeline.autoRecoveryResult = (err as Error)?.message?.includes('timeout') ? 'timeout' : 'error';
545
+ options.onProgress?.(
546
+ `[resume] Auto-recovery failed: ${err instanceof Error ? err.message : 'unknown'}`,
547
+ );
548
+ // Fall through to return STUCK below
549
+ }
550
+
551
+ // Persist state (whether auto-recovery succeeded or not)
552
+ try {
553
+ await updateState(options.projectDir, { pipeline });
554
+ } catch { /* best-effort */ }
555
+
556
+ // If auto-recovery succeeded, fall through to runPipeline()
557
+ if (pipeline.autoRecoveryResult === 'success') {
558
+ return runPipeline(options);
559
+ }
560
+ }
561
+
562
+ // No guidance, no auto-recovery (or auto-recovery failed) — return STUCK
563
+ try {
564
+ await updateState(options.projectDir, { pipeline });
565
+ } catch {
566
+ // Best-effort — cleanup is still in memory for next resume
567
+ }
568
+ options.onProgress?.(
569
+ `[resume] Pipeline is stuck at ${pipeline.failedPhase} after ${pipeline.recoveryCount} recovery attempts. ` +
570
+ `Provide guidance to attempt recovery.`,
571
+ );
572
+ return {
573
+ success: false,
574
+ finalPhase: 'STUCK',
575
+ artifacts: pipeline.artifacts,
576
+ recoveryIterations: pipeline.recoveryCount,
577
+ error: `Pipeline stuck at ${pipeline.failedPhase} after ${pipeline.recoveryCount} recovery iterations. Provide guidance to retry.`,
578
+ };
579
+ }
580
+ } else if (
581
+ (pipeline?.pipelinePhase === 'STUCK' || pipeline?.pipelinePhase === 'RECOVERY_LOOP') &&
582
+ !pipeline?.failedPhase
583
+ ) {
584
+ options.onProgress?.(
585
+ `[resume] Pipeline is ${pipeline.pipelinePhase} but failedPhase is missing — cannot auto-recover`,
586
+ );
587
+ return {
588
+ success: false,
589
+ finalPhase: 'STUCK',
590
+ artifacts: pipeline?.artifacts ?? [],
591
+ recoveryIterations: pipeline?.recoveryCount ?? 0,
592
+ error: 'Pipeline is stuck with no failed phase recorded. Manual intervention required.',
593
+ };
594
+ }
595
+
252
596
  return runPipeline(options);
253
597
  }
254
598
 
@@ -265,12 +609,13 @@ function mergeGateResult(
265
609
  gateResult: GateResult,
266
610
  ): void {
267
611
  const existing = pipeline.gateResults[phase];
268
- if (existing?.score !== undefined || existing?.consensusScore !== undefined) {
269
- // Preserve consensus scores from the phase handler
612
+ if (existing?.score !== undefined || existing?.consensusScore !== undefined || existing?.finalStatus !== undefined) {
613
+ // Preserve consensus scores and finalStatus from the phase handler
270
614
  pipeline.gateResults[phase] = {
271
615
  ...gateResult,
272
616
  score: existing.score ?? gateResult.score,
273
617
  consensusScore: existing.consensusScore ?? gateResult.consensusScore,
618
+ finalStatus: existing.finalStatus ?? gateResult.finalStatus, // v2.4.3
274
619
  };
275
620
  } else {
276
621
  pipeline.gateResults[phase] = gateResult;
@@ -290,7 +635,7 @@ function getNextCRRoute(
290
635
  const nextCR = pending.find((cr) => cr.status === 'proposed');
291
636
  if (!nextCR) return undefined;
292
637
 
293
- // Mark as approved (it has been routed to the consensus phase)
638
+ // Mark as routed/in-flight (kept as 'approved' for backward compatibility)
294
639
  nextCR.status = 'approved';
295
640
 
296
641
  return {
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Consensus Packet Builder — aggregates reviewer votes into a ConsensusPacket.
3
3
  * Auto-computes consensus result (score, approval) and final status.
4
- * v1.1: Added confidence-weighted scoring.
4
+ * v2.0: Option B scoring — avg(baseWeight * confidence) per reviewer.
5
+ * REJECT guard: REJECT with true blockers prevents APPROVED (not ARBITRATED).
6
+ * Normalization summary passthrough.
5
7
  */
6
8
 
7
9
  import { randomUUID } from 'node:crypto';
@@ -11,6 +13,7 @@ import type {
11
13
  ReviewerVote,
12
14
  ConsensusPacket,
13
15
  } from '../types.js';
16
+ import { isNoneVariant } from '../../shared/text-utils.js';
14
17
 
15
18
  export interface ConsensusRules {
16
19
  threshold: number;
@@ -18,6 +21,13 @@ export interface ConsensusRules {
18
21
  min_reviewers: number;
19
22
  }
20
23
 
24
+ export interface NormalizationSummary {
25
+ tagged_blockers_demoted_to_suggestions: number;
26
+ tagged_blockers_demoted_to_required: number;
27
+ untagged_from_blocking_routed_to_required: number;
28
+ forced_rejects: number;
29
+ }
30
+
21
31
  export interface BuildConsensusPacketArgs {
22
32
  planPacketRef: ArtifactRef;
23
33
  votes: ReviewerVote[];
@@ -27,6 +37,7 @@ export interface BuildConsensusPacketArgs {
27
37
  merged_patch?: string;
28
38
  artifact_ref?: ArtifactRef;
29
39
  };
40
+ normalizationMoves?: NormalizationSummary;
30
41
  }
31
42
 
32
43
  /** Vote weight mapping: APPROVE=1.0, CONDITIONAL=0.5, REJECT=0.0 */
@@ -40,43 +51,48 @@ const VOTE_WEIGHTS: Record<string, number> = {
40
51
  * Compute both simple and confidence-weighted consensus scores.
41
52
  *
42
53
  * Simple score: approve count / total votes (backward compat).
43
- * Weighted score: each vote's weight (APPROVE=1, CONDITIONAL=0.5, REJECT=0)
44
- * multiplied by voter confidence, then averaged by total confidence.
45
- * If any vote has blocking_issues, weighted_score is forced to 0.
54
+ * Option B weighted score: average of (vote_weight * confidence) per reviewer.
55
+ * v2.4.2: Returns honest rawWeighted score + has_true_blockers flag.
56
+ * Force-zero veto removed governance guard in buildConsensusPacket() is
57
+ * the single enforcement point for blocker-based rejection.
46
58
  */
47
59
  export function computeConsensusScore(
48
60
  votes: ReviewerVote[],
49
- ): { score: number; weighted_score: number } {
50
- if (votes.length === 0) return { score: 0, weighted_score: 0 };
61
+ ): { score: number; weighted_score: number; has_true_blockers: boolean } {
62
+ if (votes.length === 0) return { score: 0, weighted_score: 0, has_true_blockers: false };
51
63
 
52
64
  // Simple score (backward compat): approve ratio
53
65
  const approvedCount = votes.filter((v) => v.vote === 'APPROVE').length;
54
66
  const score = approvedCount / votes.length;
55
67
 
56
- // Weighted score: confidence-weighted vote values
57
- let totalWeight = 0;
68
+ // Option B scoring: average of (vote_weight * confidence) per reviewer
58
69
  let weightedSum = 0;
59
70
  for (const v of votes) {
60
- const w = v.confidence;
61
- weightedSum += (VOTE_WEIGHTS[v.vote] ?? 0) * w;
62
- totalWeight += w;
71
+ const baseWeight = VOTE_WEIGHTS[v.vote] ?? 0;
72
+ weightedSum += baseWeight * v.confidence;
63
73
  }
64
- const rawWeighted = totalWeight > 0 ? weightedSum / totalWeight : 0;
74
+ const rawWeighted = weightedSum / votes.length;
65
75
 
66
- // Override: any vote with blocking_issues forces weighted_score to 0
67
- const hasBlockingIssues = votes.some((v) => v.blocking_issues.length > 0);
76
+ // v2.4.2: Detect true blockers but don't force score to 0.
77
+ // Downstream code uses has_true_blockers for decisions instead.
78
+ const has_true_blockers = votes.some(
79
+ (v) => v.vote === 'REJECT' && v.blocking_issues.some((issue) => !isNoneVariant(issue)),
80
+ );
68
81
 
69
82
  return {
70
83
  score,
71
- weighted_score: hasBlockingIssues ? 0 : rawWeighted,
84
+ weighted_score: rawWeighted,
85
+ has_true_blockers,
72
86
  };
73
87
  }
74
88
 
75
89
  export function buildConsensusPacket(args: BuildConsensusPacketArgs): ConsensusPacket {
76
- const { planPacketRef, votes, rules, arbitratorResult } = args;
90
+ const { planPacketRef, votes, rules, arbitratorResult, normalizationMoves } = args;
91
+
92
+ const { score, weighted_score, has_true_blockers } = computeConsensusScore(votes);
77
93
 
78
- const { score, weighted_score } = computeConsensusScore(votes);
79
- const approved = score >= rules.threshold && votes.length >= rules.quorum;
94
+ // Use weighted_score (not simple score) for approval decision
95
+ const approved = weighted_score >= rules.threshold && votes.length >= rules.quorum;
80
96
 
81
97
  let finalStatus: 'APPROVED' | 'REJECTED' | 'ARBITRATED';
82
98
  if (arbitratorResult) {
@@ -87,6 +103,14 @@ export function buildConsensusPacket(args: BuildConsensusPacketArgs): ConsensusP
87
103
  finalStatus = 'REJECTED';
88
104
  }
89
105
 
106
+ // Governance guard: REJECT with true blockers prevents APPROVED (but not ARBITRATED)
107
+ const hasRejectWithTrueBlockers = votes.some(
108
+ (v) => v.vote === 'REJECT' && v.blocking_issues.some((i) => !isNoneVariant(i)),
109
+ );
110
+ if (hasRejectWithTrueBlockers && finalStatus === 'APPROVED') {
111
+ finalStatus = 'REJECTED';
112
+ }
113
+
90
114
  return {
91
115
  metadata: {
92
116
  packet_id: randomUUID(),
@@ -105,8 +129,10 @@ export function buildConsensusPacket(args: BuildConsensusPacketArgs): ConsensusP
105
129
  score,
106
130
  weighted_score,
107
131
  participating_reviewers: votes.length,
132
+ has_true_blockers,
108
133
  },
109
134
  arbitrator_result: arbitratorResult,
110
135
  final_status: finalStatus,
136
+ normalization_moves: normalizationMoves,
111
137
  };
112
138
  }
@@ -11,12 +11,12 @@ import { successResult, failureResult } from './phase-context.js';
11
11
  import { generateRepoSnapshot, createSnapshotArtifact } from '../repo-snapshot.js';
12
12
 
13
13
  export async function runArchitecture(context: PhaseContext): Promise<PhaseResult> {
14
- const { pipeline, artifactManager, skillLoader, projectDir } = context;
14
+ const { pipeline, artifactManager, skillLoader, skillUsageRegistry, projectDir } = context;
15
15
  const artifacts = [];
16
16
 
17
17
  try {
18
- // 1. Load architect skill
19
- const architectSkill = skillLoader.loadSkill('ARCHITECT');
18
+ // 1. Load architect skill with metadata
19
+ const { definition: architectSkill, meta: architectMeta } = skillLoader.loadSkillWithMeta('ARCHITECT');
20
20
 
21
21
  // 2. Read approved master plan
22
22
  const masterPlanArtifact = pipeline.artifacts.find((a) => a.type === 'master_plan');
@@ -50,6 +50,9 @@ export async function runArchitecture(context: PhaseContext): Promise<PhaseResul
50
50
  const result = await executePrompt(architecturePrompt);
51
51
  const architectureDoc = result.response;
52
52
 
53
+ // Record skill usage — architect skill injected into prompt
54
+ skillUsageRegistry.record('ARCHITECT', 'ARCHITECTURE', 'system_prompt', architectMeta.source, architectMeta.version);
55
+
53
56
  // 4. Store architecture artifact
54
57
  const archEntry = artifactManager.createAndStoreText(
55
58
  'architecture',