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
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Tests for auto-recovery.ts — strategic guidance from arbitrator (v2.6.0).
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+
7
+ // Mock the adapter/query layer
8
+ vi.mock('../../src/pipeline/consensus/arbitrator-query.js', () => ({
9
+ queryProvider: vi.fn(),
10
+ }));
11
+ vi.mock('../../src/pipeline/consensus/consensus-runner.js', () => ({
12
+ getModelForProvider: vi.fn(() => 'gemini-2.5-flash'),
13
+ }));
14
+
15
+ import {
16
+ buildFailureContext,
17
+ buildAutoRecoveryPrompt,
18
+ attemptAutoRecovery,
19
+ injectAutoRecoveryGuidance,
20
+ } from '../../src/pipeline/auto-recovery.js';
21
+ import { queryProvider } from '../../src/pipeline/consensus/arbitrator-query.js';
22
+ import { createDefaultPipelineState } from '../../src/pipeline/types.js';
23
+ import type { PipelineState, ArtifactEntry } from '../../src/pipeline/types.js';
24
+
25
+ // ─── Mock ArtifactManager ────────────────────────────────
26
+
27
+ function createMockArtifactManager() {
28
+ return {
29
+ createAndStoreText: vi.fn(
30
+ (type: string, _content: string, phase: string): ArtifactEntry => ({
31
+ id: `test-${type}-${Date.now()}`,
32
+ type: type as ArtifactEntry['type'],
33
+ phase: phase as ArtifactEntry['phase'],
34
+ version: 1,
35
+ path: `docs/${type}.md`,
36
+ sha256: 'test-sha',
37
+ timestamp: new Date().toISOString(),
38
+ immutable: true,
39
+ content_type: 'markdown',
40
+ group_id: `group-${type}`,
41
+ }),
42
+ ),
43
+ createAndStoreJson: vi.fn(),
44
+ ensureDocsStructure: vi.fn(),
45
+ updateIndex: vi.fn(),
46
+ };
47
+ }
48
+
49
+ // ─── Helpers ─────────────────────────────────────────────
50
+
51
+ function makePipelineWithFailures(): PipelineState {
52
+ const pipeline = createDefaultPipelineState();
53
+ pipeline.failedPhase = 'QA_VALIDATION';
54
+ pipeline.recoveryCount = 5;
55
+ pipeline.maxRecoveryIterations = 5;
56
+ pipeline.gateResults['QA_VALIDATION'] = {
57
+ phase: 'QA_VALIDATION',
58
+ pass: false,
59
+ blockers: ['test suite failing', 'build errors in auth module'],
60
+ missingArtifacts: [],
61
+ failedChecks: [],
62
+ timestamp: new Date().toISOString(),
63
+ };
64
+ pipeline.gateChecks['QA_VALIDATION'] = [
65
+ {
66
+ check_type: 'test',
67
+ status: 'fail',
68
+ command: 'npm test',
69
+ exit_code: 1,
70
+ duration_ms: 5000,
71
+ timestamp: '',
72
+ stderr_summary: 'FAIL src/auth.test.ts: Cannot find module ./auth-utils',
73
+ },
74
+ ];
75
+ pipeline.gateResults['IMPLEMENTATION'] = {
76
+ phase: 'IMPLEMENTATION',
77
+ pass: true,
78
+ blockers: [],
79
+ missingArtifacts: [],
80
+ failedChecks: [],
81
+ timestamp: new Date().toISOString(),
82
+ };
83
+ pipeline.sessionGuidance = 'Fix the auth module imports';
84
+ return pipeline;
85
+ }
86
+
87
+ describe('buildFailureContext', () => {
88
+ it('should extract blockers and check failures', () => {
89
+ const pipeline = makePipelineWithFailures();
90
+ const ctx = buildFailureContext(pipeline, '/tmp/test-project');
91
+
92
+ expect(ctx.failedPhase).toBe('QA_VALIDATION');
93
+ expect(ctx.recoveryCount).toBe(5);
94
+ expect(ctx.maxIterations).toBe(5);
95
+ expect(ctx.blockers).toContain('test suite failing');
96
+ expect(ctx.checkFailures.length).toBe(1);
97
+ expect(ctx.checkFailures[0]).toContain('npm test');
98
+ });
99
+
100
+ it('should extract transition sequence from gateResults', () => {
101
+ const pipeline = makePipelineWithFailures();
102
+ const ctx = buildFailureContext(pipeline, '/tmp/test-project');
103
+
104
+ expect(ctx.transitionSequence.length).toBeGreaterThan(0);
105
+ expect(ctx.transitionSequence.some(s => s.includes('QA_VALIDATION'))).toBe(true);
106
+ expect(ctx.transitionSequence.some(s => s.includes('FAIL'))).toBe(true);
107
+ });
108
+
109
+ it('should handle missing artifact files gracefully', () => {
110
+ const pipeline = makePipelineWithFailures();
111
+ pipeline.artifacts.push({
112
+ id: 'rca-1',
113
+ type: 'rca_report',
114
+ phase: 'RECOVERY_LOOP',
115
+ version: 1,
116
+ path: 'docs/nonexistent-rca.md',
117
+ sha256: 'abc',
118
+ timestamp: new Date().toISOString(),
119
+ immutable: true,
120
+ content_type: 'markdown',
121
+ group_id: 'g1',
122
+ });
123
+
124
+ const ctx = buildFailureContext(pipeline, '/tmp/test-project');
125
+
126
+ expect(ctx.rcaSummaries.length).toBe(1);
127
+ expect(ctx.rcaSummaries[0]).toContain('Could not read');
128
+ });
129
+
130
+ it('should return empty arrays when no failures exist', () => {
131
+ const pipeline = createDefaultPipelineState();
132
+ const ctx = buildFailureContext(pipeline, '/tmp/test-project');
133
+
134
+ expect(ctx.failedPhase).toBe('unknown');
135
+ expect(ctx.blockers).toEqual([]);
136
+ expect(ctx.checkFailures).toEqual([]);
137
+ expect(ctx.rcaSummaries).toEqual([]);
138
+ });
139
+ });
140
+
141
+ describe('buildAutoRecoveryPrompt', () => {
142
+ it('should include all required sections', () => {
143
+ const pipeline = makePipelineWithFailures();
144
+ const ctx = buildFailureContext(pipeline, '/tmp/test-project');
145
+ const prompt = buildAutoRecoveryPrompt(ctx);
146
+
147
+ expect(prompt).toContain('Strategic Recovery Analysis');
148
+ expect(prompt).toContain('Failure Timeline');
149
+ expect(prompt).toContain('Current Blockers');
150
+ expect(prompt).toContain('Root Pattern');
151
+ expect(prompt).toContain('Primary Strategy');
152
+ expect(prompt).toContain('Stop Doing');
153
+ expect(prompt).toContain('Concrete Next Steps');
154
+ });
155
+
156
+ it('should include failure details', () => {
157
+ const pipeline = makePipelineWithFailures();
158
+ const ctx = buildFailureContext(pipeline, '/tmp/test-project');
159
+ const prompt = buildAutoRecoveryPrompt(ctx);
160
+
161
+ expect(prompt).toContain('QA_VALIDATION');
162
+ expect(prompt).toContain('5/5');
163
+ expect(prompt).toContain('test suite failing');
164
+ });
165
+
166
+ it('should include check failures section when present', () => {
167
+ const pipeline = makePipelineWithFailures();
168
+ const ctx = buildFailureContext(pipeline, '/tmp/test-project');
169
+ const prompt = buildAutoRecoveryPrompt(ctx);
170
+
171
+ expect(prompt).toContain('Check Failures');
172
+ expect(prompt).toContain('npm test');
173
+ });
174
+
175
+ it('should instruct against tactical dumps', () => {
176
+ const pipeline = makePipelineWithFailures();
177
+ const ctx = buildFailureContext(pipeline, '/tmp/test-project');
178
+ const prompt = buildAutoRecoveryPrompt(ctx);
179
+
180
+ expect(prompt).toContain('Do NOT produce another tactical dump');
181
+ });
182
+ });
183
+
184
+ describe('attemptAutoRecovery', () => {
185
+ beforeEach(() => {
186
+ vi.clearAllMocks();
187
+ });
188
+
189
+ it('should return success when queryProvider returns valid text', async () => {
190
+ const guidance = 'Root pattern: the auth module has circular imports. ' +
191
+ 'Primary strategy: flatten the import graph by extracting shared types.';
192
+ vi.mocked(queryProvider).mockResolvedValue(guidance);
193
+
194
+ const pipeline = makePipelineWithFailures();
195
+ const artifactManager = createMockArtifactManager();
196
+
197
+ const result = await attemptAutoRecovery({
198
+ pipeline,
199
+ projectDir: '/tmp/test-project',
200
+ artifactManager: artifactManager as any,
201
+ consensusConfig: { arbitrator: 'gemini' } as any,
202
+ });
203
+
204
+ expect(result.success).toBe(true);
205
+ expect(result.guidance).toBe(guidance);
206
+ expect(result.artifact).toBeDefined();
207
+ expect(result.artifact?.type).toBe('auto_recovery_guidance');
208
+ expect(artifactManager.createAndStoreText).toHaveBeenCalledOnce();
209
+ });
210
+
211
+ it('should inject guidance into pipeline.sessionGuidance', async () => {
212
+ const guidance = 'Strategic guidance content that is longer than fifty characters for validation.';
213
+ vi.mocked(queryProvider).mockResolvedValue(guidance);
214
+
215
+ const pipeline = makePipelineWithFailures();
216
+ const originalGuidance = pipeline.sessionGuidance;
217
+
218
+ await attemptAutoRecovery({
219
+ pipeline,
220
+ projectDir: '/tmp/test-project',
221
+ artifactManager: createMockArtifactManager() as any,
222
+ consensusConfig: { arbitrator: 'gemini' } as any,
223
+ });
224
+
225
+ expect(pipeline.sessionGuidance).toContain('AUTO-RECOVERY GUIDANCE');
226
+ expect(pipeline.sessionGuidance).toContain(guidance);
227
+ // Base guidance should be preserved
228
+ expect(pipeline.sessionGuidance).toContain(originalGuidance!);
229
+ });
230
+
231
+ it('should return failure when response is too short', async () => {
232
+ vi.mocked(queryProvider).mockResolvedValue('Too short');
233
+
234
+ const pipeline = makePipelineWithFailures();
235
+
236
+ const result = await attemptAutoRecovery({
237
+ pipeline,
238
+ projectDir: '/tmp/test-project',
239
+ artifactManager: createMockArtifactManager() as any,
240
+ consensusConfig: { arbitrator: 'gemini' } as any,
241
+ });
242
+
243
+ expect(result.success).toBe(false);
244
+ expect(result.guidance).toBeNull();
245
+ expect(result.artifact).toBeNull();
246
+ });
247
+
248
+ it('should return failure when queryProvider returns null (timeout)', async () => {
249
+ vi.mocked(queryProvider).mockResolvedValue(null);
250
+
251
+ const pipeline = makePipelineWithFailures();
252
+
253
+ const result = await attemptAutoRecovery({
254
+ pipeline,
255
+ projectDir: '/tmp/test-project',
256
+ artifactManager: createMockArtifactManager() as any,
257
+ consensusConfig: { arbitrator: 'gemini' } as any,
258
+ });
259
+
260
+ expect(result.success).toBe(false);
261
+ expect(result.guidance).toBeNull();
262
+ expect(result.artifact).toBeNull();
263
+ });
264
+
265
+ it('should use default arbitrator when none specified in config', async () => {
266
+ const guidance = 'Long enough guidance text that meets the fifty character minimum requirement for validation.';
267
+ vi.mocked(queryProvider).mockResolvedValue(guidance);
268
+
269
+ const pipeline = makePipelineWithFailures();
270
+
271
+ await attemptAutoRecovery({
272
+ pipeline,
273
+ projectDir: '/tmp/test-project',
274
+ artifactManager: createMockArtifactManager() as any,
275
+ consensusConfig: undefined,
276
+ });
277
+
278
+ // Should have been called (uses default 'gemini')
279
+ expect(queryProvider).toHaveBeenCalledOnce();
280
+ const callArgs = vi.mocked(queryProvider).mock.calls[0];
281
+ expect(callArgs[1].provider).toBe('gemini');
282
+ });
283
+ });
284
+
285
+ describe('injectAutoRecoveryGuidance', () => {
286
+ it('should inject guidance with marker', () => {
287
+ const pipeline = createDefaultPipelineState();
288
+ pipeline.sessionGuidance = 'Base guidance content';
289
+
290
+ injectAutoRecoveryGuidance(pipeline, 'New strategic guidance');
291
+
292
+ expect(pipeline.sessionGuidance).toContain('Base guidance content');
293
+ expect(pipeline.sessionGuidance).toContain('--- AUTO-RECOVERY GUIDANCE ---');
294
+ expect(pipeline.sessionGuidance).toContain('New strategic guidance');
295
+ });
296
+
297
+ it('should replace prior auto-recovery block (idempotent)', () => {
298
+ const pipeline = createDefaultPipelineState();
299
+ pipeline.sessionGuidance = 'Base guidance\n\n--- AUTO-RECOVERY GUIDANCE ---\nOld guidance';
300
+
301
+ injectAutoRecoveryGuidance(pipeline, 'New strategic guidance');
302
+
303
+ // Should only have ONE marker block
304
+ const markerCount = (pipeline.sessionGuidance!.match(/--- AUTO-RECOVERY GUIDANCE ---/g) || []).length;
305
+ expect(markerCount).toBe(1);
306
+ // Old guidance should be gone
307
+ expect(pipeline.sessionGuidance).not.toContain('Old guidance');
308
+ // New guidance should be present
309
+ expect(pipeline.sessionGuidance).toContain('New strategic guidance');
310
+ // Base guidance should be preserved
311
+ expect(pipeline.sessionGuidance).toContain('Base guidance');
312
+ });
313
+
314
+ it('should handle empty sessionGuidance', () => {
315
+ const pipeline = createDefaultPipelineState();
316
+ pipeline.sessionGuidance = undefined;
317
+
318
+ injectAutoRecoveryGuidance(pipeline, 'Guidance text');
319
+
320
+ expect(pipeline.sessionGuidance).toContain('--- AUTO-RECOVERY GUIDANCE ---');
321
+ expect(pipeline.sessionGuidance).toContain('Guidance text');
322
+ });
323
+
324
+ it('should cap guidance at 3000 chars', () => {
325
+ const pipeline = createDefaultPipelineState();
326
+ const longGuidance = 'x'.repeat(5000);
327
+
328
+ injectAutoRecoveryGuidance(pipeline, longGuidance);
329
+
330
+ // The guidance portion should be capped
331
+ const markerIdx = pipeline.sessionGuidance!.indexOf('--- AUTO-RECOVERY GUIDANCE ---');
332
+ const guidanceAfterMarker = pipeline.sessionGuidance!.slice(
333
+ markerIdx + '--- AUTO-RECOVERY GUIDANCE ---'.length + 1,
334
+ );
335
+ expect(guidanceAfterMarker.length).toBeLessThanOrEqual(3000);
336
+ });
337
+ });
@@ -7,6 +7,8 @@ import {
7
7
  buildChangeRequest,
8
8
  routeChangeRequest,
9
9
  formatChangeRequest,
10
+ computeDriftKey,
11
+ isDuplicateCR,
10
12
  } from '../../src/pipeline/change-request.js';
11
13
  import type { ArtifactRef } from '../../src/pipeline/types.js';
12
14
 
@@ -178,3 +180,71 @@ describe('formatChangeRequest', () => {
178
180
  expect(md).toContain('Config files changed');
179
181
  });
180
182
  });
183
+
184
+ // ─── v2.4.9: Drift Key Dedup ─────────────────────────────
185
+
186
+ describe('computeDriftKey', () => {
187
+ it('should produce the same key regardless of input order', () => {
188
+ const key1 = computeDriftKey('config', 'snap-1', ['b.json', 'a.json'], ['b:x->y', 'a:p->q']);
189
+ const key2 = computeDriftKey('config', 'snap-1', ['a.json', 'b.json'], ['a:p->q', 'b:x->y']);
190
+ expect(key1).toBe(key2);
191
+ });
192
+
193
+ it('should produce different keys for different baselines', () => {
194
+ const key1 = computeDriftKey('config', 'snap-1', ['a.json'], ['a:x->y']);
195
+ const key2 = computeDriftKey('config', 'snap-2', ['a.json'], ['a:x->y']);
196
+ expect(key1).not.toBe(key2);
197
+ });
198
+
199
+ it('should produce different keys for different change types', () => {
200
+ const key1 = computeDriftKey('config', 'snap-1', ['a.json'], []);
201
+ const key2 = computeDriftKey('scope', 'snap-1', ['a.json'], []);
202
+ expect(key1).not.toBe(key2);
203
+ });
204
+
205
+ it('should return a 32-char hex string', () => {
206
+ const key = computeDriftKey('config', 'snap-1', ['a.json'], ['a:x->y']);
207
+ expect(key).toMatch(/^[0-9a-f]{32}$/);
208
+ });
209
+ });
210
+
211
+ describe('isDuplicateCR', () => {
212
+ it('should return false for undefined pendingCRs', () => {
213
+ expect(isDuplicateCR(undefined, 'dk-1')).toBe(false);
214
+ });
215
+
216
+ it('should return false when no matching drift_key', () => {
217
+ const pending = [
218
+ { cr_id: 'CR-1', change_type: 'config', target_phase: 'QA_VALIDATION', status: 'proposed', drift_key: 'dk-other' },
219
+ ];
220
+ expect(isDuplicateCR(pending, 'dk-1')).toBe(false);
221
+ });
222
+
223
+ it('should return true for proposed CR with same drift_key', () => {
224
+ const pending = [
225
+ { cr_id: 'CR-1', change_type: 'config', target_phase: 'QA_VALIDATION', status: 'proposed', drift_key: 'dk-1' },
226
+ ];
227
+ expect(isDuplicateCR(pending, 'dk-1')).toBe(true);
228
+ });
229
+
230
+ it('should return true for approved CR with same drift_key', () => {
231
+ const pending = [
232
+ { cr_id: 'CR-1', change_type: 'config', target_phase: 'QA_VALIDATION', status: 'approved', drift_key: 'dk-1' },
233
+ ];
234
+ expect(isDuplicateCR(pending, 'dk-1')).toBe(true);
235
+ });
236
+
237
+ it('should return true for resolved CR with same drift_key', () => {
238
+ const pending = [
239
+ { cr_id: 'CR-1', change_type: 'config', target_phase: 'QA_VALIDATION', status: 'resolved', drift_key: 'dk-1' },
240
+ ];
241
+ expect(isDuplicateCR(pending, 'dk-1')).toBe(true);
242
+ });
243
+
244
+ it('should return false when only rejected CR has same drift_key', () => {
245
+ const pending = [
246
+ { cr_id: 'CR-1', change_type: 'config', target_phase: 'QA_VALIDATION', status: 'rejected', drift_key: 'dk-1' },
247
+ ];
248
+ expect(isDuplicateCR(pending, 'dk-1')).toBe(false);
249
+ });
250
+ });
@@ -155,5 +155,47 @@ describe('CommandResolver', () => {
155
155
  expect(cmds.build).toBeUndefined();
156
156
  expect(cmds.test).toBeUndefined();
157
157
  });
158
+
159
+ it('should resolve install command for npm project', () => {
160
+ const snap = makeSnapshot({
161
+ config_files: [makeConfig('package.json')],
162
+ package_manager: 'npm',
163
+ scripts: {},
164
+ });
165
+
166
+ const cmds = resolveCommands(snap);
167
+ expect(cmds.install).toBe('npm install');
168
+ });
169
+
170
+ it('should resolve install command for yarn project', () => {
171
+ const snap = makeSnapshot({
172
+ config_files: [makeConfig('package.json')],
173
+ package_manager: 'yarn',
174
+ scripts: {},
175
+ });
176
+
177
+ const cmds = resolveCommands(snap);
178
+ expect(cmds.install).toBe('yarn install');
179
+ });
180
+
181
+ it('should resolve install command for python with requirements.txt', () => {
182
+ const snap = makeSnapshot({
183
+ config_files: [makeConfig('requirements.txt')],
184
+ languages_detected: ['python'],
185
+ });
186
+
187
+ const cmds = resolveCommands(snap);
188
+ expect(cmds.install).toBe('pip install -r requirements.txt');
189
+ });
190
+
191
+ it('should not resolve install for pyproject.toml-only python', () => {
192
+ const snap = makeSnapshot({
193
+ config_files: [makeConfig('pyproject.toml')],
194
+ languages_detected: ['python'],
195
+ });
196
+
197
+ const cmds = resolveCommands(snap);
198
+ expect(cmds.install).toBeUndefined();
199
+ });
158
200
  });
159
201
  });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Tests for arbitrator-query.ts — generic provider query with timeout.
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+
7
+ // Mock all adapter modules before importing the module under test
8
+ vi.mock('../../../src/adapters/openai.js', () => ({
9
+ requestRawReview: vi.fn(),
10
+ }));
11
+ vi.mock('../../../src/adapters/gemini.js', () => ({
12
+ requestRawReview: vi.fn(),
13
+ }));
14
+ vi.mock('../../../src/adapters/grok.js', () => ({
15
+ requestRawReview: vi.fn(),
16
+ }));
17
+
18
+ import { queryProvider } from '../../../src/pipeline/consensus/arbitrator-query.js';
19
+ import type { ProviderConfig } from '../../../src/pipeline/consensus/arbitrator-query.js';
20
+
21
+ describe('queryProvider', () => {
22
+ const openaiConfig: ProviderConfig = { provider: 'openai', model: 'gpt-4.1', temperature: 0.3 };
23
+ const geminiConfig: ProviderConfig = { provider: 'gemini', model: 'gemini-2.5-flash', temperature: 0.3 };
24
+ const grokConfig: ProviderConfig = { provider: 'grok', model: 'grok-3', temperature: 0.3 };
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ });
29
+
30
+ it('should return raw text from openai adapter', async () => {
31
+ const { requestRawReview } = await import('../../../src/adapters/openai.js');
32
+ vi.mocked(requestRawReview).mockResolvedValue('Strategic guidance response');
33
+
34
+ const result = await queryProvider('test prompt', openaiConfig);
35
+
36
+ expect(result).toBe('Strategic guidance response');
37
+ expect(requestRawReview).toHaveBeenCalledOnce();
38
+ });
39
+
40
+ it('should return raw text from gemini adapter', async () => {
41
+ const { requestRawReview } = await import('../../../src/adapters/gemini.js');
42
+ vi.mocked(requestRawReview).mockResolvedValue('Gemini response');
43
+
44
+ const result = await queryProvider('test prompt', geminiConfig);
45
+
46
+ expect(result).toBe('Gemini response');
47
+ expect(requestRawReview).toHaveBeenCalledOnce();
48
+ });
49
+
50
+ it('should return raw text from grok adapter', async () => {
51
+ const { requestRawReview } = await import('../../../src/adapters/grok.js');
52
+ vi.mocked(requestRawReview).mockResolvedValue('Grok response');
53
+
54
+ const result = await queryProvider('test prompt', grokConfig);
55
+
56
+ expect(result).toBe('Grok response');
57
+ expect(requestRawReview).toHaveBeenCalledOnce();
58
+ });
59
+
60
+ it('should return null on timeout', async () => {
61
+ const { requestRawReview } = await import('../../../src/adapters/openai.js');
62
+ // Simulate slow promise that never resolves within timeout
63
+ vi.mocked(requestRawReview).mockImplementation(
64
+ () => new Promise((resolve) => setTimeout(() => resolve('too late'), 5000)),
65
+ );
66
+
67
+ const result = await queryProvider('test prompt', openaiConfig, 50);
68
+
69
+ expect(result).toBeNull();
70
+ });
71
+
72
+ it('should return null on error', async () => {
73
+ const { requestRawReview } = await import('../../../src/adapters/openai.js');
74
+ vi.mocked(requestRawReview).mockRejectedValue(new Error('API key invalid'));
75
+
76
+ const result = await queryProvider('test prompt', openaiConfig);
77
+
78
+ expect(result).toBeNull();
79
+ });
80
+
81
+ it('should return null on empty response', async () => {
82
+ const { requestRawReview } = await import('../../../src/adapters/openai.js');
83
+ vi.mocked(requestRawReview).mockResolvedValue('');
84
+
85
+ const result = await queryProvider('test prompt', openaiConfig);
86
+
87
+ expect(result).toBeNull();
88
+ });
89
+
90
+ it('should return null on whitespace-only response', async () => {
91
+ const { requestRawReview } = await import('../../../src/adapters/openai.js');
92
+ vi.mocked(requestRawReview).mockResolvedValue(' \n ');
93
+
94
+ const result = await queryProvider('test prompt', openaiConfig);
95
+
96
+ expect(result).toBeNull();
97
+ });
98
+
99
+ it('should throw for unknown provider', async () => {
100
+ const unknownConfig: ProviderConfig = { provider: 'anthropic', model: 'claude', temperature: 0.3 };
101
+
102
+ // queryProvider catches errors and returns null
103
+ const result = await queryProvider('test prompt', unknownConfig);
104
+
105
+ expect(result).toBeNull();
106
+ });
107
+ });