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,283 @@
1
+ /**
2
+ * Auto-recovery via arbitrator strategic guidance (v2.6.0).
3
+ *
4
+ * When the pipeline exhausts its recovery budget, this module consults
5
+ * the arbitrator for a strategic perspective before entering STUCK.
6
+ * The arbitrator sees the full failure history and identifies patterns,
7
+ * not individual failures.
8
+ */
9
+
10
+ import { readFileSync, existsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import logging from 'node:console';
13
+
14
+ import type { PipelineState, ArtifactEntry } from './types.js';
15
+ import type { ArtifactManager } from './artifact-manager.js';
16
+ import type { ConsensusConfig } from '../types/consensus.js';
17
+ import { queryProvider } from './consensus/arbitrator-query.js';
18
+ import { getModelForProvider } from './consensus/consensus-runner.js';
19
+
20
+ const logger = logging;
21
+
22
+ // ─── Types ───────────────────────────────────────────────
23
+
24
+ interface FailureContext {
25
+ failedPhase: string;
26
+ recoveryCount: number;
27
+ maxIterations: number;
28
+ blockers: string[];
29
+ checkFailures: string[];
30
+ transitionSequence: string[];
31
+ rcaSummaries: string[];
32
+ recoveryPlanSummaries: string[];
33
+ sessionGuidance: string;
34
+ }
35
+
36
+ export interface AutoRecoveryResult {
37
+ success: boolean;
38
+ guidance: string | null;
39
+ artifact: ArtifactEntry | null;
40
+ }
41
+
42
+ interface AutoRecoveryOptions {
43
+ pipeline: PipelineState;
44
+ projectDir: string;
45
+ artifactManager: ArtifactManager;
46
+ consensusConfig?: Partial<ConsensusConfig>;
47
+ }
48
+
49
+ // ─── AUTO-RECOVERY GUIDANCE marker ───────────────────────
50
+
51
+ const AUTO_RECOVERY_MARKER = '--- AUTO-RECOVERY GUIDANCE ---';
52
+
53
+ // ─── Failure Context Builder ─────────────────────────────
54
+
55
+ /**
56
+ * Extract structured failure evidence from pipeline state.
57
+ * All file reads are wrapped in try/catch with placeholder text.
58
+ */
59
+ export function buildFailureContext(
60
+ pipeline: PipelineState,
61
+ projectDir: string,
62
+ ): FailureContext {
63
+ const failedPhase = pipeline.failedPhase ?? 'unknown';
64
+ const recoveryCount = pipeline.recoveryCount;
65
+ const maxIterations = pipeline.maxRecoveryIterations;
66
+
67
+ // Gate blockers
68
+ const gateResult = pipeline.failedPhase
69
+ ? pipeline.gateResults[pipeline.failedPhase]
70
+ : undefined;
71
+ const blockers = gateResult?.blockers ?? [];
72
+
73
+ // Check failures (command + stderr excerpt)
74
+ const checkResults = pipeline.failedPhase
75
+ ? pipeline.gateChecks[pipeline.failedPhase] ?? []
76
+ : [];
77
+ const checkFailures = checkResults
78
+ .filter((c) => c.status === 'fail')
79
+ .map((c) => `${c.check_type}: cmd="${c.command}" stderr="${(c.stderr_summary ?? '').slice(0, 200)}"`)
80
+ .slice(0, 5);
81
+
82
+ // Phase transition sequence — last 10 entries from gateResults
83
+ const transitionSequence = Object.entries(pipeline.gateResults)
84
+ .map(([phase, result]) => {
85
+ const topBlocker = result.blockers?.[0] ?? 'none';
86
+ return `${phase}: ${result.pass ? 'PASS' : 'FAIL'} (${topBlocker})`;
87
+ })
88
+ .slice(-10);
89
+
90
+ // Last 3 RCA report summaries (read text artifacts from disk)
91
+ const rcaArtifacts = pipeline.artifacts
92
+ .filter((a) => a.type === 'rca_report' && a.content_type === 'markdown')
93
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
94
+ .slice(0, 3);
95
+ const rcaSummaries = rcaArtifacts.map((a) => readArtifactSummary(projectDir, a.path, 500));
96
+
97
+ // Last 2 recovery fix plan summaries
98
+ const fixPlanArtifacts = pipeline.artifacts
99
+ .filter((a) => a.type === 'recovery_fix_plan')
100
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
101
+ .slice(0, 2);
102
+ const recoveryPlanSummaries = fixPlanArtifacts.map((a) => readArtifactSummary(projectDir, a.path, 500));
103
+
104
+ const sessionGuidance = pipeline.sessionGuidance ?? '';
105
+
106
+ return {
107
+ failedPhase,
108
+ recoveryCount,
109
+ maxIterations,
110
+ blockers,
111
+ checkFailures,
112
+ transitionSequence,
113
+ rcaSummaries,
114
+ recoveryPlanSummaries,
115
+ sessionGuidance,
116
+ };
117
+ }
118
+
119
+ // ─── Prompt Builder ──────────────────────────────────────
120
+
121
+ /**
122
+ * Build the strategic recovery prompt for the arbitrator.
123
+ * Focuses on pattern analysis, not individual fixes.
124
+ */
125
+ export function buildAutoRecoveryPrompt(ctx: FailureContext): string {
126
+ const lines: string[] = [
127
+ '# Strategic Recovery Analysis',
128
+ '',
129
+ `The pipeline failed at ${ctx.failedPhase} after ${ctx.recoveryCount}/${ctx.maxIterations} recovery attempts.`,
130
+ 'All tactical fixes have been tried. You need to identify the PATTERN, not the individual failure.',
131
+ '',
132
+ '## Failure Timeline',
133
+ ...ctx.transitionSequence.map((s) => `- ${s}`),
134
+ '',
135
+ '## Current Blockers',
136
+ ...ctx.blockers.map((b) => `- ${b}`),
137
+ ...(ctx.checkFailures.length > 0
138
+ ? ['', '## Check Failures', ...ctx.checkFailures.map((f) => `- ${f}`)]
139
+ : []),
140
+ '',
141
+ '## What Was Already Tried',
142
+ ];
143
+
144
+ if (ctx.rcaSummaries.length > 0) {
145
+ lines.push('### RCA Reports');
146
+ ctx.rcaSummaries.forEach((s, i) => {
147
+ lines.push(`#### RCA ${i + 1}`, s, '');
148
+ });
149
+ }
150
+
151
+ if (ctx.recoveryPlanSummaries.length > 0) {
152
+ lines.push('### Recovery Fix Plans');
153
+ ctx.recoveryPlanSummaries.forEach((s, i) => {
154
+ lines.push(`#### Fix Plan ${i + 1}`, s, '');
155
+ });
156
+ }
157
+
158
+ if (ctx.sessionGuidance) {
159
+ lines.push('## Current Guidance', ctx.sessionGuidance.slice(0, 1500), '');
160
+ }
161
+
162
+ lines.push(
163
+ '## Your Task',
164
+ '',
165
+ `1. **Root Pattern**: What recurring pattern caused all ${ctx.recoveryCount} attempts to fail?`,
166
+ ' Pick the SINGLE most likely root pattern.',
167
+ '',
168
+ '2. **Primary Strategy**: One concrete strategic change that breaks the pattern.',
169
+ ' (e.g., "reduce scope to X", "replace Y approach with Z", "remove module W entirely")',
170
+ '',
171
+ '3. **Fallback Strategy**: One alternative if the primary doesn\'t work.',
172
+ '',
173
+ '4. **Stop Doing** (list 3 things): Based on the history, what should the pipeline',
174
+ ' STOP trying? These are approaches that have been attempted and failed.',
175
+ '',
176
+ '5. **Concrete Next Steps**: 3-5 high-level steps (not code-level fixes).',
177
+ '',
178
+ 'Be decisive. Pick one direction. Do NOT produce another tactical dump.',
179
+ );
180
+
181
+ return lines.join('\n');
182
+ }
183
+
184
+ // ─── Main Entry Point ────────────────────────────────────
185
+
186
+ /**
187
+ * Attempt auto-recovery by consulting the arbitrator for strategic guidance.
188
+ *
189
+ * @returns Result with success flag, guidance text, and stored artifact
190
+ */
191
+ export async function attemptAutoRecovery(
192
+ opts: AutoRecoveryOptions,
193
+ ): Promise<AutoRecoveryResult> {
194
+ const { pipeline, projectDir, artifactManager, consensusConfig } = opts;
195
+
196
+ // Build failure context and strategic prompt
197
+ const ctx = buildFailureContext(pipeline, projectDir);
198
+ const prompt = buildAutoRecoveryPrompt(ctx);
199
+
200
+ // Resolve arbitrator provider config
201
+ const arbitratorName = consensusConfig?.arbitrator ?? 'gemini';
202
+ const model = getModelForProvider(consensusConfig, arbitratorName);
203
+ const providerConfig = {
204
+ provider: arbitratorName,
205
+ model,
206
+ temperature: 0.3,
207
+ };
208
+
209
+ logger.log(
210
+ `[auto-recovery] Querying ${providerConfig.provider}/${providerConfig.model} for strategic guidance`,
211
+ );
212
+
213
+ // Query with 90s timeout
214
+ const raw = await queryProvider(prompt, providerConfig, 90_000);
215
+
216
+ if (!raw) {
217
+ logger.warn('[auto-recovery] Provider returned no response (timeout or error)');
218
+ return { success: false, guidance: null, artifact: null };
219
+ }
220
+
221
+ // Validate minimum response length
222
+ if (raw.trim().length < 50) {
223
+ logger.warn(`[auto-recovery] Response too short (${raw.trim().length} chars), discarding`);
224
+ return { success: false, guidance: null, artifact: null };
225
+ }
226
+
227
+ // Store as artifact
228
+ const artifact = artifactManager.createAndStoreText(
229
+ 'auto_recovery_guidance',
230
+ `# Auto-Recovery Strategic Guidance\n\n${raw}`,
231
+ 'RECOVERY_LOOP',
232
+ );
233
+
234
+ // Inject into sessionGuidance (idempotent — replaces prior auto-recovery block)
235
+ injectAutoRecoveryGuidance(pipeline, raw);
236
+
237
+ logger.log(`[auto-recovery] Guidance received and injected (${raw.length} chars)`);
238
+
239
+ return { success: true, guidance: raw, artifact };
240
+ }
241
+
242
+ // ─── Guidance Injection ──────────────────────────────────
243
+
244
+ /**
245
+ * Inject auto-recovery guidance into pipeline.sessionGuidance.
246
+ * Replaces any prior auto-recovery block while preserving base guidance.
247
+ */
248
+ export function injectAutoRecoveryGuidance(
249
+ pipeline: PipelineState,
250
+ guidance: string,
251
+ ): void {
252
+ const existing = pipeline.sessionGuidance ?? '';
253
+
254
+ // Strip prior auto-recovery block if present
255
+ const base = existing.includes(AUTO_RECOVERY_MARKER)
256
+ ? existing.slice(0, existing.indexOf(AUTO_RECOVERY_MARKER)).trim()
257
+ : existing;
258
+
259
+ pipeline.sessionGuidance = [
260
+ base,
261
+ '',
262
+ AUTO_RECOVERY_MARKER,
263
+ guidance.slice(0, 3000),
264
+ ].join('\n').trim();
265
+ }
266
+
267
+ // ─── Helpers ─────────────────────────────────────────────
268
+
269
+ /**
270
+ * Read artifact content from disk, returning first N chars or placeholder.
271
+ */
272
+ function readArtifactSummary(projectDir: string, artifactPath: string, maxChars: number): string {
273
+ try {
274
+ const fullPath = join(projectDir, artifactPath);
275
+ if (!existsSync(fullPath)) {
276
+ return `[Could not read: ${artifactPath} — file not found]`;
277
+ }
278
+ const content = readFileSync(fullPath, 'utf-8');
279
+ return content.slice(0, maxChars);
280
+ } catch {
281
+ return `[Could not read: ${artifactPath}]`;
282
+ }
283
+ }
@@ -4,7 +4,7 @@
4
4
  * Each CR routes to the appropriate consensus phase for approval.
5
5
  */
6
6
 
7
- import { randomUUID } from 'node:crypto';
7
+ import { randomUUID, createHash } from 'node:crypto';
8
8
 
9
9
  import type {
10
10
  PipelinePhase,
@@ -24,6 +24,8 @@ export interface BuildChangeRequestArgs {
24
24
  affectedArtifacts: ArtifactRef[];
25
25
  affectedPhases: PipelinePhase[];
26
26
  riskLevel: 'low' | 'medium' | 'high';
27
+ /** Deterministic drift fingerprint for CR deduplication (v2.4.9) */
28
+ driftKey?: string;
27
29
  }
28
30
 
29
31
  /**
@@ -50,6 +52,7 @@ export function buildChangeRequest(args: BuildChangeRequestArgs): ChangeRequest
50
52
  risk_level: args.riskLevel,
51
53
  },
52
54
  status: 'proposed',
55
+ drift_key: args.driftKey,
53
56
  };
54
57
  }
55
58
 
@@ -117,3 +120,62 @@ export function formatChangeRequest(cr: ChangeRequest): string {
117
120
 
118
121
  return lines.join('\n');
119
122
  }
123
+
124
+ // ─── Drift Key Dedup (v2.4.9) ────────────────────────────
125
+
126
+ /**
127
+ * Compute a deterministic drift key for CR deduplication.
128
+ * Same drift (same change type, baseline, changed configs, content hashes)
129
+ * always produces the same key, regardless of input order.
130
+ *
131
+ * Args:
132
+ * changeType: The CR change type (config, scope, etc.).
133
+ * baselineSnapshotId: The artifact_id of the baseline snapshot.
134
+ * changedConfigs: List of changed config file paths.
135
+ * configHashPairs: Array of "path:beforeHash->afterHash" strings.
136
+ *
137
+ * Returns:
138
+ * A 32-char hex string (SHA-256 prefix).
139
+ */
140
+ export function computeDriftKey(
141
+ changeType: string,
142
+ baselineSnapshotId: string,
143
+ changedConfigs: string[],
144
+ configHashPairs: string[],
145
+ ): string {
146
+ const sortedConfigs = [...changedConfigs].sort().join(',');
147
+ const sortedPairs = [...configHashPairs].sort().join(',');
148
+ const input = `${changeType}|${baselineSnapshotId}|${sortedConfigs}|${sortedPairs}`;
149
+ return createHash('sha256').update(input).digest('hex').slice(0, 32);
150
+ }
151
+
152
+ /** Pending CR shape from PipelineState */
153
+ interface PendingCR {
154
+ cr_id: string;
155
+ change_type: string;
156
+ target_phase: string;
157
+ status: string;
158
+ drift_key?: string;
159
+ }
160
+
161
+ /**
162
+ * Check whether a pending CR with the same drift_key already exists.
163
+ * Returns true if any non-rejected CR has the same drift_key (proposed,
164
+ * approved, or resolved CRs all count as "already tracked").
165
+ *
166
+ * Args:
167
+ * pendingCRs: The current pending change requests array (may be undefined).
168
+ * driftKey: The drift key to check.
169
+ *
170
+ * Returns:
171
+ * true if a non-rejected duplicate exists.
172
+ */
173
+ export function isDuplicateCR(
174
+ pendingCRs: PendingCR[] | undefined,
175
+ driftKey: string,
176
+ ): boolean {
177
+ if (!pendingCRs) return false;
178
+ return pendingCRs.some(
179
+ (cr) => cr.drift_key === driftKey && cr.status !== 'rejected',
180
+ );
181
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { exec } from 'node:child_process';
10
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
10
+ import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
11
11
  import { join, extname } from 'node:path';
12
12
 
13
13
  import type {
@@ -17,13 +17,16 @@ import type {
17
17
  RepoSnapshot,
18
18
  ArtifactEntry,
19
19
  PipelinePhase,
20
+ PipelineState,
20
21
  } from './types.js';
21
22
  import { ArtifactManager } from './artifact-manager.js';
23
+ import { assertSkillCoverage } from './skills/coverage-gate.js';
22
24
 
23
25
  // ─── Constants ───────────────────────────────────────────
24
26
 
25
27
  /** Default timeout per check type in milliseconds */
26
28
  const DEFAULT_TIMEOUTS: Record<string, number> = {
29
+ install: 15 * 60 * 1000, // 15 minutes
27
30
  build: 20 * 60 * 1000, // 20 minutes
28
31
  test: 10 * 60 * 1000, // 10 minutes
29
32
  lint: 5 * 60 * 1000, // 5 minutes
@@ -58,6 +61,7 @@ const PLACEHOLDER_PATTERNS = [
58
61
  /\btemp\b(?!late)/i, // 'temp' but not 'template'
59
62
  /lorem ipsum/i,
60
63
  /example\.com/i,
64
+ /coming soon/i,
61
65
  ];
62
66
 
63
67
  // ─── Command Sanitization ────────────────────────────────
@@ -198,13 +202,14 @@ export function storeCheckResults(
198
202
 
199
203
  function mapCheckTypeToArtifactType(
200
204
  checkType: GateCheckType,
201
- ): 'build_check' | 'test_check' | 'lint_check' | 'typecheck_check' | 'placeholder_scan' {
205
+ ): 'build_check' | 'test_check' | 'lint_check' | 'typecheck_check' | 'placeholder_scan' | 'install_check' {
202
206
  switch (checkType) {
203
207
  case 'build': return 'build_check';
204
208
  case 'test': return 'test_check';
205
209
  case 'lint': return 'lint_check';
206
210
  case 'typecheck': return 'typecheck_check';
207
211
  case 'placeholder_scan': return 'placeholder_scan';
212
+ case 'install': return 'install_check';
208
213
  default: return 'build_check';
209
214
  }
210
215
  }
@@ -391,6 +396,41 @@ export async function runStartCheck(
391
396
  });
392
397
  }
393
398
 
399
+ // ─── Skill Coverage Check (v2.2.1) ──────────────────────
400
+
401
+ /**
402
+ * Run deterministic skill coverage check against pipeline state.
403
+ * No subprocess needed — pure in-memory assertion.
404
+ *
405
+ * Args:
406
+ * pipeline: Current pipeline state with activeRoles and skillUsageEvents.
407
+ * currentPhase: Current pipeline phase for phase-aware deferral (v2.4.5).
408
+ * Omit for strict mode (checks all roles).
409
+ *
410
+ * Returns:
411
+ * GateCheckResult with pass/fail status.
412
+ */
413
+ export function runSkillCoverageCheck(
414
+ pipeline: PipelineState,
415
+ currentPhase?: PipelinePhase,
416
+ ): GateCheckResult {
417
+ const events = pipeline.skillUsageEvents ?? [];
418
+ const result = assertSkillCoverage(pipeline.activeRoles, events, pipeline, currentPhase);
419
+
420
+ return {
421
+ check_type: 'skill_coverage',
422
+ status: result.pass ? 'pass' : 'fail',
423
+ command: 'skill-coverage-check',
424
+ exit_code: result.pass ? 0 : 1,
425
+ stderr_summary: result.pass
426
+ ? `All ${result.covered.length} active roles have skill usage` +
427
+ (result.deferred.length > 0 ? ` (${result.deferred.length} deferred)` : '')
428
+ : `Missing skill usage: ${result.missing.map((m) => `${m.role} (${m.reason})`).join(', ')}`,
429
+ duration_ms: 0,
430
+ timestamp: new Date().toISOString(),
431
+ };
432
+ }
433
+
394
434
  // ─── Env Check (v1.1 Gap #5) ────────────────────────────
395
435
 
396
436
  /**
@@ -502,3 +542,102 @@ function parseEnvVarValues(content: string): Record<string, string> {
502
542
  }
503
543
  return vars;
504
544
  }
545
+
546
+ // ─── Install Skip Heuristic ─────────────────────────────
547
+
548
+ /** Marker file for install skip heuristic */
549
+ interface InstallMarker {
550
+ lockfileHash: string;
551
+ timestamp: string;
552
+ }
553
+
554
+ /** Canonical lockfile names by ecosystem */
555
+ const LOCKFILE_MAP: Record<string, string> = {
556
+ npm: 'package-lock.json',
557
+ pnpm: 'pnpm-lock.yaml',
558
+ yarn: 'yarn.lock',
559
+ bun: 'bun.lockb',
560
+ poetry: 'poetry.lock',
561
+ pip: 'requirements.txt',
562
+ };
563
+
564
+ /**
565
+ * Determine whether install can be skipped based on lockfile hash.
566
+ * Returns true only if the marker matches the current lockfile hash
567
+ * AND the install target directory exists (node_modules or .venv).
568
+ */
569
+ export function shouldSkipInstall(projectDir: string, snapshot: RepoSnapshot): boolean {
570
+ const markerPath = join(projectDir, '.popeye', 'install-marker.json');
571
+ if (!existsSync(markerPath)) return false;
572
+
573
+ try {
574
+ const marker: InstallMarker = JSON.parse(readFileSync(markerPath, 'utf-8'));
575
+ const currentHash = getCanonicalLockfileHash(snapshot);
576
+ if (!currentHash || marker.lockfileHash !== currentHash) return false;
577
+
578
+ // Verify install target directory exists
579
+ const pm = snapshot.package_manager ?? 'npm';
580
+ const isPython = pm === 'pip' || pm === 'poetry';
581
+ const targetDir = isPython
582
+ ? join(projectDir, '.venv')
583
+ : join(projectDir, 'node_modules');
584
+ return existsSync(targetDir);
585
+ } catch {
586
+ return false;
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Write install marker after successful install.
592
+ * Stores the current lockfile hash for future skip checks.
593
+ */
594
+ export function writeInstallMarker(projectDir: string, snapshot: RepoSnapshot): void {
595
+ const markerDir = join(projectDir, '.popeye');
596
+ const markerPath = join(markerDir, 'install-marker.json');
597
+
598
+ const lockfileHash = getCanonicalLockfileHash(snapshot);
599
+ if (!lockfileHash) return;
600
+
601
+ try {
602
+ if (!existsSync(markerDir)) {
603
+ mkdirSync(markerDir, { recursive: true });
604
+ }
605
+ const marker: InstallMarker = {
606
+ lockfileHash,
607
+ timestamp: new Date().toISOString(),
608
+ };
609
+ writeFileSync(markerPath, JSON.stringify(marker, null, 2));
610
+ } catch {
611
+ // Non-fatal — worst case we re-run install next time
612
+ }
613
+ }
614
+
615
+ /**
616
+ * Invalidate install marker so the next check re-runs install.
617
+ * Called on missing-module errors or dependency changes.
618
+ */
619
+ export function invalidateInstallMarker(projectDir: string): void {
620
+ const markerPath = join(projectDir, '.popeye', 'install-marker.json');
621
+ try {
622
+ if (existsSync(markerPath)) unlinkSync(markerPath);
623
+ } catch {
624
+ // Non-fatal
625
+ }
626
+ }
627
+
628
+ /** Get content_hash of the canonical lockfile from the snapshot */
629
+ function getCanonicalLockfileHash(snapshot: RepoSnapshot): string | undefined {
630
+ const pm = snapshot.package_manager ?? 'npm';
631
+ const lockfileName = LOCKFILE_MAP[pm];
632
+
633
+ if (lockfileName) {
634
+ const entry = snapshot.config_files.find((c) => c.type === lockfileName);
635
+ if (entry) return entry.content_hash;
636
+ }
637
+
638
+ // Fallback: use package.json hash if no lockfile found
639
+ const pkgEntry = snapshot.config_files.find((c) => c.type === 'package.json');
640
+ if (pkgEntry) return pkgEntry.content_hash;
641
+
642
+ return undefined;
643
+ }
@@ -40,14 +40,21 @@ export function resolveCommands(
40
40
  case 'python':
41
41
  resolved = resolvePythonCommands(snapshot);
42
42
  break;
43
- case 'mixed':
43
+ case 'mixed': {
44
44
  // Prefer Node commands, augment with Python where Node is missing
45
45
  resolved = resolveNodeCommands(pm, scripts, snapshot);
46
+ const pyResolved = resolvePythonCommands(snapshot);
46
47
  if (!resolved.test) {
47
- const pyResolved = resolvePythonCommands(snapshot);
48
48
  resolved.test = pyResolved.test;
49
49
  }
50
+ // Chain both install commands for mixed projects
51
+ if (resolved.install && pyResolved.install) {
52
+ resolved.install = `${resolved.install} && ${pyResolved.install}`;
53
+ } else if (!resolved.install) {
54
+ resolved.install = pyResolved.install;
55
+ }
50
56
  break;
57
+ }
51
58
  default:
52
59
  resolved = { resolved_from: 'none' };
53
60
  }
@@ -60,6 +67,8 @@ export function resolveCommands(
60
67
  if (overrides.typecheck) resolved.typecheck = overrides.typecheck;
61
68
  if (overrides.migrations) resolved.migrations = overrides.migrations;
62
69
  if (overrides.start) resolved.start = overrides.start;
70
+ if (overrides.install) resolved.install = overrides.install;
71
+ if (overrides.install_cwd) resolved.install_cwd = overrides.install_cwd;
63
72
  }
64
73
 
65
74
  return resolved;
@@ -120,6 +129,19 @@ function resolveNodeCommands(
120
129
  resolved.start = `${run} dev`;
121
130
  }
122
131
 
132
+ // Install — always resolve based on package manager
133
+ resolved.install = pm === 'yarn' ? 'yarn install' : `${pm} install`;
134
+
135
+ // Workspace detection: install must run at the workspace root
136
+ const rootPkg = snapshot.config_files.find((c) => c.type === 'package.json');
137
+ const hasWorkspaces = rootPkg?.key_fields?.workspaces !== undefined;
138
+ const hasPnpmWorkspace = snapshot.config_files.some(
139
+ (c) => c.type === 'pnpm-workspace.yaml',
140
+ );
141
+ if (hasWorkspaces || hasPnpmWorkspace) {
142
+ resolved.install_cwd = '.';
143
+ }
144
+
123
145
  return resolved;
124
146
  }
125
147
 
@@ -164,5 +186,15 @@ function resolvePythonCommands(snapshot: RepoSnapshot): ResolvedCommands {
164
186
  // Start
165
187
  resolved.start = 'uvicorn main:app --host 0.0.0.0 --port 8000';
166
188
 
189
+ // Install — conservative: only well-known safe patterns
190
+ const hasPoetryLock = snapshot.config_files.some((c) => c.type === 'poetry.lock');
191
+ const hasReqs = snapshot.config_files.some((c) => c.type === 'requirements.txt');
192
+ if (hasPoetryLock) {
193
+ resolved.install = 'poetry install';
194
+ } else if (hasReqs) {
195
+ resolved.install = 'pip install -r requirements.txt';
196
+ }
197
+ // No install for pyproject.toml-only (may need build backends, system deps)
198
+
167
199
  return resolved;
168
200
  }