devflow-engine 1.0.0__py3-none-any.whl

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 (393) hide show
  1. devflow_engine/__init__.py +3 -0
  2. devflow_engine/agentic_prompts.py +100 -0
  3. devflow_engine/agentic_runtime.py +398 -0
  4. devflow_engine/api_key_flow_harness.py +539 -0
  5. devflow_engine/api_keys.py +357 -0
  6. devflow_engine/bootstrap/__init__.py +2 -0
  7. devflow_engine/bootstrap/provision_from_template.py +84 -0
  8. devflow_engine/cli/__init__.py +0 -0
  9. devflow_engine/cli/app.py +7270 -0
  10. devflow_engine/core/__init__.py +0 -0
  11. devflow_engine/core/config.py +86 -0
  12. devflow_engine/core/logging.py +29 -0
  13. devflow_engine/core/paths.py +45 -0
  14. devflow_engine/core/toml_kv.py +33 -0
  15. devflow_engine/devflow_event_worker.py +1292 -0
  16. devflow_engine/devflow_state.py +201 -0
  17. devflow_engine/devin2/__init__.py +9 -0
  18. devflow_engine/devin2/agent_definition.py +120 -0
  19. devflow_engine/devin2/pi_runner.py +204 -0
  20. devflow_engine/devin_orchestration.py +69 -0
  21. devflow_engine/docs/prompts/anti-patterns.md +42 -0
  22. devflow_engine/docs/prompts/devin-agent-prompt.md +55 -0
  23. devflow_engine/docs/prompts/devin2-agent-prompt.md +81 -0
  24. devflow_engine/docs/prompts/examples/devin-vapi-clone-reference-exchange.json +85 -0
  25. devflow_engine/doctor/__init__.py +2 -0
  26. devflow_engine/doctor/triage.py +140 -0
  27. devflow_engine/error/__init__.py +0 -0
  28. devflow_engine/error/remediation.py +21 -0
  29. devflow_engine/errors/error_solver_dag.py +522 -0
  30. devflow_engine/errors/runtime_observability.py +67 -0
  31. devflow_engine/idea/__init__.py +4 -0
  32. devflow_engine/idea/actors.py +481 -0
  33. devflow_engine/idea/agentic.py +465 -0
  34. devflow_engine/idea/analyze.py +93 -0
  35. devflow_engine/idea/devin_chat_dag.py +1 -0
  36. devflow_engine/idea/diff.py +99 -0
  37. devflow_engine/idea/drafts.py +446 -0
  38. devflow_engine/idea/idea_creation_dag.py +643 -0
  39. devflow_engine/idea/ideation_enrichment.py +355 -0
  40. devflow_engine/idea/ideation_enrichment_worker.py +19 -0
  41. devflow_engine/idea/paths.py +28 -0
  42. devflow_engine/idea/promote.py +53 -0
  43. devflow_engine/idea/redaction.py +27 -0
  44. devflow_engine/idea/repo_tools.py +1277 -0
  45. devflow_engine/idea/response_mode.py +30 -0
  46. devflow_engine/idea/story_pipeline.py +1585 -0
  47. devflow_engine/idea/sufficiency.py +376 -0
  48. devflow_engine/idea/traditional_stories.py +1257 -0
  49. devflow_engine/implementation/__init__.py +0 -0
  50. devflow_engine/implementation/alembic_preflight.py +700 -0
  51. devflow_engine/implementation/dag.py +8450 -0
  52. devflow_engine/implementation/green_gate.py +93 -0
  53. devflow_engine/implementation/prompts.py +108 -0
  54. devflow_engine/implementation/test_runtime.py +623 -0
  55. devflow_engine/integration/__init__.py +19 -0
  56. devflow_engine/integration/agentic.py +66 -0
  57. devflow_engine/integration/dag.py +3539 -0
  58. devflow_engine/integration/prompts.py +114 -0
  59. devflow_engine/integration/supabase_schema.sql +31 -0
  60. devflow_engine/integration/supabase_sync.py +177 -0
  61. devflow_engine/llm/__init__.py +1 -0
  62. devflow_engine/llm/cli_one_shot.py +84 -0
  63. devflow_engine/llm/cli_stream.py +371 -0
  64. devflow_engine/llm/execution_context.py +26 -0
  65. devflow_engine/llm/invoke.py +1322 -0
  66. devflow_engine/llm/provider_api.py +304 -0
  67. devflow_engine/llm/repo_knowledge.py +588 -0
  68. devflow_engine/llm_primitives.py +315 -0
  69. devflow_engine/orchestration.py +62 -0
  70. devflow_engine/planning/__init__.py +0 -0
  71. devflow_engine/planning/analyze_repo.py +92 -0
  72. devflow_engine/planning/render_drafts.py +133 -0
  73. devflow_engine/playground/__init__.py +0 -0
  74. devflow_engine/playground/hooks.py +26 -0
  75. devflow_engine/playwright_workflow/__init__.py +5 -0
  76. devflow_engine/playwright_workflow/dag.py +1317 -0
  77. devflow_engine/process/__init__.py +5 -0
  78. devflow_engine/process/dag.py +59 -0
  79. devflow_engine/project_registration/__init__.py +3 -0
  80. devflow_engine/project_registration/dag.py +1581 -0
  81. devflow_engine/project_registry.py +109 -0
  82. devflow_engine/prompts/devin/generic/prompt.md +6 -0
  83. devflow_engine/prompts/devin/ideation/prompt.md +263 -0
  84. devflow_engine/prompts/devin/ideation/scenarios.md +5 -0
  85. devflow_engine/prompts/devin/ideation_loop/prompt.md +6 -0
  86. devflow_engine/prompts/devin/insight/prompt.md +11 -0
  87. devflow_engine/prompts/devin/insight/scenarios.md +5 -0
  88. devflow_engine/prompts/devin/intake/prompt.md +15 -0
  89. devflow_engine/prompts/devin/iterate/prompt.md +12 -0
  90. devflow_engine/prompts/devin/shared/eval_doctrine.md +9 -0
  91. devflow_engine/prompts/devin/shared/principles.md +246 -0
  92. devflow_engine/prompts/devin_eval/assessment/prompt.md +18 -0
  93. devflow_engine/prompts/idea/api_ideation_agent/prompt.md +8 -0
  94. devflow_engine/prompts/idea/api_insight_agent/prompt.md +8 -0
  95. devflow_engine/prompts/idea/response_doctrine/prompt.md +18 -0
  96. devflow_engine/prompts/implementation/dependency_assessment/prompt.md +12 -0
  97. devflow_engine/prompts/implementation/green/green/prompt.md +11 -0
  98. devflow_engine/prompts/implementation/green/node_config/prompt.md +3 -0
  99. devflow_engine/prompts/implementation/green_review/outcome_review/prompt.md +5 -0
  100. devflow_engine/prompts/implementation/green_review/prior_run_review/prompt.md +5 -0
  101. devflow_engine/prompts/implementation/red/prompt.md +27 -0
  102. devflow_engine/prompts/implementation/redreview/prompt.md +23 -0
  103. devflow_engine/prompts/implementation/redreview_repair/prompt.md +16 -0
  104. devflow_engine/prompts/implementation/setupdoc/prompt.md +10 -0
  105. devflow_engine/prompts/implementation/story_planning/prompt.md +13 -0
  106. devflow_engine/prompts/implementation/test_design/prompt.md +27 -0
  107. devflow_engine/prompts/integration/README.md +185 -0
  108. devflow_engine/prompts/integration/green/example.md +67 -0
  109. devflow_engine/prompts/integration/green/green/prompt.md +10 -0
  110. devflow_engine/prompts/integration/green/node_config/prompt.md +42 -0
  111. devflow_engine/prompts/integration/green/past_prompts/20260417T212300/green/prompt.md +15 -0
  112. devflow_engine/prompts/integration/green/past_prompts/20260417T212300/node_config/prompt.md +42 -0
  113. devflow_engine/prompts/integration/green_enrich/example.md +79 -0
  114. devflow_engine/prompts/integration/green_enrich/green_enrich/prompt.md +9 -0
  115. devflow_engine/prompts/integration/green_enrich/node_config/prompt.md +41 -0
  116. devflow_engine/prompts/integration/green_enrich/past_prompts/20260417T212300/green_enrich/prompt.md +14 -0
  117. devflow_engine/prompts/integration/green_enrich/past_prompts/20260417T212300/node_config/prompt.md +41 -0
  118. devflow_engine/prompts/integration/red/code_repair/prompt.md +12 -0
  119. devflow_engine/prompts/integration/red/example.md +152 -0
  120. devflow_engine/prompts/integration/red/node_config/prompt.md +86 -0
  121. devflow_engine/prompts/integration/red/past_prompts/20260417T212300/code_repair/prompt.md +19 -0
  122. devflow_engine/prompts/integration/red/past_prompts/20260417T212300/node_config/prompt.md +84 -0
  123. devflow_engine/prompts/integration/red/past_prompts/20260417T212300/red/prompt.md +16 -0
  124. devflow_engine/prompts/integration/red/past_prompts/20260417T212300/red_repair/prompt.md +15 -0
  125. devflow_engine/prompts/integration/red/past_prompts/20260417T215032/code_repair/prompt.md +10 -0
  126. devflow_engine/prompts/integration/red/past_prompts/20260417T215032/node_config/prompt.md +84 -0
  127. devflow_engine/prompts/integration/red/past_prompts/20260417T215032/red_repair/prompt.md +11 -0
  128. devflow_engine/prompts/integration/red/red/prompt.md +11 -0
  129. devflow_engine/prompts/integration/red/red_repair/prompt.md +12 -0
  130. devflow_engine/prompts/integration/red_review/example.md +71 -0
  131. devflow_engine/prompts/integration/red_review/node_config/prompt.md +41 -0
  132. devflow_engine/prompts/integration/red_review/past_prompts/20260417T212300/node_config/prompt.md +41 -0
  133. devflow_engine/prompts/integration/red_review/past_prompts/20260417T212300/red_review/prompt.md +15 -0
  134. devflow_engine/prompts/integration/red_review/red_review/prompt.md +9 -0
  135. devflow_engine/prompts/integration/resolve/example.md +111 -0
  136. devflow_engine/prompts/integration/resolve/node_config/prompt.md +64 -0
  137. devflow_engine/prompts/integration/resolve/past_prompts/20260417T212300/node_config/prompt.md +64 -0
  138. devflow_engine/prompts/integration/resolve/past_prompts/20260417T212300/resolve_implicated_users/prompt.md +15 -0
  139. devflow_engine/prompts/integration/resolve/past_prompts/20260417T212300/resolve_side_effects/prompt.md +15 -0
  140. devflow_engine/prompts/integration/resolve/resolve_implicated_users/prompt.md +10 -0
  141. devflow_engine/prompts/integration/resolve/resolve_side_effects/prompt.md +10 -0
  142. devflow_engine/prompts/integration/validate/build_idea_acceptance_coverage/prompt.md +12 -0
  143. devflow_engine/prompts/integration/validate/code_repair/prompt.md +13 -0
  144. devflow_engine/prompts/integration/validate/example.md +143 -0
  145. devflow_engine/prompts/integration/validate/node_config/prompt.md +87 -0
  146. devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/code_repair/prompt.md +19 -0
  147. devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/node_config/prompt.md +67 -0
  148. devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/validate_enrich_gate/prompt.md +17 -0
  149. devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/validate_repair/prompt.md +16 -0
  150. devflow_engine/prompts/integration/validate/past_prompts/20260417T215032/code_repair/prompt.md +10 -0
  151. devflow_engine/prompts/integration/validate/past_prompts/20260417T215032/node_config/prompt.md +67 -0
  152. devflow_engine/prompts/integration/validate/past_prompts/20260417T215032/validate_repair/prompt.md +9 -0
  153. devflow_engine/prompts/integration/validate/validate_enrich_gate/prompt.md +10 -0
  154. devflow_engine/prompts/integration/validate/validate_repair/prompt.md +20 -0
  155. devflow_engine/prompts/integration/write_workflows/example.md +100 -0
  156. devflow_engine/prompts/integration/write_workflows/node_config/prompt.md +44 -0
  157. devflow_engine/prompts/integration/write_workflows/past_prompts/20260417T212300/node_config/prompt.md +44 -0
  158. devflow_engine/prompts/integration/write_workflows/past_prompts/20260417T212300/write_workflows/prompt.md +17 -0
  159. devflow_engine/prompts/integration/write_workflows/write_workflows/prompt.md +11 -0
  160. devflow_engine/prompts/iterate/README.md +7 -0
  161. devflow_engine/prompts/iterate/coder/prompt.md +11 -0
  162. devflow_engine/prompts/iterate/framer/prompt.md +11 -0
  163. devflow_engine/prompts/iterate/iterator/prompt.md +13 -0
  164. devflow_engine/prompts/iterate/observer/prompt.md +11 -0
  165. devflow_engine/prompts/recovery/diagnosis/prompt.md +7 -0
  166. devflow_engine/prompts/recovery/execution/prompt.md +8 -0
  167. devflow_engine/prompts/recovery/execution_verification/prompt.md +7 -0
  168. devflow_engine/prompts/recovery/failure_investigation/prompt.md +10 -0
  169. devflow_engine/prompts/recovery/preflight_health_repo_repair/prompt.md +8 -0
  170. devflow_engine/prompts/recovery/remediation_execution/prompt.md +11 -0
  171. devflow_engine/prompts/recovery/root_cause_investigation/prompt.md +12 -0
  172. devflow_engine/prompts/scope_idea/doctrine/prompt.md +7 -0
  173. devflow_engine/prompts/source_doc_eval/document/prompt.md +6 -0
  174. devflow_engine/prompts/source_doc_eval/targeted_mutation/prompt.md +9 -0
  175. devflow_engine/prompts/source_doc_mutation/domain_entities/prompt.md +6 -0
  176. devflow_engine/prompts/source_doc_mutation/product_brief/prompt.md +6 -0
  177. devflow_engine/prompts/source_doc_mutation/project_doc_coherence/prompt.md +7 -0
  178. devflow_engine/prompts/source_doc_mutation/project_doc_render/prompt.md +9 -0
  179. devflow_engine/prompts/source_doc_mutation/source_doc_coherence/prompt.md +5 -0
  180. devflow_engine/prompts/source_doc_mutation/source_doc_enrichment_coherence/prompt.md +6 -0
  181. devflow_engine/prompts/source_doc_mutation/user_workflows/prompt.md +6 -0
  182. devflow_engine/prompts/source_scope/doctrine/prompt.md +10 -0
  183. devflow_engine/prompts/ui_grounding/doctrine/prompt.md +7 -0
  184. devflow_engine/recovery/__init__.py +3 -0
  185. devflow_engine/recovery/dag.py +2609 -0
  186. devflow_engine/recovery/models.py +220 -0
  187. devflow_engine/refactor.py +93 -0
  188. devflow_engine/registry/__init__.py +1 -0
  189. devflow_engine/registry/cards.py +238 -0
  190. devflow_engine/registry/domain_normalize.py +60 -0
  191. devflow_engine/registry/effects.py +65 -0
  192. devflow_engine/registry/enforce_report.py +150 -0
  193. devflow_engine/registry/module_cards_classify.py +164 -0
  194. devflow_engine/registry/module_cards_draft.py +184 -0
  195. devflow_engine/registry/module_cards_gate.py +59 -0
  196. devflow_engine/registry/packages.py +347 -0
  197. devflow_engine/registry/pathways.py +323 -0
  198. devflow_engine/review/__init__.py +11 -0
  199. devflow_engine/review/dag.py +588 -0
  200. devflow_engine/review/review_story.py +67 -0
  201. devflow_engine/scope_idea/__init__.py +3 -0
  202. devflow_engine/scope_idea/agentic.py +39 -0
  203. devflow_engine/scope_idea/dag.py +1069 -0
  204. devflow_engine/scope_idea/models.py +175 -0
  205. devflow_engine/skills/builtins/devflow/queue_failure_investigation/SKILL.md +112 -0
  206. devflow_engine/skills/builtins/devflow/queue_idea_to_story/SKILL.md +120 -0
  207. devflow_engine/skills/builtins/devflow/queue_integration/SKILL.md +105 -0
  208. devflow_engine/skills/builtins/devflow/queue_recovery/SKILL.md +108 -0
  209. devflow_engine/skills/builtins/devflow/queue_runtime_core/SKILL.md +155 -0
  210. devflow_engine/skills/builtins/devflow/queue_story_implementation/SKILL.md +122 -0
  211. devflow_engine/skills/builtins/devin/idea_to_story_handoff/SKILL.md +120 -0
  212. devflow_engine/skills/builtins/devin/ideation/SKILL.md +168 -0
  213. devflow_engine/skills/builtins/devin/ideation/state-and-phrasing-reference.md +18 -0
  214. devflow_engine/skills/builtins/devin/insight/SKILL.md +22 -0
  215. devflow_engine/skills/registry.example.yaml +42 -0
  216. devflow_engine/source_doc_assumptions.py +291 -0
  217. devflow_engine/source_doc_mutation_dag.py +1606 -0
  218. devflow_engine/source_doc_mutation_eval.py +417 -0
  219. devflow_engine/source_doc_mutation_worker.py +25 -0
  220. devflow_engine/source_docs_schema.py +207 -0
  221. devflow_engine/source_docs_updater.py +309 -0
  222. devflow_engine/source_scope/__init__.py +15 -0
  223. devflow_engine/source_scope/agentic.py +45 -0
  224. devflow_engine/source_scope/dag.py +1626 -0
  225. devflow_engine/source_scope/models.py +177 -0
  226. devflow_engine/stores/__init__.py +0 -0
  227. devflow_engine/stores/execution_store.py +3534 -0
  228. devflow_engine/story/__init__.py +0 -0
  229. devflow_engine/story/contracts.py +160 -0
  230. devflow_engine/story/discovery.py +47 -0
  231. devflow_engine/story/evidence.py +118 -0
  232. devflow_engine/story/hashing.py +27 -0
  233. devflow_engine/story/implemented_queue_purge.py +148 -0
  234. devflow_engine/story/indexer.py +105 -0
  235. devflow_engine/story/io.py +20 -0
  236. devflow_engine/story/markdown_contracts.py +298 -0
  237. devflow_engine/story/reconciliation.py +408 -0
  238. devflow_engine/story/validate_stories.py +149 -0
  239. devflow_engine/story/validate_tests_story.py +512 -0
  240. devflow_engine/story/validation.py +133 -0
  241. devflow_engine/ui_grounding/__init__.py +11 -0
  242. devflow_engine/ui_grounding/agentic.py +31 -0
  243. devflow_engine/ui_grounding/dag.py +874 -0
  244. devflow_engine/ui_grounding/models.py +224 -0
  245. devflow_engine/ui_grounding/pencil_bridge.py +247 -0
  246. devflow_engine/vendor/__init__.py +0 -0
  247. devflow_engine/vendor/datalumina_genai/__init__.py +11 -0
  248. devflow_engine/vendor/datalumina_genai/core/__init__.py +0 -0
  249. devflow_engine/vendor/datalumina_genai/core/exceptions.py +9 -0
  250. devflow_engine/vendor/datalumina_genai/core/nodes/__init__.py +0 -0
  251. devflow_engine/vendor/datalumina_genai/core/nodes/agent.py +48 -0
  252. devflow_engine/vendor/datalumina_genai/core/nodes/agent_streaming_node.py +26 -0
  253. devflow_engine/vendor/datalumina_genai/core/nodes/base.py +89 -0
  254. devflow_engine/vendor/datalumina_genai/core/nodes/concurrent.py +30 -0
  255. devflow_engine/vendor/datalumina_genai/core/nodes/router.py +69 -0
  256. devflow_engine/vendor/datalumina_genai/core/schema.py +72 -0
  257. devflow_engine/vendor/datalumina_genai/core/task.py +52 -0
  258. devflow_engine/vendor/datalumina_genai/core/validate.py +139 -0
  259. devflow_engine/vendor/datalumina_genai/core/workflow.py +200 -0
  260. devflow_engine/worker.py +1086 -0
  261. devflow_engine/worker_guard.py +233 -0
  262. devflow_engine-1.0.0.dist-info/METADATA +235 -0
  263. devflow_engine-1.0.0.dist-info/RECORD +393 -0
  264. devflow_engine-1.0.0.dist-info/WHEEL +4 -0
  265. devflow_engine-1.0.0.dist-info/entry_points.txt +3 -0
  266. devin/__init__.py +6 -0
  267. devin/dag.py +58 -0
  268. devin/dag_two_arm.py +138 -0
  269. devin/devin_chat_scenario_catalog.json +588 -0
  270. devin/devin_eval.py +677 -0
  271. devin/nodes/__init__.py +0 -0
  272. devin/nodes/ideation/__init__.py +0 -0
  273. devin/nodes/ideation/node.py +195 -0
  274. devin/nodes/ideation/playground.py +267 -0
  275. devin/nodes/ideation/prompt.md +65 -0
  276. devin/nodes/ideation/scenarios/continue_refinement.py +13 -0
  277. devin/nodes/ideation/scenarios/continue_refinement_evals.py +18 -0
  278. devin/nodes/ideation/scenarios/idea_fits_existing_patterns.py +17 -0
  279. devin/nodes/ideation/scenarios/idea_fits_existing_patterns_evals.py +16 -0
  280. devin/nodes/ideation/scenarios/large_idea_split.py +4 -0
  281. devin/nodes/ideation/scenarios/large_idea_split_evals.py +17 -0
  282. devin/nodes/ideation/scenarios/source_documentation_added.py +4 -0
  283. devin/nodes/ideation/scenarios/source_documentation_added_evals.py +16 -0
  284. devin/nodes/ideation/scenarios/user_says_create_it.py +30 -0
  285. devin/nodes/ideation/scenarios/user_says_create_it_evals.py +23 -0
  286. devin/nodes/ideation/scenarios/vague_idea.py +16 -0
  287. devin/nodes/ideation/scenarios/vague_idea_evals.py +47 -0
  288. devin/nodes/ideation/tools.json +312 -0
  289. devin/nodes/insight/__init__.py +0 -0
  290. devin/nodes/insight/node.py +49 -0
  291. devin/nodes/insight/playground.py +154 -0
  292. devin/nodes/insight/prompt.md +61 -0
  293. devin/nodes/insight/scenarios/architecture_pattern_query.py +15 -0
  294. devin/nodes/insight/scenarios/architecture_pattern_query_evals.py +25 -0
  295. devin/nodes/insight/scenarios/codebase_exploration.py +15 -0
  296. devin/nodes/insight/scenarios/codebase_exploration_evals.py +23 -0
  297. devin/nodes/insight/scenarios/devin_ideation_routing.py +19 -0
  298. devin/nodes/insight/scenarios/devin_ideation_routing_evals.py +39 -0
  299. devin/nodes/insight/scenarios/devin_insight_routing.py +20 -0
  300. devin/nodes/insight/scenarios/devin_insight_routing_evals.py +40 -0
  301. devin/nodes/insight/scenarios/operational_debugging.py +15 -0
  302. devin/nodes/insight/scenarios/operational_debugging_evals.py +23 -0
  303. devin/nodes/insight/scenarios/operational_question.py +9 -0
  304. devin/nodes/insight/scenarios/operational_question_evals.py +8 -0
  305. devin/nodes/insight/scenarios/queue_status.py +15 -0
  306. devin/nodes/insight/scenarios/queue_status_evals.py +23 -0
  307. devin/nodes/insight/scenarios/source_doc_explanation.py +14 -0
  308. devin/nodes/insight/scenarios/source_doc_explanation_evals.py +21 -0
  309. devin/nodes/insight/scenarios/worker_state_check.py +15 -0
  310. devin/nodes/insight/scenarios/worker_state_check_evals.py +22 -0
  311. devin/nodes/insight/tools.json +126 -0
  312. devin/nodes/intake/__init__.py +0 -0
  313. devin/nodes/intake/node.py +27 -0
  314. devin/nodes/intake/playground.py +47 -0
  315. devin/nodes/intake/prompt.md +12 -0
  316. devin/nodes/intake/scenarios/ideation_routing.py +4 -0
  317. devin/nodes/intake/scenarios/ideation_routing_evals.py +5 -0
  318. devin/nodes/intake/scenarios/insight_routing.py +4 -0
  319. devin/nodes/intake/scenarios/insight_routing_evals.py +5 -0
  320. devin/nodes/iterate/README.md +44 -0
  321. devin/nodes/iterate/__init__.py +1 -0
  322. devin/nodes/iterate/_archived_design_stages/01-objectives-requirements.md +112 -0
  323. devin/nodes/iterate/_archived_design_stages/02-evals.md +131 -0
  324. devin/nodes/iterate/_archived_design_stages/03-tools-and-boundaries.md +110 -0
  325. devin/nodes/iterate/_archived_design_stages/04-harness-and-playground.md +32 -0
  326. devin/nodes/iterate/_archived_design_stages/05-prompt-deferred.md +11 -0
  327. devin/nodes/iterate/_archived_design_stages/coder_agent_design/01-objectives-requirements.md +20 -0
  328. devin/nodes/iterate/_archived_design_stages/coder_agent_design/02-evals.md +8 -0
  329. devin/nodes/iterate/_archived_design_stages/coder_agent_design/03-tools-and-boundaries.md +14 -0
  330. devin/nodes/iterate/_archived_design_stages/coder_agent_design/04-harness-and-playground.md +12 -0
  331. devin/nodes/iterate/_archived_design_stages/framer_agent_design/01-objectives-requirements.md +20 -0
  332. devin/nodes/iterate/_archived_design_stages/framer_agent_design/02-evals.md +8 -0
  333. devin/nodes/iterate/_archived_design_stages/framer_agent_design/03-tools-and-boundaries.md +13 -0
  334. devin/nodes/iterate/_archived_design_stages/framer_agent_design/04-harness-and-playground.md +12 -0
  335. devin/nodes/iterate/_archived_design_stages/iterator_agent_design/01-objectives-requirements.md +25 -0
  336. devin/nodes/iterate/_archived_design_stages/iterator_agent_design/02-evals.md +9 -0
  337. devin/nodes/iterate/_archived_design_stages/iterator_agent_design/03-tools-and-boundaries.md +14 -0
  338. devin/nodes/iterate/_archived_design_stages/iterator_agent_design/04-harness-and-playground.md +12 -0
  339. devin/nodes/iterate/_archived_design_stages/observer_agent_design/01-objectives-requirements.md +20 -0
  340. devin/nodes/iterate/_archived_design_stages/observer_agent_design/02-evals.md +8 -0
  341. devin/nodes/iterate/_archived_design_stages/observer_agent_design/03-tools-and-boundaries.md +14 -0
  342. devin/nodes/iterate/_archived_design_stages/observer_agent_design/04-harness-and-playground.md +13 -0
  343. devin/nodes/iterate/agent-roles.md +89 -0
  344. devin/nodes/iterate/agents/README.md +10 -0
  345. devin/nodes/iterate/artifacts.md +504 -0
  346. devin/nodes/iterate/contract.md +100 -0
  347. devin/nodes/iterate/eval-plan.md +74 -0
  348. devin/nodes/iterate/node.py +100 -0
  349. devin/nodes/iterate/pipeline/README.md +13 -0
  350. devin/nodes/iterate/playground-contract.md +76 -0
  351. devin/nodes/iterate/prompt.md +11 -0
  352. devin/nodes/iterate/scenarios/README.md +38 -0
  353. devin/nodes/iterate/scenarios/artifact-and-loop-scenarios.md +101 -0
  354. devin/nodes/iterate/scenarios/coder_artifact_alignment.py +32 -0
  355. devin/nodes/iterate/scenarios/coder_artifact_alignment_evals.py +45 -0
  356. devin/nodes/iterate/scenarios/coder_bounded_fix.py +27 -0
  357. devin/nodes/iterate/scenarios/coder_bounded_fix_evals.py +45 -0
  358. devin/nodes/iterate/scenarios/devin_iterate_routing.py +21 -0
  359. devin/nodes/iterate/scenarios/devin_iterate_routing_evals.py +36 -0
  360. devin/nodes/iterate/scenarios/framer_scope_boundary.py +25 -0
  361. devin/nodes/iterate/scenarios/framer_scope_boundary_evals.py +57 -0
  362. devin/nodes/iterate/scenarios/framer_task_framing.py +25 -0
  363. devin/nodes/iterate/scenarios/framer_task_framing_evals.py +58 -0
  364. devin/nodes/iterate/scenarios/iterate_error_fix.py +21 -0
  365. devin/nodes/iterate/scenarios/iterate_error_fix_evals.py +39 -0
  366. devin/nodes/iterate/scenarios/iterate_quick_change.py +21 -0
  367. devin/nodes/iterate/scenarios/iterate_quick_change_evals.py +35 -0
  368. devin/nodes/iterate/scenarios/iterate_to_idea_promotion.py +23 -0
  369. devin/nodes/iterate/scenarios/iterate_to_idea_promotion_evals.py +53 -0
  370. devin/nodes/iterate/scenarios/iterate_to_insight_reroute.py +23 -0
  371. devin/nodes/iterate/scenarios/iterate_to_insight_reroute_evals.py +53 -0
  372. devin/nodes/iterate/scenarios/observer_evidence_seam.py +28 -0
  373. devin/nodes/iterate/scenarios/observer_evidence_seam_evals.py +55 -0
  374. devin/nodes/iterate/scenarios/observer_repro_creation.py +28 -0
  375. devin/nodes/iterate/scenarios/observer_repro_creation_evals.py +45 -0
  376. devin/nodes/iterate/scenarios/routing-matrix.md +45 -0
  377. devin/nodes/shared/__init__.py +0 -0
  378. devin/nodes/shared/filemaker_expert.md +80 -0
  379. devin/nodes/shared/filemaker_expert.py +354 -0
  380. devin/nodes/shared/filemaker_expert_eval/runner.py +176 -0
  381. devin/nodes/shared/filemaker_expert_eval/scenarios.json +65 -0
  382. devin/nodes/shared/goldilocks_advisor_eval/runner.py +214 -0
  383. devin/nodes/shared/goldilocks_advisor_eval/scenarios.json +58 -0
  384. devin/nodes/shared/helpers.py +156 -0
  385. devin/nodes/shared/idea_compliance_advisor_eval/runner.py +252 -0
  386. devin/nodes/shared/idea_compliance_advisor_eval/scenarios.json +75 -0
  387. devin/nodes/shared/models.py +44 -0
  388. devin/nodes/shared/post.py +40 -0
  389. devin/nodes/shared/router.py +107 -0
  390. devin/nodes/shared/tools.py +191 -0
  391. devin/shared/devin-chat-rubric.md +237 -0
  392. devin/shared/devin-chat-scenario-suite.md +90 -0
  393. devin/shared/eval_doctrine.md +9 -0
@@ -0,0 +1,3539 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import hashlib
5
+ import logging
6
+ import os
7
+ from urllib.error import HTTPError
8
+ import re
9
+ import subprocess
10
+ import uuid
11
+ from collections.abc import Callable as _Callable
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Any
15
+ from urllib.request import Request, urlopen
16
+
17
+ from pydantic import BaseModel, Field
18
+
19
+ from ..idea.paths import get_idea_paths
20
+ from ..vendor.datalumina_genai.core.nodes.agent import AgentConfig, AgentNode
21
+ from ..vendor.datalumina_genai.core.nodes.base import Node
22
+ from ..vendor.datalumina_genai.core.schema import NodeConfig, WorkflowSchema
23
+ from ..vendor.datalumina_genai.core.task import TaskContext
24
+ from ..vendor.datalumina_genai.core.workflow import Workflow
25
+ from ..project_registry import find_project_for_repo_root, resolve_project_entry
26
+ from ..project_registration.dag import _infer_owner_repo, _lookup_supabase_project_uuid
27
+ from ..devflow_state import _is_uuid_like, publish_devflow_state
28
+ from . import agentic as integration_agentic
29
+ from .prompts import load_integration_node_instruction
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ INTEGRATION_RESUME_METADATA = "resume_fingerprint.json"
34
+ _SOURCE_EVIDENCE_EXTENSIONS = {".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"}
35
+ _SOURCE_ROOT_NAMES = ("app", "src")
36
+ _PROJECT_MANIFESTS = ("pyproject.toml", "package.json", "setup.py", "setup.cfg", "requirements.txt", "Pipfile")
37
+ _IGNORED_EVIDENCE_DIR_NAMES = {".devflow", ".git", "node_modules", "dist", "build", "coverage", "__pycache__", ".venv", "venv"}
38
+ _STORY_CODE_PATH_KEYS = {"file_targets", "implementation_targets", "paths", "path", "file_path", "files", "written_paths", "validator_input_paths", "seams", "preferred_pathways"}
39
+ _PROJECT_CODE_ROOT_METADATA_KEYS = ("implementation_roots", "implementation_root", "source_roots", "source_root", "code_roots", "code_root", "app_roots", "app_root", "backend_root", "frontend_root")
40
+
41
+
42
+ def _sha256_json(payload: dict[str, Any] | list[Any]) -> str:
43
+ return hashlib.sha256(
44
+ json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
45
+ ).hexdigest()
46
+
47
+
48
+ def _sha256_file(path: Path) -> str:
49
+ return hashlib.sha256(path.read_bytes()).hexdigest()
50
+
51
+
52
+ def _relative_repo_path(*, repo_root: Path, path: Path) -> str:
53
+ try:
54
+ return str(path.relative_to(repo_root))
55
+ except ValueError:
56
+ return str(path)
57
+
58
+
59
+ def _story_code_paths(payload: Any) -> list[str]:
60
+ results: list[str] = []
61
+
62
+ def _push(value: Any) -> None:
63
+ if isinstance(value, str):
64
+ candidate = value.strip()
65
+ if candidate:
66
+ results.append(candidate)
67
+ elif isinstance(value, dict):
68
+ path_value = value.get("path")
69
+ if isinstance(path_value, str) and path_value.strip():
70
+ results.append(path_value.strip())
71
+ elif isinstance(value, list):
72
+ for item in value:
73
+ _push(item)
74
+
75
+ def _walk(node: Any) -> None:
76
+ if isinstance(node, dict):
77
+ for key, value in node.items():
78
+ if str(key or "").strip().lower() in _STORY_CODE_PATH_KEYS:
79
+ _push(value)
80
+ _walk(value)
81
+ elif isinstance(node, list):
82
+ for item in node:
83
+ _walk(item)
84
+
85
+ _walk(payload)
86
+ return results
87
+
88
+
89
+ def _story_code_root(repo_root: Path, raw_path: str) -> Path | None:
90
+ candidate = raw_path.strip()
91
+ if not candidate:
92
+ return None
93
+ path = Path(candidate)
94
+ if path.is_absolute():
95
+ try:
96
+ path = path.relative_to(repo_root)
97
+ except ValueError:
98
+ return None
99
+ parts = path.parts
100
+ if not parts or parts[0].startswith('.') or parts[0] in {'tests', 'ai_docs'}:
101
+ return None
102
+ for index, part in enumerate(parts[:-1]):
103
+ if part in _SOURCE_ROOT_NAMES:
104
+ return repo_root.joinpath(*parts[: index + 1])
105
+ if len(parts) > 1:
106
+ return repo_root.joinpath(*parts[:-1])
107
+ return None
108
+
109
+
110
+ def _project_configured_code_roots(repo_root: Path) -> list[Path]:
111
+ project_entry = find_project_for_repo_root(repo_root)
112
+ metadata = (project_entry or {}).get("metadata")
113
+ if not isinstance(metadata, dict):
114
+ return []
115
+
116
+ roots: list[Path] = []
117
+ for key in _PROJECT_CODE_ROOT_METADATA_KEYS:
118
+ raw = metadata.get(key)
119
+ values = raw if isinstance(raw, list) else [raw]
120
+ for value in values:
121
+ if not isinstance(value, str) or not value.strip():
122
+ continue
123
+ candidate = Path(value.strip())
124
+ if not candidate.is_absolute():
125
+ candidate = repo_root / candidate
126
+ try:
127
+ resolved = candidate.resolve()
128
+ resolved.relative_to(repo_root.resolve())
129
+ except Exception:
130
+ continue
131
+ if resolved.is_dir():
132
+ roots.append(resolved)
133
+ return sorted(set(roots))
134
+
135
+
136
+ def _find_manifest_project_roots(repo_root: Path) -> list[Path]:
137
+ project_roots: list[Path] = []
138
+ candidates = [repo_root]
139
+ candidates.extend(
140
+ path
141
+ for path in repo_root.iterdir()
142
+ if path.is_dir() and path.name not in _IGNORED_EVIDENCE_DIR_NAMES and not path.name.startswith(".")
143
+ )
144
+ for path in candidates:
145
+ if any((path / name).exists() for name in _PROJECT_MANIFESTS):
146
+ project_roots.append(path)
147
+ return sorted(set(project_roots))
148
+
149
+
150
+ def _expand_project_code_roots(project_roots: list[Path]) -> list[Path]:
151
+ roots: list[Path] = []
152
+ for project_root in project_roots:
153
+ nested_roots = [project_root / name for name in _SOURCE_ROOT_NAMES if (project_root / name).is_dir()]
154
+ if nested_roots:
155
+ roots.extend(nested_roots)
156
+ continue
157
+ direct_files = [child for child in project_root.iterdir() if child.is_file() and child.suffix in _SOURCE_EVIDENCE_EXTENSIONS]
158
+ if direct_files:
159
+ roots.append(project_root)
160
+ return sorted(set(root for root in roots if root.exists()))
161
+
162
+
163
+ def _implementation_code_roots(*, repo_root: Path, implemented_stories: list[dict[str, Any]]) -> list[Path]:
164
+ story_roots = sorted({
165
+ root
166
+ for story in implemented_stories
167
+ for raw_path in _story_code_paths(story)
168
+ for root in [_story_code_root(repo_root, raw_path)]
169
+ if root is not None and root.exists()
170
+ })
171
+ if story_roots:
172
+ return story_roots
173
+
174
+ configured_roots = _project_configured_code_roots(repo_root)
175
+ if configured_roots:
176
+ return configured_roots
177
+
178
+ project_roots = _find_manifest_project_roots(repo_root)
179
+ nested_project_roots = [root for root in project_roots if root != repo_root]
180
+ manifest_roots = _expand_project_code_roots(nested_project_roots or project_roots)
181
+ if manifest_roots:
182
+ return manifest_roots
183
+
184
+ return [path for path in (repo_root / 'app', repo_root / 'src') if path.is_dir()]
185
+
186
+
187
+ def _iter_code_evidence_files(*, repo_root: Path, implemented_stories: list[dict[str, Any]]) -> list[Path]:
188
+ files: list[Path] = []
189
+ seen: set[Path] = set()
190
+ for root in _implementation_code_roots(repo_root=repo_root, implemented_stories=implemented_stories):
191
+ for dirpath, dirnames, filenames in os.walk(root):
192
+ dirnames[:] = [name for name in dirnames if name not in _IGNORED_EVIDENCE_DIR_NAMES and not name.startswith('.')]
193
+ current_root = Path(dirpath)
194
+ for filename in filenames:
195
+ file_path = current_root / filename
196
+ if file_path.suffix not in _SOURCE_EVIDENCE_EXTENSIONS or file_path in seen:
197
+ continue
198
+ files.append(file_path)
199
+ seen.add(file_path)
200
+ return sorted(files)
201
+
202
+
203
+ def _integration_snapshot_paths(*, repo_root: Path, idea_id: str) -> list[Path]:
204
+ idea_dir = repo_root / ".devflow" / "ideas" / idea_id
205
+ paths: list[Path] = []
206
+ idea_json_path = idea_dir / "idea.json"
207
+ if idea_json_path.exists():
208
+ paths.append(idea_json_path)
209
+ story_sets_dir = idea_dir / "devflow_story_sets"
210
+ implemented_stories: list[dict[str, Any]] = []
211
+ if story_sets_dir.exists():
212
+ for path in sorted(story_sets_dir.rglob("*.json")):
213
+ if path.name == "manifest.json":
214
+ continue
215
+ paths.append(path)
216
+ try:
217
+ story_data = json.loads(path.read_text(encoding="utf-8"))
218
+ except Exception:
219
+ continue
220
+ if isinstance(story_data, dict):
221
+ implemented_stories.append(story_data)
222
+ paths.extend(_iter_code_evidence_files(repo_root=repo_root, implemented_stories=implemented_stories))
223
+ ai_docs_dir = repo_root / "ai_docs"
224
+ if ai_docs_dir.exists():
225
+ paths.extend(sorted(ai_docs_dir.rglob("*.md")))
226
+ return paths
227
+
228
+
229
+ def _compute_integration_freshness(
230
+ *,
231
+ repo_root: Path,
232
+ idea_id: str,
233
+ payload_body: dict[str, Any],
234
+ ) -> dict[str, Any]:
235
+ payload_sha256 = _sha256_json(payload_body)
236
+ repo_files: list[dict[str, str]] = []
237
+ for path in _integration_snapshot_paths(repo_root=repo_root, idea_id=idea_id):
238
+ repo_files.append({"path": _relative_repo_path(repo_root=repo_root, path=path), "sha256": _sha256_file(path)})
239
+ repo_snapshot_sha256 = _sha256_json(repo_files)
240
+ fingerprint = _sha256_json(
241
+ {
242
+ "idea_id": idea_id,
243
+ "payload_sha256": payload_sha256,
244
+ "repo_snapshot_sha256": repo_snapshot_sha256,
245
+ }
246
+ )
247
+ return {
248
+ "fingerprint": fingerprint,
249
+ "payload_sha256": payload_sha256,
250
+ "repo_snapshot_sha256": repo_snapshot_sha256,
251
+ }
252
+
253
+
254
+ def _payload_body_without_freshness(payload: dict[str, Any]) -> dict[str, Any]:
255
+ body = dict(payload)
256
+ body.pop("freshness", None)
257
+ return body
258
+
259
+
260
+ def _load_resume_metadata(run_dir: Path) -> dict[str, Any] | None:
261
+ path = run_dir / INTEGRATION_RESUME_METADATA
262
+ if not path.exists():
263
+ return None
264
+ try:
265
+ data = json.loads(path.read_text(encoding="utf-8"))
266
+ except Exception:
267
+ return None
268
+ return data if isinstance(data, dict) else None
269
+
270
+
271
+ def _resume_dir_matches_fingerprint(*, run_dir: Path, expected_fingerprint: str | None) -> bool:
272
+ if not expected_fingerprint:
273
+ return False
274
+ metadata = _load_resume_metadata(run_dir)
275
+ if metadata is None:
276
+ return False
277
+ return str(metadata.get("fingerprint") or "") == expected_fingerprint
278
+
279
+
280
+ def _write_resume_metadata(run_dir: Path, freshness: dict[str, Any]) -> Path:
281
+ path = run_dir / INTEGRATION_RESUME_METADATA
282
+ _write_json(path, freshness)
283
+ return path
284
+
285
+
286
+ def prepare_integration_payload(*, repo_root: Path, idea_id: str) -> Path:
287
+ """Build and write integration_payload.json for the given idea.
288
+
289
+ Scans idea.json, devflow_story_sets, app/src Python files, and ai_docs markdown.
290
+ Returns the path to the written payload file.
291
+ """
292
+ idea_dir = repo_root / '.devflow' / 'ideas' / idea_id
293
+ idea_json_path = idea_dir / 'idea.json'
294
+ if not idea_json_path.exists():
295
+ raise ValueError(f'idea.json not found: {idea_json_path}')
296
+ implemented_idea = json.loads(idea_json_path.read_text(encoding='utf-8'))
297
+
298
+ story_sets_dir = idea_dir / 'devflow_story_sets'
299
+ implemented_stories: list[dict[str, Any]] = []
300
+ if story_sets_dir.exists():
301
+ for story_path in sorted(story_sets_dir.rglob('*.json')):
302
+ if story_path.name == 'manifest.json':
303
+ continue
304
+ try:
305
+ story_data = json.loads(story_path.read_text(encoding='utf-8'))
306
+ story_id = str(story_data.get('story_id') or story_data.get('id') or story_path.stem)
307
+ title = str(story_data.get('title') or story_data.get('name') or story_id)
308
+ summary = {
309
+ 'story_id': story_id,
310
+ 'title': title,
311
+ 'side_effect_ids': list(story_data.get('side_effect_ids') or []),
312
+ 'required_planes': list(story_data.get('required_planes') or []),
313
+ }
314
+ for key in _STORY_CODE_PATH_KEYS:
315
+ if key in story_data:
316
+ summary[key] = story_data.get(key)
317
+ implemented_stories.append(summary)
318
+ except Exception:
319
+ pass
320
+
321
+ code_evidence: list[dict[str, Any]] = []
322
+ for code_path in _iter_code_evidence_files(repo_root=repo_root, implemented_stories=implemented_stories):
323
+ code_evidence.append({'path': _relative_repo_path(repo_root=repo_root, path=code_path), 'side_effect_ids': [], 'interaction_points': []})
324
+
325
+ source_docs: list[dict[str, Any]] = []
326
+ ai_docs_dir = repo_root / 'ai_docs'
327
+ if ai_docs_dir.exists():
328
+ for md_path in sorted(ai_docs_dir.rglob('*.md')):
329
+ try:
330
+ rel = str(md_path.relative_to(repo_root))
331
+ except ValueError:
332
+ rel = str(md_path)
333
+ source_docs.append({'path': rel})
334
+
335
+ payload: dict[str, Any] = {
336
+ 'idea_id': idea_id,
337
+ 'implemented_idea': implemented_idea,
338
+ 'implemented_stories': implemented_stories,
339
+ 'code_evidence': code_evidence,
340
+ 'source_docs': source_docs,
341
+ }
342
+ payload['freshness'] = _compute_integration_freshness(
343
+ repo_root=repo_root,
344
+ idea_id=idea_id,
345
+ payload_body=payload,
346
+ )
347
+ payload_path = idea_dir / 'integration_payload.json'
348
+ payload_path.parent.mkdir(parents=True, exist_ok=True)
349
+ payload_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + '\n', encoding='utf-8')
350
+ return payload_path
351
+
352
+
353
+ def _keychain_get(service: str, account: str) -> str | None:
354
+ try:
355
+ proc = subprocess.run(
356
+ ["security", "find-generic-password", "-s", service, "-a", account, "-w"],
357
+ capture_output=True,
358
+ text=True,
359
+ check=False,
360
+ timeout=10,
361
+ )
362
+ except Exception:
363
+ return None
364
+ if proc.returncode != 0:
365
+ return None
366
+ return proc.stdout.strip() or None
367
+
368
+
369
+ def _resolve_supabase_rest_config() -> tuple[str, str] | None:
370
+ if os.environ.get("PYTEST_CURRENT_TEST"):
371
+ return None
372
+ url = (
373
+ os.environ.get("DEVFLOW_SUPABASE_URL")
374
+ or os.environ.get("SUPABASE_URL")
375
+ or _keychain_get("Supabase URL", "Clarity")
376
+ )
377
+ key = (
378
+ os.environ.get("DEVFLOW_SUPABASE_SERVICE_KEY")
379
+ or os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
380
+ or os.environ.get("SUPABASE_SERVICE_KEY")
381
+ or _keychain_get("Supabase Service Key", "Clarity")
382
+ )
383
+ if not url or not key:
384
+ return None
385
+ return url.rstrip("/"), key
386
+
387
+
388
+ def _postgrest_request(
389
+ *,
390
+ method: str,
391
+ url: str,
392
+ key: str,
393
+ body: Any | None = None,
394
+ prefer: str | None = None,
395
+ ) -> Any:
396
+ payload = None if body is None else json.dumps(body).encode("utf-8")
397
+ req = Request(url, data=payload, method=method)
398
+ req.add_header("apikey", key)
399
+ req.add_header("Authorization", f"Bearer {key}")
400
+ if body is not None:
401
+ req.add_header("Content-Type", "application/json")
402
+ if prefer:
403
+ req.add_header("Prefer", prefer)
404
+ with urlopen(req, timeout=30) as resp:
405
+ raw = resp.read().decode("utf-8")
406
+ return json.loads(raw) if raw else None
407
+
408
+ @dataclass(frozen=True)
409
+ class Stage:
410
+ node_id: str
411
+ name: str
412
+ deps: list[str]
413
+
414
+
415
+ @dataclass(frozen=True)
416
+ class IntegrationDagResult:
417
+ exit_code: int
418
+ message: str
419
+ pipeline_dir: Path
420
+ artifacts: dict[str, str]
421
+ iterations_used: int
422
+
423
+
424
+ CANONICAL_ACTOR_ALLOWED_COMBINATIONS: set[tuple[str, str, str]] = {
425
+ ("human", "open", "public"),
426
+ ("human", "organization", "authenticated"),
427
+ ("human", "organization", "admin"),
428
+ ("human", "global", "super_user"),
429
+ ("ai", "organization", "authenticated"),
430
+ ("ai", "organization", "admin"),
431
+ ("system", "organization", "authenticated"),
432
+ ("system", "organization", "admin"),
433
+ ("system", "global", "super_user"),
434
+ }
435
+
436
+
437
+ class CanonicalActor(BaseModel):
438
+ kind: str
439
+ scope: str
440
+ authority: str
441
+ rationale: str = ""
442
+
443
+
444
+ class DeniedActorCandidate(BaseModel):
445
+ kind: str
446
+ scope: str
447
+ authority: str
448
+ reason: str
449
+
450
+
451
+ class SideEffectArtifactItem(BaseModel):
452
+ id: str
453
+ summary: str
454
+ story_ids: list[str] = Field(default_factory=list)
455
+ rationale: str = ""
456
+ interaction_points: list[dict[str, Any]] = Field(default_factory=list)
457
+ needed_interaction_points: list[dict[str, Any]] = Field(default_factory=list)
458
+ process_sequence: list[str] = Field(default_factory=list)
459
+ branches: list[dict[str, Any]] = Field(default_factory=list)
460
+ resulting_artifacts: list[str] = Field(default_factory=list)
461
+
462
+
463
+ class SideEffectsArtifact(BaseModel):
464
+ idea_id: str
465
+ side_effects: list[SideEffectArtifactItem]
466
+ alignment_context: dict[str, Any] = Field(default_factory=dict)
467
+
468
+
469
+ class ImplicatedUsersArtifact(BaseModel):
470
+ idea_id: str
471
+ implicated_users: list[CanonicalActor]
472
+ denied_candidates: list[DeniedActorCandidate] = Field(default_factory=list)
473
+
474
+
475
+ class StoryBackingItem(BaseModel):
476
+ story_id: str
477
+ title: str
478
+
479
+
480
+ class WorkflowArtifact(BaseModel):
481
+ workflow_id: str
482
+ idea_id: str
483
+ side_effect: dict[str, str]
484
+ implicated_users: list[dict[str, Any]]
485
+ story_backing: list[dict[str, Any]] = Field(default_factory=list)
486
+ code_backing: list[dict[str, Any]] = Field(default_factory=list)
487
+ source_doc_backing: list[dict[str, Any]] = Field(default_factory=list)
488
+ interaction_points: list[dict[str, Any]] = Field(default_factory=list)
489
+ needed_interaction_points: list[dict[str, Any]] = Field(default_factory=list)
490
+ process_sequence: list[str] = Field(default_factory=list)
491
+ branches: list[dict[str, Any]] = Field(default_factory=list)
492
+ resulting_artifacts: list[str] = Field(default_factory=list)
493
+ mermaid: str | None = None
494
+
495
+
496
+ class WorkflowSetArtifact(BaseModel):
497
+ idea_id: str
498
+ workflows: list[WorkflowArtifact]
499
+
500
+
501
+ class ValidateEnrichFinding(BaseModel):
502
+ workflow_id: str
503
+ severity: str
504
+ summary: str
505
+ details: list[str] = Field(default_factory=list)
506
+ blocking: bool = False
507
+
508
+
509
+ class ValidateRepairDeltaLedger(BaseModel):
510
+ verdict_changes: list[str] = Field(default_factory=list)
511
+ evidence_changes: list[str] = Field(default_factory=list)
512
+ newly_resolved_seams: list[str] = Field(default_factory=list)
513
+ contradiction_citations: list[str] = Field(default_factory=list)
514
+
515
+
516
+ class IdeaAcceptanceCoverageEntry(BaseModel):
517
+ criterion_index: int
518
+ criterion: str
519
+ verdict: str
520
+ proof_summary: str = ""
521
+ story_ids: list[str] = Field(default_factory=list)
522
+ workflow_ids: list[str] = Field(default_factory=list)
523
+ side_effect_ids: list[str] = Field(default_factory=list)
524
+ evidence_refs: list[str] = Field(default_factory=list)
525
+ gaps: list[str] = Field(default_factory=list)
526
+ missing_seams: list[str] = Field(default_factory=list)
527
+
528
+
529
+ class IdeaAcceptanceCoverageArtifact(BaseModel):
530
+ idea_id: str
531
+ summary: str
532
+ criteria: list[IdeaAcceptanceCoverageEntry] = Field(default_factory=list)
533
+ protected_sections: list[dict[str, Any]] = Field(default_factory=list)
534
+ do_not_touch: list[str] = Field(default_factory=list)
535
+ execution_lineage: dict[str, Any] | None = None
536
+
537
+
538
+ class VegIdeaAcceptanceBuilderArtifact(BaseModel):
539
+ idea_id: str
540
+ summary: str
541
+ enriched_workflows: list[WorkflowArtifact]
542
+ findings: list[ValidateEnrichFinding] = Field(default_factory=list)
543
+ idea_acceptance_coverage: IdeaAcceptanceCoverageArtifact
544
+ repair_delta_ledger: ValidateRepairDeltaLedger | None = None
545
+
546
+
547
+ class HarnessSelection(BaseModel):
548
+ harness_kind: str
549
+ interaction_point_ref: str
550
+ interaction_point_kind: str
551
+ rationale: str
552
+ gap_explicit: bool = False
553
+
554
+
555
+ class IntegrationAssertion(BaseModel):
556
+ assertion_id: str
557
+ summary: str
558
+ evidence_anchor: str = ""
559
+
560
+
561
+ class RedWorkflowPackage(BaseModel):
562
+ workflow_id: str
563
+ side_effect_id: str
564
+ status: str
565
+ harness_selection: HarnessSelection
566
+ failing_artifacts: list[dict[str, Any]] = Field(default_factory=list)
567
+ expected_assertions: list[IntegrationAssertion] = Field(default_factory=list)
568
+ implicated_user_routes: list[dict[str, Any]] = Field(default_factory=list)
569
+ gap_notes: list[str] = Field(default_factory=list)
570
+
571
+
572
+ class RedArtifact(BaseModel):
573
+ idea_id: str
574
+ packages: list[RedWorkflowPackage]
575
+ summary: str
576
+
577
+
578
+ class RedReviewFinding(BaseModel):
579
+ workflow_id: str
580
+ verdict: str
581
+ findings: list[str] = Field(default_factory=list)
582
+ repair_directions: list[str] = Field(default_factory=list)
583
+
584
+
585
+ class RedReviewArtifact(BaseModel):
586
+ idea_id: str
587
+ summary: str
588
+ reviewed_packages: list[RedWorkflowPackage]
589
+ findings: list[RedReviewFinding]
590
+
591
+
592
+ class GreenWorkflowPackage(BaseModel):
593
+ workflow_id: str
594
+ side_effect_id: str
595
+ implementation_changes: list[dict[str, Any]] = Field(default_factory=list)
596
+ harness_support_changes: list[dict[str, Any]] = Field(default_factory=list)
597
+ passing_strategy: list[str] = Field(default_factory=list)
598
+ proof_artifacts: list[dict[str, Any]] = Field(default_factory=list)
599
+
600
+
601
+ class GreenArtifact(BaseModel):
602
+ idea_id: str
603
+ summary: str
604
+ packages: list[GreenWorkflowPackage]
605
+
606
+
607
+ class GreenEnrichWorkflowPackage(BaseModel):
608
+ workflow_id: str
609
+ strengthened_assertions: list[IntegrationAssertion] = Field(default_factory=list)
610
+ observability_improvements: list[dict[str, Any]] = Field(default_factory=list)
611
+ brittleness_reductions: list[str] = Field(default_factory=list)
612
+
613
+
614
+ class GreenEnrichArtifact(BaseModel):
615
+ idea_id: str
616
+ summary: str
617
+ packages: list[GreenEnrichWorkflowPackage]
618
+
619
+
620
+ class CommitWorkflowPackage(BaseModel):
621
+ workflow_id: str
622
+ side_effect_id: str
623
+ included_artifacts: list[str] = Field(default_factory=list)
624
+ traceability: dict[str, Any] = Field(default_factory=dict)
625
+
626
+
627
+ class CommitArtifact(BaseModel):
628
+ idea_id: str
629
+ summary: str
630
+ packages: list[CommitWorkflowPackage]
631
+ package_manifest: dict[str, Any] = Field(default_factory=dict)
632
+
633
+
634
+ class FilePatch(BaseModel):
635
+ file_path: str # relative to repo_root
636
+ content: str # full file content (overwrite)
637
+ rationale: str = "" # why this patch was needed
638
+
639
+
640
+ class CodeRepairArtifact(BaseModel):
641
+ patches: list[FilePatch] = Field(default_factory=list)
642
+ updated_workflows: list[WorkflowArtifact] = Field(default_factory=list)
643
+ repair_summary: str = ""
644
+ unresolvable_failures: list[str] = Field(default_factory=list)
645
+
646
+
647
+ def _workflow_side_effect_map(side_effects_artifact: dict[str, Any]) -> dict[str, dict[str, Any]]:
648
+ return {
649
+ str(item.get("id") or ""): dict(item)
650
+ for item in (side_effects_artifact.get("side_effects") or [])
651
+ if str(item.get("id") or "")
652
+ }
653
+
654
+
655
+ def _ground_workflow_in_side_effect(*, workflow: dict[str, Any], side_effects_artifact: dict[str, Any]) -> dict[str, Any]:
656
+ grounded = dict(workflow)
657
+ side_effect_id = str(grounded.get("side_effect", {}).get("id") or "")
658
+ side_effect = _workflow_side_effect_map(side_effects_artifact).get(side_effect_id)
659
+ if side_effect is None:
660
+ return grounded
661
+ grounded["interaction_points"] = list(side_effect.get("interaction_points") or [])
662
+ grounded["needed_interaction_points"] = list(side_effect.get("needed_interaction_points") or [])
663
+ if not grounded.get("process_sequence"):
664
+ grounded["process_sequence"] = list(side_effect.get("process_sequence") or [])
665
+ if not grounded.get("resulting_artifacts"):
666
+ grounded["resulting_artifacts"] = list(side_effect.get("resulting_artifacts") or [])
667
+ return grounded
668
+
669
+
670
+ def _ground_workflows_in_side_effects(*, workflows: list[dict[str, Any]], side_effects_artifact: dict[str, Any]) -> list[dict[str, Any]]:
671
+ return [
672
+ _ground_workflow_in_side_effect(workflow=workflow, side_effects_artifact=side_effects_artifact)
673
+ for workflow in workflows
674
+ ]
675
+
676
+
677
+ def _run_veg_idea_acceptance_builder(
678
+ *,
679
+ repo_root: Path,
680
+ pipeline_dir: Path,
681
+ idea_id: str,
682
+ implemented_idea: dict[str, Any] | None,
683
+ implemented_stories: list[dict[str, Any]] | None,
684
+ side_effects_artifact: dict[str, Any],
685
+ implicated_users_artifact: dict[str, Any],
686
+ workflows: list[dict[str, Any]],
687
+ code_evidence: list[dict[str, Any]] | None,
688
+ source_docs: list[dict[str, Any]] | None,
689
+ node_id: str,
690
+ ) -> VegIdeaAcceptanceBuilderArtifact:
691
+ artifact, envelope = integration_agentic.run_integration_agent_step(
692
+ repo_root=repo_root,
693
+ stage_name="build_idea_acceptance_coverage",
694
+ output_model=VegIdeaAcceptanceBuilderArtifact,
695
+ context_payload={
696
+ "idea_id": idea_id,
697
+ "implemented_idea": implemented_idea or {},
698
+ "implemented_stories": list(implemented_stories or []),
699
+ "side_effects_artifact": side_effects_artifact,
700
+ "implicated_users_artifact": implicated_users_artifact,
701
+ "workflows": workflows,
702
+ "code_evidence": list(code_evidence or []),
703
+ "source_docs": list(source_docs or []),
704
+ },
705
+ guidance=[],
706
+ timeout_seconds=_integration_agent_timeout_seconds(),
707
+ )
708
+ integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id=node_id, envelope=envelope)
709
+ return artifact
710
+
711
+
712
+ def default_integration_stages() -> list[Stage]:
713
+ return [
714
+ Stage("resolve_side_effects", "ResolveSideEffectsAndImplicatedUsers", []),
715
+ Stage("write_workflows", "WriteWorkflowPerSideEffect", ["resolve_side_effects"]),
716
+ Stage("veg", "VEGHybridValidateEnrichGate", ["write_workflows"]),
717
+ Stage("red", "Red", ["veg"]),
718
+ Stage("redreview", "RedReview", ["red"]),
719
+ Stage("green", "Green", ["redreview"]),
720
+ Stage("greenenrich", "GreenEnrich", ["green"]),
721
+ Stage("commit", "Commit", ["greenenrich"]),
722
+ ]
723
+
724
+
725
+ def render_stage_plan(stages: list[Stage]) -> str:
726
+ return "\n".join(
727
+ f"{stage.node_id} ({stage.name}) deps=[{','.join(stage.deps) if stage.deps else '-'}]"
728
+ for stage in stages
729
+ ) + "\n"
730
+
731
+
732
+ def _write_json(path: Path, payload: dict[str, Any] | list[Any]) -> None:
733
+ path.parent.mkdir(parents=True, exist_ok=True)
734
+ path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
735
+
736
+
737
+ def _slug(value: str) -> str:
738
+ text = re.sub(r"[^a-z0-9]+", "_", value.lower()).strip("_")
739
+ return text or "item"
740
+
741
+
742
+ def _integration_agent_timeout_seconds() -> int:
743
+ raw = os.environ.get("DEVFLOW_INTEGRATION_AGENT_TIMEOUT")
744
+ if not raw:
745
+ return 1800
746
+ try:
747
+ value = int(raw)
748
+ except ValueError:
749
+ return 1800
750
+ return value if value > 0 else 1800
751
+
752
+
753
+ def _preferred_harness_for_interaction_point(point: dict[str, Any]) -> str:
754
+ kind = str(point.get("kind") or "").strip().lower()
755
+ # Fallback: enriched interaction points may use "type" instead of "kind"
756
+ if not kind:
757
+ kind = _normalize_interaction_type(str(point.get("type") or "").strip().lower())
758
+ if kind == "ui":
759
+ return "playwright"
760
+ if kind == "endpoint":
761
+ return "endpoint_harness"
762
+ if kind == "cli":
763
+ return "cli_harness"
764
+ if kind == "artifact":
765
+ return "artifact_probe"
766
+ if kind in {"background", "event", "queue", "job"}:
767
+ return "background_observer"
768
+ return "integration_harness"
769
+
770
+
771
+ def _normalize_interaction_type(raw_type: str) -> str:
772
+ """Map enriched interaction-point ``type`` values to canonical ``kind`` values.
773
+
774
+ Enrichment agents generate specific type strings (``api_boundary``,
775
+ ``health_check``, …) that don't match the narrow ``kind`` vocabulary used by
776
+ ``_preferred_harness_for_interaction_point``. We first try an exact lookup,
777
+ then fall back to keyword-based family matching so newly-invented type
778
+ strings still resolve correctly.
779
+ """
780
+ _TYPE_TO_KIND: dict[str, str] = {
781
+ "api_endpoint": "endpoint",
782
+ "http_endpoint": "endpoint",
783
+ "rest_endpoint": "endpoint",
784
+ "graphql_endpoint": "endpoint",
785
+ "health_check": "endpoint",
786
+ "webhook": "endpoint",
787
+ "websocket": "endpoint",
788
+ "api_boundary": "endpoint",
789
+ "auth_boundary": "endpoint",
790
+ "network_boundary": "endpoint",
791
+ "cli_command": "cli",
792
+ "cli_entrypoint": "cli",
793
+ "ui_component": "ui",
794
+ "ui_page": "ui",
795
+ "browser": "ui",
796
+ "file_artifact": "artifact",
797
+ "artifact_output": "artifact",
798
+ "background_job": "background",
799
+ "cron_job": "background",
800
+ "queue_consumer": "queue",
801
+ "event_handler": "event",
802
+ }
803
+ exact = _TYPE_TO_KIND.get(raw_type)
804
+ if exact is not None:
805
+ return exact
806
+ # Keyword-family fallback for types not in the exact map
807
+ _FAMILY_KEYWORDS: list[tuple[tuple[str, ...], str]] = [
808
+ (("endpoint", "api", "http", "rest", "graphql", "health", "webhook", "websocket", "boundary"), "endpoint"),
809
+ (("cli",), "cli"),
810
+ (("ui", "browser", "page"), "ui"),
811
+ (("artifact", "file"), "artifact"),
812
+ (("background", "cron", "job"), "background"),
813
+ (("queue",), "queue"),
814
+ (("event",), "event"),
815
+ ]
816
+ for keywords, kind in _FAMILY_KEYWORDS:
817
+ if any(kw in raw_type for kw in keywords):
818
+ return kind
819
+ return raw_type
820
+
821
+
822
+ def _load_code_evidence_for_workflows(
823
+ *,
824
+ workflows: list[dict[str, Any]],
825
+ code_evidence: list[dict[str, Any]] | None,
826
+ repo_root: Path,
827
+ max_files: int = 20,
828
+ ) -> list[dict[str, Any]]:
829
+ """Load actual file contents for code evidence relevant to the failing workflows."""
830
+ if not code_evidence:
831
+ return []
832
+ # Collect all side_effect_ids referenced by the failing workflows
833
+ relevant_se_ids: set[str] = set()
834
+ for wf in workflows:
835
+ se = wf.get("side_effect") or {}
836
+ if se.get("id"):
837
+ relevant_se_ids.add(str(se["id"]))
838
+
839
+ loaded: list[dict[str, Any]] = []
840
+ seen: set[str] = set()
841
+ for item in code_evidence:
842
+ if len(loaded) >= max_files:
843
+ break
844
+ item_se_ids = set(str(x) for x in (item.get("side_effect_ids") or []))
845
+ if relevant_se_ids and not relevant_se_ids.intersection(item_se_ids) and item_se_ids:
846
+ continue # not relevant to these workflows
847
+ path_str = str(item.get("path") or "")
848
+ if not path_str or path_str in seen:
849
+ continue
850
+ seen.add(path_str)
851
+ full_path = repo_root / path_str
852
+ if full_path.exists():
853
+ try:
854
+ content = full_path.read_text(encoding="utf-8", errors="replace")
855
+ loaded.append({"path": path_str, "content": content[:8000]}) # cap per file
856
+ except Exception:
857
+ loaded.append({"path": path_str, "content": "(unreadable)"})
858
+ else:
859
+ loaded.append({"path": path_str, "content": "(file not found)"})
860
+ return loaded
861
+
862
+
863
+ def _build_post_integration_playwright_request(
864
+ *,
865
+ idea_id: str,
866
+ trigger_run_id: str | None,
867
+ workflows: list[dict[str, Any]],
868
+ red_artifact: dict[str, Any],
869
+ ) -> dict[str, Any]:
870
+ red_packages = {
871
+ str(package.get("workflow_id") or ""): package
872
+ for package in red_artifact.get("packages") or []
873
+ if isinstance(package, dict)
874
+ }
875
+ workflow_requests: list[dict[str, Any]] = []
876
+ for workflow in workflows:
877
+ workflow_id = str(workflow.get("workflow_id") or "")
878
+ package = red_packages.get(workflow_id) or {}
879
+ harness = package.get("harness_selection") or {}
880
+ interaction_points = list(workflow.get("interaction_points") or [])
881
+ ui_points = [
882
+ point for point in interaction_points
883
+ if _preferred_harness_for_interaction_point(point) == "playwright"
884
+ ]
885
+ harness_kind = str(harness.get("harness_kind") or "")
886
+ requires_playwright = bool(ui_points or harness_kind == "playwright")
887
+ workflow_requests.append({
888
+ "workflow_id": workflow_id,
889
+ "side_effect_id": str(
890
+ workflow.get("side_effect", {}).get("id")
891
+ or package.get("side_effect_id")
892
+ or ""
893
+ ),
894
+ "requires_playwright": requires_playwright,
895
+ "harness_kind": harness_kind or ("playwright" if requires_playwright else "integration_harness"),
896
+ "interaction_points": ui_points if ui_points else interaction_points[:1],
897
+ "story_backing": list(workflow.get("story_backing") or []),
898
+ "required_proof": [
899
+ "real browser execution for visible UI controls",
900
+ "assert observable business results, not only clicks or page load",
901
+ (
902
+ "verify role, status, filter, reset, and cancellation behavior "
903
+ "where the workflow exposes those controls"
904
+ ),
905
+ "fail early when expected seed records are missing",
906
+ ] if requires_playwright else [],
907
+ })
908
+
909
+ required_workflows = [item for item in workflow_requests if item["requires_playwright"]]
910
+ return {
911
+ "kind": "post_integration_playwright_request.v1",
912
+ "idea_id": idea_id,
913
+ "trigger_run_id": trigger_run_id,
914
+ "status": "required" if required_workflows else "not_required",
915
+ "process": "post_integration_playwright_dag",
916
+ "workflow_requests": workflow_requests,
917
+ "required_workflow_ids": [item["workflow_id"] for item in required_workflows],
918
+ "expected_artifacts": [
919
+ "app_route_inventory.json",
920
+ "ui_element_ledger.json",
921
+ "playwright_coverage_verifier.json",
922
+ "playwright_generation_plan.json",
923
+ "generated_playwright_specs_manifest.json",
924
+ "workflow_preview_manifest.json",
925
+ ],
926
+ "blocking_gates": [
927
+ (
928
+ "ui_element_ledger has no uncovered mutation, navigation, destructive, "
929
+ "mode-toggle, filter, sort, search, or form-control rows without a "
930
+ "disabled/deferred rationale"
931
+ ),
932
+ "playwright_coverage_verifier.passed is true",
933
+ "generated specs assert business outcomes on the correct proof plane for each workflow",
934
+ (
935
+ "video/preview capture is produced only after passing E2E proof and is "
936
+ "not accepted as a substitute for assertions"
937
+ ),
938
+ ],
939
+ "worker_handoff": {
940
+ "queue_type": "post_integration_playwright",
941
+ "run_after": "successful integration queue drain with no active recovery",
942
+ "current_artifact_dir": ".devflow/post_integration_playwright/current",
943
+ },
944
+ }
945
+
946
+
947
+ class IntegrationDagEvent(BaseModel):
948
+ repo_root: str
949
+ idea_id: str
950
+ implemented_idea: dict[str, Any]
951
+ implemented_stories: list[dict[str, Any]] = Field(default_factory=list)
952
+ code_evidence: list[dict[str, Any]] = Field(default_factory=list)
953
+ source_docs: list[dict[str, Any]] = Field(default_factory=list)
954
+ pipeline_dir: str
955
+ prior_run_dir: str | None = None
956
+ resume_freshness: dict[str, Any] = Field(default_factory=dict)
957
+ project_id: str | None = None
958
+ dag_run_id: str | None = None
959
+
960
+
961
+ def _integration_dfs_progress(task_context: TaskContext, summary: str) -> None:
962
+ """Publish a progress update to DFS (devflow_state) for the current integration run."""
963
+ project_id = task_context.metadata.get("project_id")
964
+ dag_run_id = task_context.metadata.get("dag_run_id")
965
+ idea_id = getattr(getattr(task_context, "event", None), "idea_id", None)
966
+ if not project_id:
967
+ return
968
+ try:
969
+ publish_devflow_state(
970
+ project_id=project_id,
971
+ run_id=dag_run_id,
972
+ current_state="running",
973
+ current_status="processing",
974
+ run_summary=summary,
975
+ display="project",
976
+ display_path=f"idea:{idea_id}" if idea_id else None,
977
+ )
978
+ except Exception:
979
+ return
980
+
981
+
982
+ def _integration_root(repo_root: Path, idea_id: str) -> Path:
983
+ return get_idea_paths(repo_root, idea_id=idea_id).idea_dir / "integration"
984
+
985
+
986
+ def _integration_current_dir(repo_root: Path, idea_id: str) -> Path:
987
+ return _integration_root(repo_root, idea_id) / "current"
988
+
989
+
990
+ def _integration_runs_dir(repo_root: Path, idea_id: str) -> Path:
991
+ return _integration_root(repo_root, idea_id) / "runs"
992
+
993
+
994
+ def _legacy_integration_runs_dir(repo_root: Path, idea_id: str) -> Path:
995
+ return get_idea_paths(repo_root, idea_id=idea_id).idea_dir / "pipelines" / "integration_dag"
996
+
997
+
998
+ def _write_integration_artifact(*, repo_root: Path, idea_id: str, pipeline_dir: Path, filename: str, data: dict[str, Any]) -> tuple[Path, Path]:
999
+ run_path = pipeline_dir / filename
1000
+ current_dir = _integration_current_dir(repo_root, idea_id)
1001
+ current_dir.mkdir(parents=True, exist_ok=True)
1002
+ current_path = current_dir / filename
1003
+ _write_json(run_path, data)
1004
+ _write_json(current_path, data)
1005
+ return run_path, current_path
1006
+
1007
+
1008
+ def _invalidate_integration_current_from_stage(*, current_dir: Path, stage: str) -> None:
1009
+ ordered: dict[str, list[str]] = {
1010
+ "resolve": [
1011
+ "side_effects.json",
1012
+ "implicated_users.json",
1013
+ "workflow_inventory.json",
1014
+ "validation_gate.json",
1015
+ "validation_gate_agentic_review.json",
1016
+ "idea_acceptance_coverage.json",
1017
+ "validation_repair_clusters.json",
1018
+ "red_package.json",
1019
+ "red_validation.json",
1020
+ "red_review.json",
1021
+ "green_package.json",
1022
+ "green_enrich.json",
1023
+ "commit_package.json",
1024
+ ],
1025
+ "write_workflows": [
1026
+ "workflow_inventory.json",
1027
+ "validation_gate.json",
1028
+ "validation_gate_agentic_review.json",
1029
+ "idea_acceptance_coverage.json",
1030
+ "validation_repair_clusters.json",
1031
+ "red_package.json",
1032
+ "red_validation.json",
1033
+ "red_review.json",
1034
+ "green_package.json",
1035
+ "green_enrich.json",
1036
+ "commit_package.json",
1037
+ ],
1038
+ "validate": [
1039
+ "validation_gate.json",
1040
+ "validation_gate_agentic_review.json",
1041
+ "idea_acceptance_coverage.json",
1042
+ "validation_repair_clusters.json",
1043
+ "red_package.json",
1044
+ "red_validation.json",
1045
+ "red_review.json",
1046
+ "green_package.json",
1047
+ "green_enrich.json",
1048
+ "commit_package.json",
1049
+ ],
1050
+ "red": [
1051
+ "red_package.json",
1052
+ "red_validation.json",
1053
+ "red_review.json",
1054
+ "green_package.json",
1055
+ "green_enrich.json",
1056
+ "commit_package.json",
1057
+ ],
1058
+ "red_review": [
1059
+ "red_review.json",
1060
+ "green_package.json",
1061
+ "green_enrich.json",
1062
+ "commit_package.json",
1063
+ ],
1064
+ "green": [
1065
+ "green_package.json",
1066
+ "green_enrich.json",
1067
+ "commit_package.json",
1068
+ ],
1069
+ "green_enrich": [
1070
+ "green_enrich.json",
1071
+ "commit_package.json",
1072
+ ],
1073
+ "commit": ["commit_package.json"],
1074
+ }
1075
+ targets = ordered.get(stage, [])
1076
+ for name in targets:
1077
+ path = current_dir / name
1078
+ if path.exists():
1079
+ path.unlink()
1080
+ if stage in {"resolve", "write_workflows"}:
1081
+ for path in current_dir.glob("workflow_*.json"):
1082
+ if path.name != "workflow_inventory.json":
1083
+ path.unlink()
1084
+
1085
+
1086
+ def _current_dir_from_task(task_context: TaskContext) -> Path:
1087
+ current_dir = task_context.metadata.get("current_dir")
1088
+ if current_dir:
1089
+ return Path(current_dir)
1090
+ event = task_context.event
1091
+ return _integration_current_dir(Path(event.repo_root), event.idea_id)
1092
+
1093
+
1094
+ def _read_resume_artifact(task_context: TaskContext, filename: str) -> tuple[dict[str, Any] | None, Path | None]:
1095
+ event = task_context.event
1096
+ repo_root = Path(event.repo_root)
1097
+ current_dir = _integration_current_dir(repo_root, event.idea_id)
1098
+ current_path = current_dir / filename
1099
+ expected_fingerprint = str(task_context.metadata.get("resume_fingerprint") or "") or None
1100
+ if current_path.exists() and _resume_dir_matches_fingerprint(run_dir=current_dir, expected_fingerprint=expected_fingerprint):
1101
+ try:
1102
+ return json.loads(current_path.read_text(encoding="utf-8")), current_path
1103
+ except Exception:
1104
+ pass
1105
+ prior_str = task_context.metadata.get("prior_run_dir")
1106
+ if prior_str:
1107
+ prior_dir = Path(prior_str)
1108
+ prior_path = prior_dir / filename
1109
+ if prior_path.exists() and _resume_dir_matches_fingerprint(run_dir=prior_dir, expected_fingerprint=expected_fingerprint):
1110
+ try:
1111
+ return json.loads(prior_path.read_text(encoding="utf-8")), prior_path
1112
+ except Exception:
1113
+ pass
1114
+ return None, None
1115
+
1116
+
1117
+ def _try_load_prior_artifact(
1118
+ task_context: TaskContext,
1119
+ *filenames: str,
1120
+ validity_check: _Callable[[dict[str, Any]], bool] | None = None,
1121
+ ) -> dict[str, Any] | None:
1122
+ """Attempt to load a prior run artifact for checkpoint/resume.
1123
+
1124
+ Returns the parsed primary artifact (first filename) when all files exist
1125
+ and the optional validity_check passes; otherwise returns None.
1126
+ """
1127
+ if task_context.metadata.get("resume_aborted"):
1128
+ return None
1129
+ loaded: list[tuple[dict[str, Any], Path]] = []
1130
+ for fname in filenames:
1131
+ data, path = _read_resume_artifact(task_context, fname)
1132
+ if data is None or path is None:
1133
+ return None
1134
+ loaded.append((data, path))
1135
+ primary = loaded[0][0]
1136
+ if validity_check is not None and not validity_check(primary):
1137
+ return None
1138
+ return primary
1139
+
1140
+
1141
+ def _abort_resume(task_context: TaskContext) -> None:
1142
+ """Mark resume as aborted so all downstream nodes re-run."""
1143
+ task_context.metadata["resume_aborted"] = True
1144
+
1145
+
1146
+ def _score_prior_run_dir(run_dir: Path, *, expected_fingerprint: str | None = None) -> tuple[int, float]:
1147
+ """Prefer the most complete reusable prior integration run, not newest mtime alone."""
1148
+ if not _resume_dir_matches_fingerprint(run_dir=run_dir, expected_fingerprint=expected_fingerprint):
1149
+ return (0, run_dir.stat().st_mtime)
1150
+ score = 0
1151
+ try:
1152
+ inventory_path = run_dir / "workflow_inventory.json"
1153
+ if inventory_path.exists():
1154
+ inventory = json.loads(inventory_path.read_text(encoding="utf-8"))
1155
+ side_effect_ids = [str(sid) for sid in (inventory.get("side_effect_ids") or []) if str(sid)]
1156
+ workflow_files = [run_dir / f"workflow_{_slug(se_id)}.json" for se_id in side_effect_ids]
1157
+ if side_effect_ids and all(path.exists() for path in workflow_files):
1158
+ score = max(score, 1)
1159
+ gate_path = run_dir / "validation_gate.json"
1160
+ if gate_path.exists():
1161
+ gate = json.loads(gate_path.read_text(encoding="utf-8"))
1162
+ if bool(gate.get("passed")) or bool(gate.get("structural_only")):
1163
+ score = max(score, 2)
1164
+ red_validation_path = run_dir / "red_validation.json"
1165
+ if (run_dir / "red_package.json").exists() and red_validation_path.exists():
1166
+ red_validation = json.loads(red_validation_path.read_text(encoding="utf-8"))
1167
+ if bool(red_validation.get("passed")):
1168
+ score = max(score, 3)
1169
+ red_review_path = run_dir / "red_review.json"
1170
+ if red_review_path.exists():
1171
+ red_review = json.loads(red_review_path.read_text(encoding="utf-8"))
1172
+ if bool(red_review.get("agentic_review_clear")):
1173
+ score = max(score, 4)
1174
+ if (run_dir / "green_package.json").exists():
1175
+ score = max(score, 5)
1176
+ if (run_dir / "green_enrich.json").exists():
1177
+ score = max(score, 6)
1178
+ if (run_dir / "commit_package.json").exists():
1179
+ score = max(score, 7)
1180
+ except Exception:
1181
+ score = 0
1182
+ return (score, run_dir.stat().st_mtime)
1183
+
1184
+
1185
+ def _select_prior_run_dir(
1186
+ runs_parent: Path,
1187
+ current_pipeline_dir: Path,
1188
+ fallback_runs_parent: Path | None = None,
1189
+ *,
1190
+ expected_fingerprint: str | None = None,
1191
+ ) -> Path | None:
1192
+ candidates = [d for d in runs_parent.iterdir() if d.is_dir() and d != current_pipeline_dir] if runs_parent.exists() else []
1193
+ if fallback_runs_parent is not None and fallback_runs_parent.exists():
1194
+ candidates.extend(d for d in fallback_runs_parent.iterdir() if d.is_dir() and d != current_pipeline_dir)
1195
+ if not candidates:
1196
+ return None
1197
+ scored = [
1198
+ (run_dir, *_score_prior_run_dir(run_dir, expected_fingerprint=expected_fingerprint))
1199
+ for run_dir in candidates
1200
+ ]
1201
+ usable = [item for item in scored if item[1] > 0]
1202
+ if not usable:
1203
+ return None
1204
+ return max(usable, key=lambda item: (item[1], item[2]))[0]
1205
+
1206
+
1207
+ class LoadIntegrationContextNode(Node):
1208
+ async def process(self, task_context: TaskContext) -> TaskContext:
1209
+ event = task_context.event
1210
+ pipeline_dir = Path(event.pipeline_dir)
1211
+ current_dir = _integration_current_dir(Path(event.repo_root), event.idea_id)
1212
+ current_dir.mkdir(parents=True, exist_ok=True)
1213
+ pipeline_dir.mkdir(parents=True, exist_ok=True)
1214
+ task_context.metadata["pipeline_dir"] = str(pipeline_dir)
1215
+ task_context.metadata["current_dir"] = str(current_dir)
1216
+ task_context.metadata["artifacts"] = {}
1217
+ prior_run_dir = getattr(event, "prior_run_dir", None)
1218
+ task_context.metadata["prior_run_dir"] = prior_run_dir
1219
+ task_context.metadata["resume_aborted"] = False
1220
+ task_context.metadata["project_id"] = getattr(event, "project_id", None)
1221
+ task_context.metadata["dag_run_id"] = getattr(event, "dag_run_id", None)
1222
+ freshness = dict(getattr(event, "resume_freshness", None) or {})
1223
+ task_context.metadata["resume_fingerprint"] = str(freshness.get("fingerprint") or "")
1224
+ if not _resume_dir_matches_fingerprint(
1225
+ run_dir=current_dir,
1226
+ expected_fingerprint=task_context.metadata["resume_fingerprint"],
1227
+ ):
1228
+ _invalidate_integration_current_from_stage(current_dir=current_dir, stage="resolve")
1229
+ _write_resume_metadata(pipeline_dir, freshness)
1230
+ _write_resume_metadata(current_dir, freshness)
1231
+ self.save_output({"pipeline_dir": str(pipeline_dir)})
1232
+ return task_context
1233
+
1234
+
1235
+ class ResolveSideEffectsAndImplicatedUsersNode(AgentNode):
1236
+ def get_agent_config(self) -> AgentConfig:
1237
+ return AgentConfig(
1238
+ instructions=load_integration_node_instruction("resolve"),
1239
+ output_type=SideEffectsArtifact,
1240
+ )
1241
+
1242
+ async def process(self, task_context: TaskContext) -> TaskContext:
1243
+ # -- checkpoint/resume --
1244
+ prior_se = _try_load_prior_artifact(task_context, "side_effects.json", "implicated_users.json")
1245
+ if prior_se is not None:
1246
+ prior_users, prior_users_path = _read_resume_artifact(task_context, "implicated_users.json")
1247
+ side_effects_path = _current_dir_from_task(task_context) / "side_effects.json"
1248
+ implied_path = prior_users_path or (_current_dir_from_task(task_context) / "implicated_users.json")
1249
+ task_context.metadata["side_effects_artifact"] = prior_se
1250
+ task_context.metadata["implicated_users_artifact"] = prior_users or {}
1251
+ task_context.metadata["artifacts"].update({
1252
+ "side_effects": str(side_effects_path),
1253
+ "implicated_users": str(implied_path),
1254
+ })
1255
+ self.save_output({"resumed_from": str(_current_dir_from_task(task_context)), "skipped_llm": True})
1256
+ return task_context
1257
+ _abort_resume(task_context)
1258
+ # -- end checkpoint/resume --
1259
+
1260
+ _integration_dfs_progress(task_context, "Resolving side effects")
1261
+
1262
+ event = task_context.event
1263
+ repo_root = Path(event.repo_root)
1264
+ pipeline_dir = Path(task_context.metadata["pipeline_dir"])
1265
+ _invalidate_integration_current_from_stage(current_dir=_current_dir_from_task(task_context), stage="resolve")
1266
+ side_effects_artifact, side_effects_envelope = integration_agentic.run_integration_agent_step(
1267
+ repo_root=repo_root,
1268
+ stage_name="resolve_side_effects",
1269
+ output_model=SideEffectsArtifact,
1270
+ context_payload={
1271
+ "idea_id": event.idea_id,
1272
+ "implemented_idea": event.implemented_idea,
1273
+ "implemented_stories": event.implemented_stories,
1274
+ "code_evidence": event.code_evidence,
1275
+ "source_docs": event.source_docs,
1276
+ "canonical_actor_allowed_combinations": sorted(list(CANONICAL_ACTOR_ALLOWED_COMBINATIONS)),
1277
+ },
1278
+ guidance=[],
1279
+ timeout_seconds=_integration_agent_timeout_seconds(),
1280
+ )
1281
+ _integration_dfs_progress(task_context, "Resolving users")
1282
+ implicated_users_artifact, users_envelope = integration_agentic.run_integration_agent_step(
1283
+ repo_root=repo_root,
1284
+ stage_name="resolve_implicated_users",
1285
+ output_model=ImplicatedUsersArtifact,
1286
+ context_payload={
1287
+ "idea_id": event.idea_id,
1288
+ "implemented_idea": event.implemented_idea,
1289
+ "implemented_stories": event.implemented_stories,
1290
+ "code_evidence": event.code_evidence,
1291
+ "source_docs": event.source_docs,
1292
+ "canonical_actor_allowed_combinations": sorted(list(CANONICAL_ACTOR_ALLOWED_COMBINATIONS)),
1293
+ },
1294
+ guidance=[],
1295
+ timeout_seconds=_integration_agent_timeout_seconds(),
1296
+ )
1297
+ integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id="resolve_side_effects", envelope=side_effects_envelope)
1298
+ integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id="resolve_implicated_users", envelope=users_envelope)
1299
+
1300
+ side_effects_path, _ = _write_integration_artifact(
1301
+ repo_root=repo_root,
1302
+ idea_id=event.idea_id,
1303
+ pipeline_dir=pipeline_dir,
1304
+ filename="side_effects.json",
1305
+ data=side_effects_artifact.model_dump(),
1306
+ )
1307
+ implicated_users_path, _ = _write_integration_artifact(
1308
+ repo_root=repo_root,
1309
+ idea_id=event.idea_id,
1310
+ pipeline_dir=pipeline_dir,
1311
+ filename="implicated_users.json",
1312
+ data=implicated_users_artifact.model_dump(),
1313
+ )
1314
+ task_context.metadata["side_effects_artifact"] = side_effects_artifact.model_dump()
1315
+ task_context.metadata["implicated_users_artifact"] = implicated_users_artifact.model_dump()
1316
+ task_context.metadata["artifacts"].update({
1317
+ "side_effects": str(side_effects_path),
1318
+ "implicated_users": str(implicated_users_path),
1319
+ })
1320
+ self.save_output({
1321
+ "side_effects_ref": str(side_effects_path),
1322
+ "implicated_users_ref": str(implicated_users_path),
1323
+ })
1324
+ return task_context
1325
+
1326
+
1327
+ class WriteWorkflowPerSideEffectNode(AgentNode):
1328
+ def get_agent_config(self) -> AgentConfig:
1329
+ return AgentConfig(
1330
+ instructions=load_integration_node_instruction("write_workflows"),
1331
+ output_type=WorkflowSetArtifact,
1332
+ )
1333
+
1334
+ async def process(self, task_context: TaskContext) -> TaskContext:
1335
+ # -- checkpoint/resume --
1336
+ prior_inv = _try_load_prior_artifact(task_context, "workflow_inventory.json")
1337
+ if prior_inv is not None:
1338
+ current_dir = _current_dir_from_task(task_context)
1339
+ prior_se_ids = set(str(s) for s in (prior_inv.get("side_effect_ids") or []))
1340
+ current_se_ids = {
1341
+ str(item.get("id") or "")
1342
+ for item in (task_context.metadata.get("side_effects_artifact") or {}).get("side_effects") or []
1343
+ if str(item.get("id") or "")
1344
+ }
1345
+ prior_workflow_files = [current_dir / f"workflow_{_slug(str(se_id))}.json" for se_id in prior_se_ids]
1346
+ if prior_se_ids == current_se_ids and all(f.exists() for f in prior_workflow_files):
1347
+ workflows = []
1348
+ workflow_paths: dict[str, str] = {}
1349
+ for se_id in prior_se_ids:
1350
+ wf_path = current_dir / f"workflow_{_slug(str(se_id))}.json"
1351
+ wf = json.loads(wf_path.read_text(encoding="utf-8"))
1352
+ workflows.append(wf)
1353
+ workflow_paths[f"workflow_{_slug(str(se_id))}"] = str(wf_path)
1354
+ task_context.metadata["workflows"] = workflows
1355
+ task_context.metadata["artifacts"]["workflow_inventory"] = str(current_dir / "workflow_inventory.json")
1356
+ task_context.metadata["artifacts"].update(workflow_paths)
1357
+ self.save_output({"resumed_from": str(current_dir), "skipped_llm": True, "workflow_count": len(workflows)})
1358
+ return task_context
1359
+ _abort_resume(task_context)
1360
+ # -- end checkpoint/resume --
1361
+
1362
+ _integration_dfs_progress(task_context, "Writing workflows")
1363
+
1364
+ event = task_context.event
1365
+ repo_root = Path(event.repo_root)
1366
+ pipeline_dir = Path(task_context.metadata["pipeline_dir"])
1367
+ _invalidate_integration_current_from_stage(current_dir=_current_dir_from_task(task_context), stage="write_workflows")
1368
+ artifact, envelope = integration_agentic.run_integration_agent_step(
1369
+ repo_root=repo_root,
1370
+ stage_name="write_workflows",
1371
+ output_model=WorkflowSetArtifact,
1372
+ context_payload={
1373
+ "idea_id": event.idea_id,
1374
+ "side_effects_artifact": task_context.metadata["side_effects_artifact"],
1375
+ "implicated_users_artifact": task_context.metadata["implicated_users_artifact"],
1376
+ "implemented_stories": event.implemented_stories,
1377
+ "code_evidence": event.code_evidence,
1378
+ "source_docs": event.source_docs,
1379
+ },
1380
+ guidance=[],
1381
+ timeout_seconds=_integration_agent_timeout_seconds(),
1382
+ )
1383
+ integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id="write_workflows", envelope=envelope)
1384
+
1385
+ workflows = _ground_workflows_in_side_effects(
1386
+ workflows=[item.model_dump() for item in artifact.workflows],
1387
+ side_effects_artifact=task_context.metadata["side_effects_artifact"],
1388
+ )
1389
+ workflow_inventory = {
1390
+ "idea_id": event.idea_id,
1391
+ "workflow_ids": [workflow["workflow_id"] for workflow in workflows],
1392
+ "side_effect_ids": [item.get("id") for item in task_context.metadata["side_effects_artifact"].get("side_effects") or []],
1393
+ "downstream_stages": [stage.node_id for stage in default_integration_stages()[2:]],
1394
+ }
1395
+ workflow_inventory_path, _ = _write_integration_artifact(
1396
+ repo_root=repo_root,
1397
+ idea_id=event.idea_id,
1398
+ pipeline_dir=pipeline_dir,
1399
+ filename="workflow_inventory.json",
1400
+ data=workflow_inventory,
1401
+ )
1402
+ task_context.metadata["artifacts"]["workflow_inventory"] = str(workflow_inventory_path)
1403
+
1404
+ workflow_paths: dict[str, str] = {}
1405
+ for workflow in workflows:
1406
+ path, _ = _write_integration_artifact(
1407
+ repo_root=repo_root,
1408
+ idea_id=event.idea_id,
1409
+ pipeline_dir=pipeline_dir,
1410
+ filename=f"workflow_{_slug(str(workflow['side_effect']['id']))}.json",
1411
+ data=workflow,
1412
+ )
1413
+ workflow_paths[workflow["workflow_id"]] = str(path)
1414
+ task_context.metadata["workflows"] = workflows
1415
+ task_context.metadata["artifacts"].update(workflow_paths)
1416
+ self.save_output({"workflow_inventory_ref": str(workflow_inventory_path), "workflow_count": len(workflows)})
1417
+ return task_context
1418
+
1419
+
1420
+ class VegNode(Node):
1421
+ async def process(self, task_context: TaskContext) -> TaskContext:
1422
+ prior_gate = _try_load_prior_artifact(
1423
+ task_context,
1424
+ "validation_gate.json",
1425
+ "validation_gate_agentic_review.json",
1426
+ "idea_acceptance_coverage.json",
1427
+ "idea_acceptance_coverage.json",
1428
+ validity_check=lambda d: bool(d.get("passed")) or bool(d.get("structural_only")),
1429
+ )
1430
+ if prior_gate is not None:
1431
+ current_dir = _current_dir_from_task(task_context)
1432
+ review_payload, review_path = _read_resume_artifact(task_context, "validation_gate_agentic_review.json")
1433
+ coverage_payload, coverage_path = _read_resume_artifact(task_context, "idea_acceptance_coverage.json")
1434
+ cluster_payload, cluster_path = _read_resume_artifact(task_context, "validation_repair_clusters.json")
1435
+ task_context.metadata["validation_report"] = prior_gate
1436
+ task_context.metadata["iterations_used"] = prior_gate.get("iterations_used", 1)
1437
+ task_context.metadata["builder_review"] = review_payload or {}
1438
+ task_context.metadata["idea_acceptance_coverage"] = coverage_payload or {}
1439
+ task_context.metadata["workflows"] = _ground_workflows_in_side_effects(
1440
+ workflows=list((review_payload or {}).get("enriched_workflows") or task_context.metadata.get("workflows") or []),
1441
+ side_effects_artifact=task_context.metadata["side_effects_artifact"],
1442
+ )
1443
+ task_context.metadata["artifacts"]["validation_gate"] = str(current_dir / "validation_gate.json")
1444
+ if review_path is not None:
1445
+ task_context.metadata["artifacts"]["validation_gate_agentic_review"] = str(review_path)
1446
+ if coverage_path is not None:
1447
+ task_context.metadata["artifacts"]["idea_acceptance_coverage"] = str(coverage_path)
1448
+ if cluster_payload is not None:
1449
+ task_context.metadata["repair_clusters"] = cluster_payload
1450
+ if cluster_path is not None:
1451
+ task_context.metadata["artifacts"]["validation_repair_clusters"] = str(cluster_path)
1452
+ self.save_output({"resumed_from": str(current_dir), "skipped_llm": True})
1453
+ return task_context
1454
+ expected_fingerprint = str(task_context.metadata.get("resume_fingerprint") or "") or None
1455
+ failed_prior_gate: dict[str, Any] | None = None
1456
+ failed_gate_candidates = [_current_dir_from_task(task_context) / "validation_gate.json"]
1457
+ prior_run_dir = task_context.metadata.get("prior_run_dir")
1458
+ if prior_run_dir:
1459
+ failed_gate_candidates.append(Path(str(prior_run_dir)) / "validation_gate.json")
1460
+ for gate_path in failed_gate_candidates:
1461
+ if not gate_path.exists() or not _resume_dir_matches_fingerprint(run_dir=gate_path.parent, expected_fingerprint=expected_fingerprint):
1462
+ continue
1463
+ try:
1464
+ gate_payload = json.loads(gate_path.read_text(encoding="utf-8"))
1465
+ except Exception:
1466
+ continue
1467
+ if isinstance(gate_payload, dict) and not bool(gate_payload.get("passed")) and not bool(gate_payload.get("structural_only")):
1468
+ failed_prior_gate = gate_payload
1469
+ break
1470
+ if failed_prior_gate is not None:
1471
+ task_context.metadata["validation_report"] = failed_prior_gate
1472
+ _abort_resume(task_context)
1473
+ self.save_output({"resume_aborted": True, "reason": "prior veg gate failed"})
1474
+ return task_context
1475
+ _abort_resume(task_context)
1476
+
1477
+ _integration_dfs_progress(task_context, "VEG first pass")
1478
+ event = task_context.event
1479
+ repo_root = Path(event.repo_root)
1480
+ pipeline_dir = Path(task_context.metadata["pipeline_dir"])
1481
+ _invalidate_integration_current_from_stage(current_dir=_current_dir_from_task(task_context), stage="validate")
1482
+
1483
+ builder_artifact = _run_veg_idea_acceptance_builder(
1484
+ repo_root=repo_root,
1485
+ pipeline_dir=pipeline_dir,
1486
+ idea_id=event.idea_id,
1487
+ implemented_idea=event.implemented_idea,
1488
+ implemented_stories=event.implemented_stories,
1489
+ side_effects_artifact=task_context.metadata["side_effects_artifact"],
1490
+ implicated_users_artifact=task_context.metadata["implicated_users_artifact"],
1491
+ workflows=task_context.metadata["workflows"],
1492
+ code_evidence=event.code_evidence,
1493
+ source_docs=event.source_docs,
1494
+ node_id="veg_builder_first_pass",
1495
+ )
1496
+ current_agentic_review = builder_artifact.model_dump()
1497
+ enriched_workflows = _ground_workflows_in_side_effects(
1498
+ workflows=[item.model_dump() for item in builder_artifact.enriched_workflows],
1499
+ side_effects_artifact=task_context.metadata["side_effects_artifact"],
1500
+ )
1501
+ current_idea_acceptance_coverage = builder_artifact.idea_acceptance_coverage.model_dump()
1502
+
1503
+ total_interaction_points = sum(
1504
+ len(workflow.get("interaction_points") or [])
1505
+ for workflow in enriched_workflows
1506
+ )
1507
+ if total_interaction_points == 0:
1508
+ _integration_dfs_progress(task_context, "VEG structural only")
1509
+ workflow_count = len(enriched_workflows)
1510
+ review_path, _ = _write_integration_artifact(
1511
+ repo_root=repo_root,
1512
+ idea_id=event.idea_id,
1513
+ pipeline_dir=pipeline_dir,
1514
+ filename="validation_gate_agentic_review.json",
1515
+ data=current_agentic_review,
1516
+ )
1517
+ coverage_path, _ = _write_integration_artifact(
1518
+ repo_root=repo_root,
1519
+ idea_id=event.idea_id,
1520
+ pipeline_dir=pipeline_dir,
1521
+ filename="idea_acceptance_coverage.json",
1522
+ data=current_idea_acceptance_coverage,
1523
+ )
1524
+ validation_report = {
1525
+ "idea_id": event.idea_id,
1526
+ "passed": True,
1527
+ "structural_only": True,
1528
+ "reason": "no actual interaction points across workflows",
1529
+ "workflow_count": workflow_count,
1530
+ "interaction_point_count": 0,
1531
+ "agentic_review": {
1532
+ "summary": current_agentic_review.get("summary"),
1533
+ "findings": list(current_agentic_review.get("findings") or []),
1534
+ "blocking_findings": [
1535
+ finding
1536
+ for finding in list(current_agentic_review.get("findings") or [])
1537
+ if bool(finding.get("blocking")) or str(finding.get("severity") or "").lower() in {"error", "blocking", "critical"}
1538
+ ],
1539
+ "enriched_workflow_count": len(current_agentic_review.get("enriched_workflows") or enriched_workflows),
1540
+ },
1541
+ "idea_acceptance_coverage": current_idea_acceptance_coverage,
1542
+ "repair_cycles": 0,
1543
+ "repair_patches_count": 0,
1544
+ }
1545
+ cluster_payload = {
1546
+ "idea_id": event.idea_id,
1547
+ "mode": "internal_sequential_clusters",
1548
+ "structural_only": True,
1549
+ "iterations": [],
1550
+ "latest_iteration": None,
1551
+ "unresolved_cluster_ids": [],
1552
+ }
1553
+ cluster_path, _ = _write_integration_artifact(
1554
+ repo_root=repo_root,
1555
+ idea_id=event.idea_id,
1556
+ pipeline_dir=pipeline_dir,
1557
+ filename="validation_repair_clusters.json",
1558
+ data=cluster_payload,
1559
+ )
1560
+ validation_path, _ = _write_integration_artifact(
1561
+ repo_root=repo_root,
1562
+ idea_id=event.idea_id,
1563
+ pipeline_dir=pipeline_dir,
1564
+ filename="validation_gate.json",
1565
+ data=validation_report,
1566
+ )
1567
+ task_context.metadata["structural_only"] = True
1568
+ task_context.metadata["iterations_used"] = 0
1569
+ task_context.metadata["validation_report"] = validation_report
1570
+ task_context.metadata["builder_review"] = current_agentic_review
1571
+ task_context.metadata["idea_acceptance_coverage"] = current_idea_acceptance_coverage
1572
+ task_context.metadata["repair_clusters"] = cluster_payload
1573
+ task_context.metadata["workflows"] = enriched_workflows
1574
+ task_context.metadata["artifacts"]["validation_gate"] = str(validation_path)
1575
+ task_context.metadata["artifacts"]["validation_gate_agentic_review"] = str(review_path)
1576
+ task_context.metadata["artifacts"]["idea_acceptance_coverage"] = str(coverage_path)
1577
+ task_context.metadata["artifacts"]["validation_repair_clusters"] = str(cluster_path)
1578
+ self.save_output({
1579
+ "structural_only": True,
1580
+ "workflow_count": workflow_count,
1581
+ "interaction_point_count": 0,
1582
+ })
1583
+ task_context.stop_workflow()
1584
+ return task_context
1585
+
1586
+ _integration_dfs_progress(task_context, "VEG deterministic gate")
1587
+ report, iterations_used = _validate_workflows(
1588
+ side_effects_artifact=task_context.metadata["side_effects_artifact"],
1589
+ implicated_users_artifact=task_context.metadata["implicated_users_artifact"],
1590
+ workflows=enriched_workflows,
1591
+ implemented_idea=event.implemented_idea,
1592
+ implemented_stories=event.implemented_stories,
1593
+ idea_acceptance_coverage=current_idea_acceptance_coverage,
1594
+ max_iterations=5,
1595
+ agentic_review=current_agentic_review,
1596
+ repo_root=repo_root,
1597
+ idea_id=event.idea_id,
1598
+ pipeline_dir=pipeline_dir,
1599
+ repair_node_id_prefix="veg",
1600
+ code_evidence=event.code_evidence,
1601
+ source_docs=event.source_docs,
1602
+ )
1603
+
1604
+ latest_repair_clusters = dict(report.pop("_repair_clusters", {}) or {})
1605
+ latest_review = dict(report.pop("_full_agentic_review", current_agentic_review) or {})
1606
+ latest_workflows = _ground_workflows_in_side_effects(
1607
+ workflows=list(report.pop("_final_workflows", enriched_workflows) or enriched_workflows),
1608
+ side_effects_artifact=task_context.metadata["side_effects_artifact"],
1609
+ )
1610
+ latest_coverage = dict(report.get("idea_acceptance_coverage") or current_idea_acceptance_coverage)
1611
+ repair_cycles = max(0, int(iterations_used) - 1)
1612
+ report["repair_cycles"] = repair_cycles
1613
+ report["repair_patches_count"] = 0
1614
+
1615
+ review_path, _ = _write_integration_artifact(
1616
+ repo_root=repo_root,
1617
+ idea_id=event.idea_id,
1618
+ pipeline_dir=pipeline_dir,
1619
+ filename="validation_gate_agentic_review.json",
1620
+ data=latest_review,
1621
+ )
1622
+ coverage_path, _ = _write_integration_artifact(
1623
+ repo_root=repo_root,
1624
+ idea_id=event.idea_id,
1625
+ pipeline_dir=pipeline_dir,
1626
+ filename="idea_acceptance_coverage.json",
1627
+ data=latest_coverage,
1628
+ )
1629
+ cluster_path, _ = _write_integration_artifact(
1630
+ repo_root=repo_root,
1631
+ idea_id=event.idea_id,
1632
+ pipeline_dir=pipeline_dir,
1633
+ filename="validation_repair_clusters.json",
1634
+ data=latest_repair_clusters,
1635
+ )
1636
+ validation_path, _ = _write_integration_artifact(
1637
+ repo_root=repo_root,
1638
+ idea_id=event.idea_id,
1639
+ pipeline_dir=pipeline_dir,
1640
+ filename="validation_gate.json",
1641
+ data=report,
1642
+ )
1643
+ task_context.metadata["artifacts"]["validation_gate"] = str(validation_path)
1644
+ task_context.metadata["artifacts"]["validation_gate_agentic_review"] = str(review_path)
1645
+ task_context.metadata["artifacts"]["idea_acceptance_coverage"] = str(coverage_path)
1646
+ task_context.metadata["artifacts"]["validation_repair_clusters"] = str(cluster_path)
1647
+ task_context.metadata["validation_report"] = report
1648
+ task_context.metadata["builder_review"] = latest_review
1649
+ task_context.metadata["idea_acceptance_coverage"] = latest_coverage
1650
+ task_context.metadata["repair_clusters"] = latest_repair_clusters
1651
+ task_context.metadata["workflows"] = latest_workflows
1652
+ task_context.metadata["iterations_used"] = iterations_used
1653
+ self.save_output(report)
1654
+ if not bool(report.get("passed")):
1655
+ task_context.stop_workflow()
1656
+ return task_context
1657
+
1658
+
1659
+ def _canonicalize_red_artifact(*, workflows: list[dict[str, Any]], red_artifact: dict[str, Any]) -> dict[str, Any]:
1660
+ workflow_map = {str(item.get("workflow_id") or ""): item for item in workflows}
1661
+ normalized = json.loads(json.dumps(red_artifact))
1662
+ for package in normalized.get("packages") or []:
1663
+ workflow_id = str(package.get("workflow_id") or "")
1664
+ workflow = workflow_map.get(workflow_id)
1665
+ if workflow is None:
1666
+ continue
1667
+ harness = dict(package.get("harness_selection") or {})
1668
+ interaction_points = list(workflow.get("interaction_points") or [])
1669
+ needed_points = list(workflow.get("needed_interaction_points") or [])
1670
+ primary_point = (interaction_points or needed_points or [{}])[0]
1671
+ expected_harness = _preferred_harness_for_interaction_point(primary_point)
1672
+ if str(harness.get("harness_kind") or "") != expected_harness:
1673
+ harness["harness_kind"] = expected_harness
1674
+ package["harness_selection"] = harness
1675
+ return normalized
1676
+
1677
+
1678
+ def _validate_red_packages(*, workflows: list[dict[str, Any]], red_artifact: dict[str, Any]) -> dict[str, Any]:
1679
+ workflow_map = {str(item.get("workflow_id") or ""): item for item in workflows}
1680
+ errors: list[str] = []
1681
+ for package in red_artifact.get("packages") or []:
1682
+ workflow_id = str(package.get("workflow_id") or "")
1683
+ workflow = workflow_map.get(workflow_id)
1684
+ if workflow is None:
1685
+ errors.append(f"red package references unknown workflow {workflow_id}")
1686
+ continue
1687
+ harness = dict(package.get("harness_selection") or {})
1688
+ actual_harness = str(harness.get("harness_kind") or "")
1689
+ interaction_points = list(workflow.get("interaction_points") or [])
1690
+ needed_points = list(workflow.get("needed_interaction_points") or [])
1691
+ primary_point = (interaction_points or needed_points or [{}])[0]
1692
+ expected_harness = _preferred_harness_for_interaction_point(primary_point)
1693
+ if not interaction_points and not needed_points and actual_harness == "artifact_probe":
1694
+ expected_harness = "artifact_probe"
1695
+ if actual_harness != expected_harness:
1696
+ errors.append(f"red harness mismatch for {workflow_id}: expected {expected_harness}")
1697
+ if not (package.get("expected_assertions") or []):
1698
+ errors.append(f"red expected assertions missing for {workflow_id}")
1699
+ if not interaction_points and needed_points and not bool(harness.get("gap_explicit")):
1700
+ errors.append(f"red gap not explicit for {workflow_id}")
1701
+ return {"passed": not errors, "errors": errors}
1702
+
1703
+
1704
+ class RedNode(AgentNode):
1705
+ def get_agent_config(self) -> AgentConfig:
1706
+ return AgentConfig(
1707
+ instructions=load_integration_node_instruction("red")
1708
+ )
1709
+
1710
+ async def process(self, task_context: TaskContext) -> TaskContext:
1711
+ # -- checkpoint/resume --
1712
+ prior_red = _try_load_prior_artifact(task_context, "red_package.json", "red_validation.json")
1713
+ if prior_red is not None:
1714
+ current_dir = _current_dir_from_task(task_context)
1715
+ prior_red_val, _ = _read_resume_artifact(task_context, "red_validation.json")
1716
+ if (prior_red_val or {}).get("passed"):
1717
+ task_context.metadata["red_artifact"] = prior_red
1718
+ task_context.metadata["artifacts"].update({
1719
+ "red_package": str(current_dir / "red_package.json"),
1720
+ "red_validation": str(current_dir / "red_validation.json"),
1721
+ })
1722
+ self.save_output({"resumed_from": str(current_dir), "skipped_llm": True})
1723
+ return task_context
1724
+ _abort_resume(task_context)
1725
+ # -- end checkpoint/resume --
1726
+
1727
+ _integration_dfs_progress(task_context, "Generating tests")
1728
+
1729
+ event = task_context.event
1730
+ repo_root = Path(event.repo_root)
1731
+ pipeline_dir = Path(task_context.metadata["pipeline_dir"])
1732
+ _invalidate_integration_current_from_stage(current_dir=_current_dir_from_task(task_context), stage="red")
1733
+
1734
+ # Step 1: Run agent to get red artifact
1735
+ artifact, envelope = integration_agentic.run_integration_agent_step(
1736
+ repo_root=repo_root,
1737
+ stage_name="red",
1738
+ output_model=RedArtifact,
1739
+ context_payload={
1740
+ "idea_id": event.idea_id,
1741
+ "validated_workflows": task_context.metadata["workflows"],
1742
+ "validation_report": task_context.metadata["validation_report"],
1743
+ "side_effects_artifact": task_context.metadata["side_effects_artifact"],
1744
+ "implicated_users_artifact": task_context.metadata["implicated_users_artifact"],
1745
+ "code_evidence": event.code_evidence,
1746
+ },
1747
+ guidance=[],
1748
+ timeout_seconds=_integration_agent_timeout_seconds(),
1749
+ )
1750
+ integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id="red", envelope=envelope)
1751
+
1752
+ # Step 2: Normalize and validate red packages
1753
+ red_data = _canonicalize_red_artifact(
1754
+ workflows=task_context.metadata["workflows"],
1755
+ red_artifact=artifact.model_dump(),
1756
+ )
1757
+ report = _validate_red_packages(workflows=task_context.metadata["workflows"], red_artifact=red_data)
1758
+
1759
+ # Step 3: If fails, enter repair loop (up to 2 iterations)
1760
+ if not report["passed"]:
1761
+ max_repair = 2
1762
+ for repair_iter in range(1, max_repair + 1):
1763
+ _integration_dfs_progress(task_context, f"Repair pass {repair_iter}/{max_repair}")
1764
+ # Identify failing packages
1765
+ failing_packages = []
1766
+ for pkg in red_data.get("packages") or []:
1767
+ wf_id = str(pkg.get("workflow_id") or "")
1768
+ if any(wf_id in err for err in report.get("errors") or []):
1769
+ failing_packages.append(pkg)
1770
+ if not failing_packages:
1771
+ failing_packages = red_data.get("packages") or []
1772
+
1773
+ failing_workflows = [
1774
+ wf for wf in task_context.metadata["workflows"]
1775
+ if str(wf.get("workflow_id") or "") in {str(p.get("workflow_id") or "") for p in failing_packages}
1776
+ ]
1777
+
1778
+ # Call repair agent
1779
+ repair_artifact, repair_envelope = integration_agentic.run_integration_code_repair_step(
1780
+ repo_root=repo_root,
1781
+ output_model=CodeRepairArtifact,
1782
+ context_payload={
1783
+ "failing_packages": failing_packages,
1784
+ "validation_errors": report.get("errors") or [],
1785
+ "failing_workflows": failing_workflows,
1786
+ "side_effects_artifact": task_context.metadata["side_effects_artifact"],
1787
+ "implicated_users_artifact": task_context.metadata["implicated_users_artifact"],
1788
+ "code_evidence": event.code_evidence,
1789
+ },
1790
+ iteration=repair_iter,
1791
+ timeout_seconds=_integration_agent_timeout_seconds(),
1792
+ )
1793
+ integration_agentic.persist_agent_run(
1794
+ pipeline_root=pipeline_dir,
1795
+ node_id=f"red_code_repair_iter{repair_iter}",
1796
+ envelope=repair_envelope,
1797
+ )
1798
+
1799
+ # Apply file patches
1800
+ for patch in repair_artifact.patches:
1801
+ patch_path = repo_root / patch.file_path
1802
+ patch_path.parent.mkdir(parents=True, exist_ok=True)
1803
+ patch_path.write_text(patch.content, encoding="utf-8")
1804
+
1805
+ # Re-run agent step for failing workflows to re-generate their red packages
1806
+ _integration_dfs_progress(task_context, f"Re-running tests {repair_iter}/{max_repair}")
1807
+ re_artifact, re_envelope = integration_agentic.run_integration_agent_step(
1808
+ repo_root=repo_root,
1809
+ stage_name=f"red_repair_iter{repair_iter}",
1810
+ output_model=RedArtifact,
1811
+ context_payload={
1812
+ "idea_id": event.idea_id,
1813
+ "validated_workflows": failing_workflows,
1814
+ "validation_report": task_context.metadata["validation_report"],
1815
+ "side_effects_artifact": task_context.metadata["side_effects_artifact"],
1816
+ "implicated_users_artifact": task_context.metadata["implicated_users_artifact"],
1817
+ "code_evidence": event.code_evidence,
1818
+ },
1819
+ guidance=[],
1820
+ timeout_seconds=_integration_agent_timeout_seconds(),
1821
+ )
1822
+
1823
+ # Merge re-generated packages back into red_data
1824
+ re_normalized = _canonicalize_red_artifact(
1825
+ workflows=failing_workflows,
1826
+ red_artifact=re_artifact.model_dump(),
1827
+ )
1828
+ re_pkg_map = {str(p.get("workflow_id") or ""): p for p in re_normalized.get("packages") or []}
1829
+ new_packages = []
1830
+ for pkg in red_data.get("packages") or []:
1831
+ wf_id = str(pkg.get("workflow_id") or "")
1832
+ new_packages.append(re_pkg_map.pop(wf_id, pkg))
1833
+ for leftover in re_pkg_map.values():
1834
+ new_packages.append(leftover)
1835
+ red_data["packages"] = new_packages
1836
+
1837
+ # Re-validate
1838
+ report = _validate_red_packages(workflows=task_context.metadata["workflows"], red_artifact=red_data)
1839
+ if report["passed"]:
1840
+ break
1841
+
1842
+ # Step 4: Write artifacts
1843
+ if not report["passed"]:
1844
+ task_context.stop_workflow()
1845
+ red_path, _ = _write_integration_artifact(
1846
+ repo_root=repo_root,
1847
+ idea_id=event.idea_id,
1848
+ pipeline_dir=pipeline_dir,
1849
+ filename="red_package.json",
1850
+ data=red_data,
1851
+ )
1852
+ red_validation_path, _ = _write_integration_artifact(
1853
+ repo_root=repo_root,
1854
+ idea_id=event.idea_id,
1855
+ pipeline_dir=pipeline_dir,
1856
+ filename="red_validation.json",
1857
+ data=report,
1858
+ )
1859
+ task_context.metadata["red_artifact"] = red_data
1860
+ task_context.metadata["artifacts"].update({
1861
+ "red_package": str(red_path),
1862
+ "red_validation": str(red_validation_path),
1863
+ })
1864
+ self.save_output({"red_package_ref": str(red_path), "validation": report})
1865
+ return task_context
1866
+
1867
+
1868
+ class RedReviewNode(AgentNode):
1869
+ def get_agent_config(self) -> AgentConfig:
1870
+ return AgentConfig(
1871
+ instructions=load_integration_node_instruction("red_review")
1872
+ )
1873
+
1874
+ async def process(self, task_context: TaskContext) -> TaskContext:
1875
+ # -- checkpoint/resume --
1876
+ prior_rr = _try_load_prior_artifact(
1877
+ task_context, "red_review.json",
1878
+ validity_check=lambda d: bool(d.get("agentic_review_clear")),
1879
+ )
1880
+ if prior_rr is not None:
1881
+ current_dir = _current_dir_from_task(task_context)
1882
+ task_context.metadata["artifacts"]["red_review"] = str(current_dir / "red_review.json")
1883
+ self.save_output({"resumed_from": str(current_dir), "skipped_llm": True})
1884
+ return task_context
1885
+ _abort_resume(task_context)
1886
+ # -- end checkpoint/resume --
1887
+
1888
+ _integration_dfs_progress(task_context, "Reviewing tests")
1889
+
1890
+ event = task_context.event
1891
+ repo_root = Path(event.repo_root)
1892
+ pipeline_dir = Path(task_context.metadata["pipeline_dir"])
1893
+ _invalidate_integration_current_from_stage(current_dir=_current_dir_from_task(task_context), stage="red_review")
1894
+ artifact, envelope = integration_agentic.run_integration_agent_step(
1895
+ repo_root=repo_root,
1896
+ stage_name="red_review",
1897
+ output_model=RedReviewArtifact,
1898
+ context_payload={
1899
+ "idea_id": event.idea_id,
1900
+ "validated_workflows": task_context.metadata["workflows"],
1901
+ "red_artifact": task_context.metadata["red_artifact"],
1902
+ "validation_report": task_context.metadata["validation_report"],
1903
+ },
1904
+ guidance=[],
1905
+ timeout_seconds=_integration_agent_timeout_seconds(),
1906
+ )
1907
+ integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id="redreview", envelope=envelope)
1908
+ path, _ = _write_integration_artifact(
1909
+ repo_root=repo_root,
1910
+ idea_id=event.idea_id,
1911
+ pipeline_dir=pipeline_dir,
1912
+ filename="red_review.json",
1913
+ data=artifact.model_dump(),
1914
+ )
1915
+ task_context.metadata["red_review_artifact"] = artifact.model_dump()
1916
+ task_context.metadata["artifacts"]["red_review"] = str(path)
1917
+ self.save_output({"red_review_ref": str(path)})
1918
+ return task_context
1919
+
1920
+
1921
+ class GreenNode(AgentNode):
1922
+ def get_agent_config(self) -> AgentConfig:
1923
+ return AgentConfig(
1924
+ instructions=load_integration_node_instruction("green")
1925
+ )
1926
+
1927
+ async def process(self, task_context: TaskContext) -> TaskContext:
1928
+ # -- checkpoint/resume --
1929
+ prior_green = _try_load_prior_artifact(task_context, "green_package.json")
1930
+ if prior_green is not None:
1931
+ current_dir = _current_dir_from_task(task_context)
1932
+ task_context.metadata["artifacts"]["green_package"] = str(current_dir / "green_package.json")
1933
+ self.save_output({"resumed_from": str(current_dir), "skipped_llm": True})
1934
+ return task_context
1935
+ _abort_resume(task_context)
1936
+ # -- end checkpoint/resume --
1937
+
1938
+ _integration_dfs_progress(task_context, "Writing green tests")
1939
+
1940
+ event = task_context.event
1941
+ repo_root = Path(event.repo_root)
1942
+ pipeline_dir = Path(task_context.metadata["pipeline_dir"])
1943
+ _invalidate_integration_current_from_stage(current_dir=_current_dir_from_task(task_context), stage="green")
1944
+ artifact, envelope = integration_agentic.run_integration_agent_step(
1945
+ repo_root=repo_root,
1946
+ stage_name="green",
1947
+ output_model=GreenArtifact,
1948
+ context_payload={
1949
+ "idea_id": event.idea_id,
1950
+ "validated_workflows": task_context.metadata["workflows"],
1951
+ "red_review_artifact": task_context.metadata["red_review_artifact"],
1952
+ "code_evidence": event.code_evidence,
1953
+ "implemented_stories": event.implemented_stories,
1954
+ },
1955
+ guidance=[],
1956
+ timeout_seconds=_integration_agent_timeout_seconds(),
1957
+ )
1958
+ integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id="green", envelope=envelope)
1959
+ path, _ = _write_integration_artifact(
1960
+ repo_root=repo_root,
1961
+ idea_id=event.idea_id,
1962
+ pipeline_dir=pipeline_dir,
1963
+ filename="green_package.json",
1964
+ data=artifact.model_dump(),
1965
+ )
1966
+ task_context.metadata["green_artifact"] = artifact.model_dump()
1967
+ task_context.metadata["artifacts"]["green_package"] = str(path)
1968
+ self.save_output({"green_package_ref": str(path)})
1969
+ return task_context
1970
+
1971
+
1972
+ class GreenEnrichNode(AgentNode):
1973
+ def get_agent_config(self) -> AgentConfig:
1974
+ return AgentConfig(
1975
+ instructions=load_integration_node_instruction("green_enrich")
1976
+ )
1977
+
1978
+ async def process(self, task_context: TaskContext) -> TaskContext:
1979
+ # -- checkpoint/resume --
1980
+ prior_ge = _try_load_prior_artifact(task_context, "green_enrich.json")
1981
+ if prior_ge is not None:
1982
+ current_dir = _current_dir_from_task(task_context)
1983
+ task_context.metadata["artifacts"]["green_enrich"] = str(current_dir / "green_enrich.json")
1984
+ self.save_output({"resumed_from": str(current_dir), "skipped_llm": True})
1985
+ return task_context
1986
+ _abort_resume(task_context)
1987
+ # -- end checkpoint/resume --
1988
+
1989
+ _integration_dfs_progress(task_context, "Enriching tests")
1990
+
1991
+ event = task_context.event
1992
+ repo_root = Path(event.repo_root)
1993
+ pipeline_dir = Path(task_context.metadata["pipeline_dir"])
1994
+ _invalidate_integration_current_from_stage(current_dir=_current_dir_from_task(task_context), stage="green_enrich")
1995
+ artifact, envelope = integration_agentic.run_integration_agent_step(
1996
+ repo_root=repo_root,
1997
+ stage_name="green_enrich",
1998
+ output_model=GreenEnrichArtifact,
1999
+ context_payload={
2000
+ "idea_id": event.idea_id,
2001
+ "validated_workflows": task_context.metadata["workflows"],
2002
+ "green_artifact": task_context.metadata["green_artifact"],
2003
+ "red_review_artifact": task_context.metadata["red_review_artifact"],
2004
+ },
2005
+ guidance=[],
2006
+ timeout_seconds=_integration_agent_timeout_seconds(),
2007
+ )
2008
+ integration_agentic.persist_agent_run(pipeline_root=pipeline_dir, node_id="greenenrich", envelope=envelope)
2009
+ path, _ = _write_integration_artifact(
2010
+ repo_root=repo_root,
2011
+ idea_id=event.idea_id,
2012
+ pipeline_dir=pipeline_dir,
2013
+ filename="green_enrich.json",
2014
+ data=artifact.model_dump(),
2015
+ )
2016
+ task_context.metadata["green_enrich_artifact"] = artifact.model_dump()
2017
+ task_context.metadata["artifacts"]["green_enrich"] = str(path)
2018
+ self.save_output({"green_enrich_ref": str(path)})
2019
+ return task_context
2020
+
2021
+
2022
+ class CommitNode(Node):
2023
+ async def process(self, task_context: TaskContext) -> TaskContext:
2024
+ _integration_dfs_progress(task_context, "Committing")
2025
+ event = task_context.event
2026
+ pipeline_dir = Path(task_context.metadata["pipeline_dir"])
2027
+ workflows = list(task_context.metadata.get("workflows") or [])
2028
+ green_packages = {
2029
+ str(item.get("workflow_id") or ""): item
2030
+ for item in (task_context.metadata.get("green_artifact") or {}).get("packages") or []
2031
+ }
2032
+ enrich_packages = {
2033
+ str(item.get("workflow_id") or ""): item
2034
+ for item in (task_context.metadata.get("green_enrich_artifact") or {}).get("packages") or []
2035
+ }
2036
+ playwright_request = _build_post_integration_playwright_request(
2037
+ idea_id=event.idea_id,
2038
+ trigger_run_id=str(task_context.metadata.get("dag_run_id") or event.dag_run_id or "") or None,
2039
+ workflows=workflows,
2040
+ red_artifact=dict(task_context.metadata.get("red_artifact") or {}),
2041
+ )
2042
+ request_path, _ = _write_integration_artifact(
2043
+ repo_root=Path(event.repo_root),
2044
+ idea_id=event.idea_id,
2045
+ pipeline_dir=pipeline_dir,
2046
+ filename="post_integration_playwright_request.json",
2047
+ data=playwright_request,
2048
+ )
2049
+ task_context.metadata["post_integration_playwright_request"] = playwright_request
2050
+ task_context.metadata["artifacts"]["post_integration_playwright_request"] = str(request_path)
2051
+ commit_packages: list[CommitWorkflowPackage] = []
2052
+ for workflow in workflows:
2053
+ workflow_id = str(workflow.get("workflow_id") or "")
2054
+ commit_packages.append(
2055
+ CommitWorkflowPackage(
2056
+ workflow_id=workflow_id,
2057
+ side_effect_id=str(workflow.get("side_effect", {}).get("id") or ""),
2058
+ included_artifacts=[
2059
+ "validation_gate.json",
2060
+ "red_package.json",
2061
+ "red_review.json",
2062
+ "green_package.json",
2063
+ "green_enrich.json",
2064
+ "post_integration_playwright_request.json",
2065
+ ],
2066
+ traceability={
2067
+ "idea_id": event.idea_id,
2068
+ "workflow_ref": workflow_id,
2069
+ "side_effect_ref": workflow.get("side_effect", {}).get("id"),
2070
+ "story_backing": workflow.get("story_backing") or [],
2071
+ "green_changes": (green_packages.get(workflow_id) or {}).get("implementation_changes") or [],
2072
+ "green_enrich_assertions": (
2073
+ enrich_packages.get(workflow_id) or {}
2074
+ ).get("strengthened_assertions") or [],
2075
+ },
2076
+ )
2077
+ )
2078
+ artifact = CommitArtifact(
2079
+ idea_id=event.idea_id,
2080
+ summary="Integration-proof package assembled and traceably linked.",
2081
+ packages=commit_packages,
2082
+ package_manifest={
2083
+ "idea_id": event.idea_id,
2084
+ "pipeline_dir": str(pipeline_dir),
2085
+ "workflow_ids": [item.workflow_id for item in commit_packages],
2086
+ "artifact_refs": dict(task_context.metadata.get("artifacts") or {}),
2087
+ "post_integration_playwright": {
2088
+ "status": playwright_request["status"],
2089
+ "required_workflow_ids": playwright_request["required_workflow_ids"],
2090
+ "request_ref": str(request_path),
2091
+ },
2092
+ },
2093
+ )
2094
+ path, _ = _write_integration_artifact(
2095
+ repo_root=Path(event.repo_root),
2096
+ idea_id=event.idea_id,
2097
+ pipeline_dir=pipeline_dir,
2098
+ filename="commit_package.json",
2099
+ data=artifact.model_dump(),
2100
+ )
2101
+ task_context.metadata["commit_artifact"] = artifact.model_dump()
2102
+ task_context.metadata["artifacts"]["commit_package"] = str(path)
2103
+ self.save_output({"commit_package_ref": str(path), "workflow_count": len(commit_packages)})
2104
+ return task_context
2105
+
2106
+
2107
+ class IntegrationWorkflow(Workflow):
2108
+ workflow_schema = WorkflowSchema(
2109
+ description="Integration DAG (agentic side-effect resolution -> workflow writing -> single hybrid VEG node with internal deterministic gate and repair loop -> real downstream red/redreview/green/greenenrich/commit chain)",
2110
+ event_schema=IntegrationDagEvent,
2111
+ start=LoadIntegrationContextNode,
2112
+ nodes=[
2113
+ NodeConfig(node=LoadIntegrationContextNode, connections=[ResolveSideEffectsAndImplicatedUsersNode]),
2114
+ NodeConfig(node=ResolveSideEffectsAndImplicatedUsersNode, connections=[WriteWorkflowPerSideEffectNode]),
2115
+ NodeConfig(node=WriteWorkflowPerSideEffectNode, connections=[VegNode]),
2116
+ NodeConfig(node=VegNode, connections=[RedNode]),
2117
+ NodeConfig(node=RedNode, connections=[RedReviewNode]),
2118
+ NodeConfig(node=RedReviewNode, connections=[GreenNode]),
2119
+ NodeConfig(node=GreenNode, connections=[GreenEnrichNode]),
2120
+ NodeConfig(node=GreenEnrichNode, connections=[CommitNode]),
2121
+ NodeConfig(node=CommitNode, connections=[]),
2122
+ ],
2123
+ )
2124
+
2125
+
2126
+ def _workflow_story_ids(workflow: dict[str, Any]) -> list[str]:
2127
+ return [
2128
+ str(item.get("story_id") or "").strip()
2129
+ for item in (workflow.get("story_backing") or [])
2130
+ if str(item.get("story_id") or "").strip()
2131
+ ]
2132
+
2133
+
2134
+ def _extract_acceptance_criteria(value: Any) -> list[str]:
2135
+ if isinstance(value, list):
2136
+ return [str(item).strip() for item in value if str(item).strip()]
2137
+ text = str(value or "").strip()
2138
+ return [text] if text else []
2139
+
2140
+
2141
+ def _story_acceptance_index(implemented_stories: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
2142
+ index: dict[str, dict[str, Any]] = {}
2143
+ for story in implemented_stories:
2144
+ story_id = str(story.get("story_id") or story.get("id") or "").strip()
2145
+ if not story_id:
2146
+ continue
2147
+ criteria = _extract_acceptance_criteria(story.get("acceptance_criteria"))
2148
+ if not criteria:
2149
+ continue
2150
+ index[story_id] = {
2151
+ "story_id": story_id,
2152
+ "title": str(story.get("title") or story.get("name") or story_id).strip(),
2153
+ "acceptance_criteria": criteria,
2154
+ "side_effect_ids": [str(item).strip() for item in (story.get("side_effect_ids") or []) if str(item).strip()],
2155
+ }
2156
+ return index
2157
+
2158
+
2159
+ def _idea_acceptance_criteria(implemented_idea: dict[str, Any] | None) -> list[str]:
2160
+ if not isinstance(implemented_idea, dict):
2161
+ return []
2162
+ idea_block = implemented_idea.get("idea") if isinstance(implemented_idea.get("idea"), dict) else implemented_idea
2163
+ return _extract_acceptance_criteria((idea_block or {}).get("acceptance_criteria"))
2164
+
2165
+
2166
+ def _determine_failing_workflow_ids(
2167
+ *,
2168
+ workflows: list[dict[str, Any]],
2169
+ errors: list[str],
2170
+ blocking_agentic_findings: list[dict[str, Any]],
2171
+ repair_sources: list[dict[str, Any]] | None = None,
2172
+ ) -> set[str]:
2173
+ workflow_ids = {str(workflow.get("workflow_id") or "") for workflow in workflows if str(workflow.get("workflow_id") or "")}
2174
+ side_effect_to_workflow_ids: dict[str, set[str]] = {}
2175
+ story_to_workflow_ids: dict[str, set[str]] = {}
2176
+ for workflow in workflows:
2177
+ workflow_id = str(workflow.get("workflow_id") or "")
2178
+ if not workflow_id:
2179
+ continue
2180
+ side_effect_id = str(workflow.get("side_effect", {}).get("id") or "")
2181
+ if side_effect_id:
2182
+ side_effect_to_workflow_ids.setdefault(side_effect_id, set()).add(workflow_id)
2183
+ for story_id in _workflow_story_ids(workflow):
2184
+ story_to_workflow_ids.setdefault(story_id, set()).add(workflow_id)
2185
+
2186
+ failing: set[str] = set()
2187
+ for error in errors:
2188
+ for workflow_id in workflow_ids:
2189
+ if workflow_id and workflow_id in error:
2190
+ failing.add(workflow_id)
2191
+ for side_effect_id, mapped_ids in side_effect_to_workflow_ids.items():
2192
+ if side_effect_id and side_effect_id in error:
2193
+ failing.update(mapped_ids)
2194
+ for story_id, mapped_ids in story_to_workflow_ids.items():
2195
+ if story_id and story_id in error:
2196
+ failing.update(mapped_ids)
2197
+ for finding in blocking_agentic_findings:
2198
+ workflow_id = str(finding.get("workflow_id") or "").strip()
2199
+ if workflow_id:
2200
+ failing.add(workflow_id)
2201
+ for source in repair_sources or []:
2202
+ for workflow_id in source.get("workflow_ids") or []:
2203
+ workflow_id = str(workflow_id or "").strip()
2204
+ if workflow_id:
2205
+ failing.add(workflow_id)
2206
+ return failing
2207
+
2208
+
2209
+ def _idea_acceptance_lineage_is_model_backed(lineage: Any) -> bool:
2210
+ if not isinstance(lineage, dict):
2211
+ return False
2212
+ if bool(lineage.get("model_backed")):
2213
+ return True
2214
+ metadata = lineage.get("metadata")
2215
+ return isinstance(metadata, dict) and bool(metadata.get("model_backed"))
2216
+
2217
+
2218
+ def _normalize_idea_acceptance_coverage(
2219
+ *,
2220
+ idea_acceptance_coverage: dict[str, Any] | None,
2221
+ canonical_criteria: list[str],
2222
+ ) -> dict[str, Any]:
2223
+ coverage = dict(idea_acceptance_coverage or {})
2224
+ raw_entries = [dict(entry) for entry in (coverage.get("criteria") or []) if isinstance(entry, dict)]
2225
+ if not canonical_criteria:
2226
+ coverage["criteria"] = []
2227
+ return coverage
2228
+
2229
+ normalized_entries: list[dict[str, Any]] = []
2230
+ used_positions: set[int] = set()
2231
+ criterion_to_index = {
2232
+ criterion: idx
2233
+ for idx, criterion in enumerate(canonical_criteria, start=1)
2234
+ }
2235
+ for idx, canonical_criterion in enumerate(canonical_criteria, start=1):
2236
+ selected_position: int | None = None
2237
+ fallback_position: int | None = None
2238
+ for position, entry in enumerate(raw_entries):
2239
+ if position in used_positions:
2240
+ continue
2241
+ entry_index = int(entry.get("criterion_index") or 0)
2242
+ entry_criterion = str(entry.get("criterion") or "").strip()
2243
+ if entry_index == idx and entry_criterion == canonical_criterion:
2244
+ selected_position = position
2245
+ break
2246
+ if fallback_position is None and entry_criterion == canonical_criterion:
2247
+ fallback_position = position
2248
+ elif fallback_position is None and entry_index == idx:
2249
+ fallback_position = position
2250
+ if selected_position is None:
2251
+ selected_position = fallback_position
2252
+ if selected_position is None:
2253
+ continue
2254
+ used_positions.add(selected_position)
2255
+ normalized_entry = dict(raw_entries[selected_position])
2256
+ normalized_entry["criterion_index"] = idx
2257
+ normalized_entry["criterion"] = canonical_criterion
2258
+ normalized_entries.append(normalized_entry)
2259
+
2260
+ for position, entry in enumerate(raw_entries):
2261
+ if position in used_positions:
2262
+ continue
2263
+ entry_criterion = str(entry.get("criterion") or "").strip()
2264
+ mapped_index = criterion_to_index.get(entry_criterion)
2265
+ if mapped_index is None:
2266
+ continue
2267
+ if any(int(item.get("criterion_index") or 0) == mapped_index for item in normalized_entries):
2268
+ continue
2269
+ normalized_entry = dict(entry)
2270
+ normalized_entry["criterion_index"] = mapped_index
2271
+ normalized_entry["criterion"] = canonical_criteria[mapped_index - 1]
2272
+ normalized_entries.append(normalized_entry)
2273
+
2274
+ normalized_entries.sort(key=lambda entry: int(entry.get("criterion_index") or 0))
2275
+ coverage["criteria"] = normalized_entries
2276
+ return coverage
2277
+
2278
+
2279
+ def _filter_idea_acceptance_coverage(
2280
+ *,
2281
+ idea_acceptance_coverage: dict[str, Any] | None,
2282
+ criterion_indices: set[int],
2283
+ ) -> dict[str, Any]:
2284
+ filtered = dict(idea_acceptance_coverage or {})
2285
+ filtered["criteria"] = [
2286
+ dict(entry)
2287
+ for entry in (filtered.get("criteria") or [])
2288
+ if int(entry.get("criterion_index") or 0) in criterion_indices
2289
+ ]
2290
+ return filtered
2291
+
2292
+
2293
+ def _validation_convergence_state(
2294
+ *,
2295
+ idea_acceptance_coverage: dict[str, Any] | None,
2296
+ deterministic_errors: list[str],
2297
+ blocking_agentic_findings: list[dict[str, Any]],
2298
+ acceptance_failures: list[dict[str, Any]],
2299
+ idea_acceptance_failures: list[dict[str, Any]],
2300
+ ) -> dict[str, Any]:
2301
+ verdict_map = {
2302
+ int(entry.get("criterion_index") or 0): str(entry.get("verdict") or "").strip().lower()
2303
+ for entry in list((idea_acceptance_coverage or {}).get("criteria") or [])
2304
+ if int(entry.get("criterion_index") or 0) > 0
2305
+ }
2306
+ finding_set = sorted({
2307
+ *[str(error).strip() for error in deterministic_errors if str(error).strip()],
2308
+ *[
2309
+ f"{str(finding.get('workflow_id') or 'global').strip()}::{str(finding.get('summary') or '').strip()}"
2310
+ for finding in blocking_agentic_findings
2311
+ if str(finding.get("summary") or "").strip()
2312
+ ],
2313
+ })
2314
+ missing_seams = sorted({
2315
+ *[
2316
+ f"criterion#{int(entry.get('criterion_index') or 0)}::{seam}"
2317
+ for entry in list((idea_acceptance_coverage or {}).get("criteria") or [])
2318
+ for seam in list(entry.get("missing_seams") or [])
2319
+ if int(entry.get("criterion_index") or 0) > 0 and str(seam).strip()
2320
+ ],
2321
+ *[
2322
+ str(failure.get("message") or "").strip()
2323
+ for failure in acceptance_failures
2324
+ if str(failure.get("source_type") or "").strip() == "acceptance_criterion_missing_seam"
2325
+ and str(failure.get("message") or "").strip()
2326
+ ],
2327
+ *[
2328
+ str(failure.get("message") or "").strip()
2329
+ for failure in idea_acceptance_failures
2330
+ if any("seam" in str(reason or "").lower() for reason in list(failure.get("reasons") or []))
2331
+ and str(failure.get("message") or "").strip()
2332
+ ],
2333
+ })
2334
+ return {
2335
+ "verdict_map": verdict_map,
2336
+ "finding_set": finding_set,
2337
+ "missing_seams": missing_seams,
2338
+ }
2339
+
2340
+
2341
+ def _build_validate_repair_protected_context(
2342
+ *,
2343
+ workflows: list[dict[str, Any]],
2344
+ failing_workflow_ids: set[str],
2345
+ idea_acceptance_coverage: dict[str, Any] | None = None,
2346
+ failing_idea_criterion_indices: set[int] | None = None,
2347
+ ) -> tuple[list[dict[str, Any]], list[str]]:
2348
+ protected_sections: list[dict[str, Any]] = []
2349
+ do_not_touch: list[str] = []
2350
+ failing_idea_criterion_indices = set(failing_idea_criterion_indices or set())
2351
+ for workflow in workflows:
2352
+ workflow_id = str(workflow.get("workflow_id") or "")
2353
+ if not workflow_id:
2354
+ continue
2355
+ side_effect_id = str(workflow.get("side_effect", {}).get("id") or "")
2356
+ if workflow_id not in failing_workflow_ids:
2357
+ protected_sections.append(
2358
+ {
2359
+ "section_type": "workflow",
2360
+ "workflow_id": workflow_id,
2361
+ "side_effect_id": side_effect_id,
2362
+ "reason": "workflow passed deterministic validation; preserve it",
2363
+ "protected_fields": [
2364
+ "workflow_id",
2365
+ "side_effect",
2366
+ "story_backing",
2367
+ "code_backing",
2368
+ "source_doc_backing",
2369
+ "implicated_users",
2370
+ "interaction_points",
2371
+ "needed_interaction_points",
2372
+ "process_sequence",
2373
+ "branches",
2374
+ "resulting_artifacts",
2375
+ ],
2376
+ }
2377
+ )
2378
+ do_not_touch.append(f"Do not modify passing workflow {workflow_id} (side_effect={side_effect_id or 'unknown'}).")
2379
+ continue
2380
+ do_not_touch.extend(
2381
+ [
2382
+ f"Within failing workflow {workflow_id}, preserve workflow_id and side_effect anchoring.",
2383
+ f"Within failing workflow {workflow_id}, preserve already-supported story_backing/code_backing/source_doc_backing unless the repair source explicitly says they are wrong.",
2384
+ ]
2385
+ )
2386
+ coverage_entries = list((idea_acceptance_coverage or {}).get("criteria") or [])
2387
+ for entry in coverage_entries:
2388
+ criterion_index = int(entry.get("criterion_index") or 0)
2389
+ criterion = str(entry.get("criterion") or "").strip()
2390
+ if criterion_index and criterion_index not in failing_idea_criterion_indices:
2391
+ protected_sections.append(
2392
+ {
2393
+ "section_type": "idea_acceptance_criterion",
2394
+ "criterion_index": criterion_index,
2395
+ "criterion": criterion,
2396
+ "reason": "idea acceptance criterion already proven; preserve its proof mapping",
2397
+ "protected_fields": [
2398
+ "criterion_index",
2399
+ "criterion",
2400
+ "verdict",
2401
+ "story_ids",
2402
+ "workflow_ids",
2403
+ "side_effect_ids",
2404
+ "evidence_refs",
2405
+ "proof_summary",
2406
+ ],
2407
+ }
2408
+ )
2409
+ do_not_touch.append(f"Do not modify passing idea acceptance criterion #{criterion_index}: {criterion}")
2410
+ do_not_touch.append(
2411
+ f"Do not revisit already-stable idea acceptance criterion #{criterion_index} unless you can cite a contradiction in the current artifact."
2412
+ )
2413
+ elif criterion_index:
2414
+ do_not_touch.append(
2415
+ f"Within failing idea acceptance criterion #{criterion_index}, preserve criterion_index and criterion text exactly; repair only missing proof, seams, or evidence mapping."
2416
+ )
2417
+ return protected_sections, do_not_touch
2418
+
2419
+
2420
+ def _validate_workflow_repair_sources(
2421
+ *,
2422
+ deterministic_errors: list[str],
2423
+ blocking_agentic_findings: list[dict[str, Any]],
2424
+ acceptance_failures: list[dict[str, Any]] | None = None,
2425
+ idea_acceptance_failures: list[dict[str, Any]] | None = None,
2426
+ ) -> list[dict[str, Any]]:
2427
+ repair_sources: list[dict[str, Any]] = [
2428
+ {
2429
+ "source_type": "deterministic_validation_error",
2430
+ "message": error,
2431
+ }
2432
+ for error in deterministic_errors
2433
+ ]
2434
+ repair_sources.extend(
2435
+ {
2436
+ "source_type": "agentic_blocking_finding",
2437
+ "workflow_id": finding.get("workflow_id"),
2438
+ "severity": finding.get("severity"),
2439
+ "summary": finding.get("summary"),
2440
+ "details": list(finding.get("details") or []),
2441
+ "blocking": bool(finding.get("blocking")),
2442
+ }
2443
+ for finding in blocking_agentic_findings
2444
+ )
2445
+ repair_sources.extend(list(acceptance_failures or []))
2446
+ repair_sources.extend(list(idea_acceptance_failures or []))
2447
+ return repair_sources
2448
+
2449
+
2450
+ def _interaction_point_seam_family(point: dict[str, Any]) -> str:
2451
+ kind = str(point.get("kind") or "").strip().lower()
2452
+ if not kind:
2453
+ kind = _normalize_interaction_type(str(point.get("type") or "").strip().lower())
2454
+ return kind or "no_seam"
2455
+
2456
+
2457
+ def _workflow_seam_family(workflow: dict[str, Any]) -> str:
2458
+ for point in list(workflow.get("interaction_points") or []) + list(workflow.get("needed_interaction_points") or []):
2459
+ family = _interaction_point_seam_family(point)
2460
+ if family and family != "no_seam":
2461
+ return family
2462
+ return "missing_seam"
2463
+
2464
+
2465
+ def _merge_repaired_workflows(
2466
+ *,
2467
+ base_workflows: list[dict[str, Any]],
2468
+ repaired_workflows: list[dict[str, Any]],
2469
+ allowed_workflow_ids: set[str],
2470
+ side_effects_artifact: dict[str, Any],
2471
+ ) -> list[dict[str, Any]]:
2472
+ repaired_map = {
2473
+ str(item.get("workflow_id") or ""): dict(item)
2474
+ for item in repaired_workflows
2475
+ if str(item.get("workflow_id") or "")
2476
+ }
2477
+ merged: list[dict[str, Any]] = []
2478
+ seen: set[str] = set()
2479
+ for workflow in base_workflows:
2480
+ workflow_id = str(workflow.get("workflow_id") or "")
2481
+ if workflow_id in allowed_workflow_ids and workflow_id in repaired_map:
2482
+ merged.append(_ground_workflow_in_side_effect(workflow=repaired_map[workflow_id], side_effects_artifact=side_effects_artifact))
2483
+ seen.add(workflow_id)
2484
+ else:
2485
+ merged.append(dict(workflow))
2486
+ for workflow_id, workflow in repaired_map.items():
2487
+ if workflow_id in allowed_workflow_ids and workflow_id not in seen:
2488
+ merged.append(_ground_workflow_in_side_effect(workflow=workflow, side_effects_artifact=side_effects_artifact))
2489
+ return merged
2490
+
2491
+
2492
+ def _merge_repaired_idea_acceptance_coverage(
2493
+ *,
2494
+ base_coverage: dict[str, Any],
2495
+ repaired_coverage: dict[str, Any],
2496
+ allowed_criterion_indices: set[int],
2497
+ canonical_criteria: list[str],
2498
+ ) -> dict[str, Any]:
2499
+ merged = dict(base_coverage or {})
2500
+ base_entries = {
2501
+ int(entry.get("criterion_index") or 0): dict(entry)
2502
+ for entry in (merged.get("criteria") or [])
2503
+ if int(entry.get("criterion_index") or 0) > 0
2504
+ }
2505
+ repaired_entries = {
2506
+ int(entry.get("criterion_index") or 0): dict(entry)
2507
+ for entry in (repaired_coverage.get("criteria") or [])
2508
+ if int(entry.get("criterion_index") or 0) > 0
2509
+ }
2510
+ for idx in allowed_criterion_indices:
2511
+ if idx in repaired_entries:
2512
+ base_entries[idx] = repaired_entries[idx]
2513
+ merged["criteria"] = [base_entries[idx] for idx in sorted(base_entries)]
2514
+ repaired_lineage = repaired_coverage.get("execution_lineage")
2515
+ if repaired_lineage:
2516
+ merged["execution_lineage"] = repaired_lineage
2517
+ return _normalize_idea_acceptance_coverage(
2518
+ idea_acceptance_coverage=merged,
2519
+ canonical_criteria=canonical_criteria,
2520
+ )
2521
+
2522
+
2523
+ def _build_validate_repair_clusters(
2524
+ *,
2525
+ workflows: list[dict[str, Any]],
2526
+ repair_sources: list[dict[str, Any]],
2527
+ blocking_agentic_findings: list[dict[str, Any]],
2528
+ idea_acceptance_coverage: dict[str, Any] | None = None,
2529
+ failing_workflow_ids: set[str],
2530
+ failing_idea_criterion_indices: set[int],
2531
+ ) -> list[dict[str, Any]]:
2532
+ workflow_map = {
2533
+ str(workflow.get("workflow_id") or ""): dict(workflow)
2534
+ for workflow in workflows
2535
+ if str(workflow.get("workflow_id") or "")
2536
+ }
2537
+ coverage_by_index = {
2538
+ int(entry.get("criterion_index") or 0): dict(entry)
2539
+ for entry in list((idea_acceptance_coverage or {}).get("criteria") or [])
2540
+ if int(entry.get("criterion_index") or 0) > 0
2541
+ }
2542
+
2543
+ def cluster_key_for(*, seam_family: str, workflow_ids: set[str], criterion_indices: set[int]) -> tuple[str, str]:
2544
+ side_effect_ids = sorted({
2545
+ str((workflow_map.get(workflow_id) or {}).get("side_effect", {}).get("id") or "")
2546
+ for workflow_id in workflow_ids
2547
+ if str((workflow_map.get(workflow_id) or {}).get("side_effect", {}).get("id") or "")
2548
+ })
2549
+ if not side_effect_ids and criterion_indices:
2550
+ for idx in criterion_indices:
2551
+ entry = coverage_by_index.get(idx) or {}
2552
+ side_effect_ids.extend(
2553
+ sid for sid in (str(item).strip() for item in (entry.get("side_effect_ids") or [])) if sid
2554
+ )
2555
+ scope = "__".join(sorted(dict.fromkeys(side_effect_ids))) or "global"
2556
+ return seam_family or "global", scope
2557
+
2558
+ clusters: dict[tuple[str, str], dict[str, Any]] = {}
2559
+
2560
+ def ensure_cluster(*, seam_family: str, workflow_ids: set[str], criterion_indices: set[int]) -> dict[str, Any]:
2561
+ family, scope = cluster_key_for(seam_family=seam_family, workflow_ids=workflow_ids, criterion_indices=criterion_indices)
2562
+ key = (family, scope)
2563
+ cluster = clusters.get(key)
2564
+ if cluster is None:
2565
+ cluster = {
2566
+ "cluster_id": f"{family}__{scope}",
2567
+ "cluster_type": "repair_cluster",
2568
+ "seam_family": family,
2569
+ "scope": scope,
2570
+ "workflow_ids": set(),
2571
+ "side_effect_ids": set(),
2572
+ "criterion_indices": set(),
2573
+ "repair_sources": [],
2574
+ "blocking_findings": [],
2575
+ "source_types": set(),
2576
+ }
2577
+ clusters[key] = cluster
2578
+ cluster["workflow_ids"].update(workflow_ids)
2579
+ cluster["criterion_indices"].update(criterion_indices)
2580
+ for workflow_id in workflow_ids:
2581
+ side_effect_id = str((workflow_map.get(workflow_id) or {}).get("side_effect", {}).get("id") or "")
2582
+ if side_effect_id:
2583
+ cluster["side_effect_ids"].add(side_effect_id)
2584
+ return cluster
2585
+
2586
+ for source in repair_sources:
2587
+ source_workflow_ids = {
2588
+ str(item).strip()
2589
+ for item in (source.get("workflow_ids") or [])
2590
+ if str(item).strip()
2591
+ }
2592
+ source_workflow_ids.update(
2593
+ workflow_id
2594
+ for workflow_id in failing_workflow_ids
2595
+ if workflow_id and workflow_id in str(source.get("message") or "")
2596
+ )
2597
+ source_workflow_ids.update(
2598
+ workflow_id
2599
+ for workflow_id in failing_workflow_ids
2600
+ if workflow_id and workflow_id == str(source.get("workflow_id") or "").strip()
2601
+ )
2602
+ criterion_index = int(source.get("criterion_index") or 0)
2603
+ if criterion_index <= 0:
2604
+ match = re.search(r"\[#(\d+)\]", str(source.get("message") or ""))
2605
+ if match:
2606
+ criterion_index = int(match.group(1))
2607
+ criterion_indices = {criterion_index} if criterion_index > 0 else set()
2608
+ if not source_workflow_ids and criterion_indices:
2609
+ for idx in criterion_indices:
2610
+ entry = coverage_by_index.get(idx) or {}
2611
+ source_workflow_ids.update(
2612
+ str(item).strip()
2613
+ for item in (entry.get("workflow_ids") or [])
2614
+ if str(item).strip()
2615
+ )
2616
+ if str(source.get("source_type") or "") == "deterministic_validation_error" and not source_workflow_ids and not criterion_indices:
2617
+ continue
2618
+ seam_families = {
2619
+ _workflow_seam_family(workflow_map[workflow_id])
2620
+ for workflow_id in source_workflow_ids
2621
+ if workflow_id in workflow_map
2622
+ }
2623
+ if source.get("source_type") in {"acceptance_criterion_missing_seam", "idea_acceptance_criterion_under_proven"} and not seam_families:
2624
+ seam_families.add("missing_seam")
2625
+ if source.get("source_type") == "idea_acceptance_builder_not_model_backed":
2626
+ seam_families.add("builder_lineage")
2627
+ if not seam_families:
2628
+ seam_families = {"global"}
2629
+ for seam_family in seam_families:
2630
+ cluster = ensure_cluster(seam_family=seam_family, workflow_ids=source_workflow_ids, criterion_indices=criterion_indices)
2631
+ cluster["repair_sources"].append(dict(source))
2632
+ source_type = str(source.get("source_type") or "deterministic_validation_error").strip()
2633
+ if source_type:
2634
+ cluster["source_types"].add(source_type)
2635
+
2636
+ for finding in blocking_agentic_findings:
2637
+ workflow_id = str(finding.get("workflow_id") or "").strip()
2638
+ workflow_ids = {workflow_id} if workflow_id else set()
2639
+ seam_family = _workflow_seam_family(workflow_map[workflow_id]) if workflow_id in workflow_map else "global"
2640
+ cluster = ensure_cluster(seam_family=seam_family, workflow_ids=workflow_ids, criterion_indices=set())
2641
+ cluster["blocking_findings"].append(dict(finding))
2642
+ cluster["source_types"].add("agentic_blocking_finding")
2643
+
2644
+ for workflow_id in sorted(failing_workflow_ids):
2645
+ seam_family = _workflow_seam_family(workflow_map.get(workflow_id) or {})
2646
+ ensure_cluster(seam_family=seam_family, workflow_ids={workflow_id}, criterion_indices=set())
2647
+
2648
+ if not clusters and (failing_workflow_ids or failing_idea_criterion_indices):
2649
+ cluster = ensure_cluster(seam_family="global", workflow_ids=set(failing_workflow_ids), criterion_indices=set(failing_idea_criterion_indices))
2650
+ cluster["source_types"].add("fallback_cluster")
2651
+
2652
+ finalized: list[dict[str, Any]] = []
2653
+ for cluster in clusters.values():
2654
+ workflow_ids = sorted(cluster["workflow_ids"])
2655
+ criterion_indices = sorted(cluster["criterion_indices"])
2656
+ if not workflow_ids and not criterion_indices and not cluster["repair_sources"] and not cluster["blocking_findings"]:
2657
+ continue
2658
+ finalized.append({
2659
+ "cluster_id": cluster["cluster_id"],
2660
+ "cluster_type": cluster["cluster_type"],
2661
+ "seam_family": cluster["seam_family"],
2662
+ "scope": cluster["scope"],
2663
+ "workflow_ids": workflow_ids,
2664
+ "side_effect_ids": sorted(cluster["side_effect_ids"]),
2665
+ "criterion_indices": criterion_indices,
2666
+ "repair_sources": cluster["repair_sources"],
2667
+ "blocking_findings": cluster["blocking_findings"],
2668
+ "source_types": sorted(cluster["source_types"]),
2669
+ "failing_workflows": [workflow_map[workflow_id] for workflow_id in workflow_ids if workflow_id in workflow_map],
2670
+ "failing_idea_acceptance_criteria": [coverage_by_index[idx] for idx in criterion_indices if idx in coverage_by_index],
2671
+ })
2672
+ finalized.sort(key=lambda item: (item["seam_family"], item["scope"], item["cluster_id"]))
2673
+ return finalized
2674
+
2675
+
2676
+ def _persist_validate_repair_clusters(
2677
+ *,
2678
+ repo_root: Path | None,
2679
+ idea_id: str | None,
2680
+ pipeline_dir: Path | None,
2681
+ payload: dict[str, Any],
2682
+ ) -> None:
2683
+ if repo_root is None or idea_id is None or pipeline_dir is None:
2684
+ return
2685
+ _write_integration_artifact(
2686
+ repo_root=repo_root,
2687
+ idea_id=idea_id,
2688
+ pipeline_dir=pipeline_dir,
2689
+ filename="validation_repair_clusters.json",
2690
+ data=payload,
2691
+ )
2692
+
2693
+
2694
+ def _validate_workflows(
2695
+ *,
2696
+ side_effects_artifact: dict[str, Any],
2697
+ implicated_users_artifact: dict[str, Any],
2698
+ workflows: list[dict[str, Any]],
2699
+ implemented_idea: dict[str, Any] | None = None,
2700
+ implemented_stories: list[dict[str, Any]] | None = None,
2701
+ idea_acceptance_coverage: dict[str, Any] | None = None,
2702
+ max_iterations: int = 4,
2703
+ agentic_review: dict[str, Any] | None = None,
2704
+ repo_root: Path | None = None,
2705
+ idea_id: str | None = None,
2706
+ pipeline_dir: Path | None = None,
2707
+ repair_node_id_prefix: str = "validate",
2708
+ code_evidence: list[dict[str, Any]] | None = None,
2709
+ source_docs: list[dict[str, Any]] | None = None,
2710
+ ) -> tuple[dict[str, Any], int]:
2711
+ iterations_used = 0
2712
+ validations: list[dict[str, Any]] = []
2713
+ pending = [dict(workflow) for workflow in workflows]
2714
+ expected_side_effect_ids = {str(item.get("id") or "") for item in side_effects_artifact.get("side_effects") or [] if str(item.get("id") or "")}
2715
+ expected_users = {
2716
+ (str(user.get("kind") or ""), str(user.get("scope") or ""), str(user.get("authority") or ""))
2717
+ for user in implicated_users_artifact.get("implicated_users") or []
2718
+ }
2719
+ implemented_stories = list(implemented_stories or [])
2720
+ story_acceptance = _story_acceptance_index(implemented_stories)
2721
+ story_index = {
2722
+ str(story.get("story_id") or story.get("id") or "").strip(): dict(story)
2723
+ for story in implemented_stories
2724
+ if str(story.get("story_id") or story.get("id") or "").strip()
2725
+ }
2726
+ idea_acceptance_criteria = _idea_acceptance_criteria(implemented_idea)
2727
+ agentic_review = dict(agentic_review or {})
2728
+ pending_idea_acceptance_coverage = _normalize_idea_acceptance_coverage(
2729
+ idea_acceptance_coverage=idea_acceptance_coverage,
2730
+ canonical_criteria=idea_acceptance_criteria,
2731
+ )
2732
+ agentic_findings = list(agentic_review.get("findings") or [])
2733
+ blocking_agentic_findings = [
2734
+ finding
2735
+ for finding in agentic_findings
2736
+ if bool(finding.get("blocking")) or str(finding.get("severity") or "").lower() in {"error", "blocking", "critical"}
2737
+ ]
2738
+
2739
+ cluster_history: list[dict[str, Any]] = []
2740
+ prior_convergence_state: dict[str, Any] | None = None
2741
+ final_errors: list[str] = []
2742
+ final_idea_acceptance_failures: list[dict[str, Any]] = []
2743
+ final_repair_clusters_payload = {
2744
+ "idea_id": idea_id or side_effects_artifact.get("idea_id"),
2745
+ "mode": "internal_sequential_clusters",
2746
+ "iterations": [],
2747
+ "latest_iteration": None,
2748
+ "unresolved_cluster_ids": [],
2749
+ }
2750
+
2751
+ for iteration in range(1, max_iterations + 1):
2752
+ iterations_used = iteration
2753
+ errors: list[str] = []
2754
+ acceptance_failures: list[dict[str, Any]] = []
2755
+ idea_acceptance_failures: list[dict[str, Any]] = []
2756
+ workflow_side_effect_ids = {str(item.get("side_effect", {}).get("id") or "") for item in pending}
2757
+ if workflow_side_effect_ids != expected_side_effect_ids:
2758
+ missing = sorted(expected_side_effect_ids - workflow_side_effect_ids)
2759
+ if missing:
2760
+ errors.append(f"missing workflows for side effects: {', '.join(missing)}")
2761
+
2762
+ attached_users = {
2763
+ (str(user.get("kind") or ""), str(user.get("scope") or ""), str(user.get("authority") or ""))
2764
+ for workflow in pending
2765
+ for user in (workflow.get("implicated_users") or [])
2766
+ }
2767
+ if not expected_users.issubset(attached_users):
2768
+ errors.append("all implicated users covered: false")
2769
+
2770
+ side_effect_map = _workflow_side_effect_map(side_effects_artifact)
2771
+ story_to_workflow_ids: dict[str, set[str]] = {}
2772
+ workflow_map: dict[str, dict[str, Any]] = {}
2773
+ for workflow in pending:
2774
+ workflow_id = str(workflow.get("workflow_id") or "")
2775
+ if workflow_id:
2776
+ workflow_map[workflow_id] = workflow
2777
+ for story_id in _workflow_story_ids(workflow):
2778
+ story_to_workflow_ids.setdefault(story_id, set()).add(workflow_id)
2779
+ for workflow in pending:
2780
+ wf_id = str(workflow.get("workflow_id") or "")
2781
+ wf_se_id = str(workflow.get("side_effect", {}).get("id") or "")
2782
+ expected_side_effect = side_effect_map.get(wf_se_id) or {}
2783
+ if wf_se_id and wf_se_id not in expected_side_effect_ids:
2784
+ errors.append(f"story alignment mismatch for {wf_id}")
2785
+ if not workflow.get("story_backing"):
2786
+ errors.append(f"story alignment missing for {wf_id}")
2787
+ if not workflow.get("code_backing"):
2788
+ errors.append(f"code alignment missing for {wf_id}")
2789
+ interaction_points = list(workflow.get("interaction_points") or [])
2790
+ needed_points = list(workflow.get("needed_interaction_points") or [])
2791
+ expected_interaction_points = list(expected_side_effect.get("interaction_points") or [])
2792
+ expected_needed_points = list(expected_side_effect.get("needed_interaction_points") or [])
2793
+ if interaction_points != expected_interaction_points or needed_points != expected_needed_points:
2794
+ errors.append(f"interaction seam drift for {wf_id}")
2795
+ if not interaction_points and not needed_points:
2796
+ errors.append(f"interaction points missing for {wf_id}")
2797
+ if workflow.get("anchor_user"):
2798
+ errors.append(f"workflow anchored to user instead of side effect for {wf_id}")
2799
+
2800
+ for story_id, story_contract in story_acceptance.items():
2801
+ related_workflow_ids = sorted(story_to_workflow_ids.get(story_id) or [])
2802
+ if not related_workflow_ids:
2803
+ for idx, criterion in enumerate(story_contract.get("acceptance_criteria") or [], start=1):
2804
+ message = f"acceptance criteria uncovered for {story_id} [#{idx}]: {criterion}"
2805
+ errors.append(message)
2806
+ acceptance_failures.append(
2807
+ {
2808
+ "source_type": "acceptance_criterion_uncovered",
2809
+ "story_id": story_id,
2810
+ "story_title": story_contract.get("title"),
2811
+ "criterion_index": idx,
2812
+ "criterion": criterion,
2813
+ "workflow_ids": [],
2814
+ "message": message,
2815
+ }
2816
+ )
2817
+ continue
2818
+ seam_workflow_ids = [
2819
+ workflow_id
2820
+ for workflow_id in related_workflow_ids
2821
+ if (workflow_map.get(workflow_id) or {}).get("interaction_points") or (workflow_map.get(workflow_id) or {}).get("needed_interaction_points")
2822
+ ]
2823
+ if seam_workflow_ids:
2824
+ continue
2825
+ for idx, criterion in enumerate(story_contract.get("acceptance_criteria") or [], start=1):
2826
+ message = f"acceptance criterion missing seam for {story_id} [#{idx}]: {criterion}"
2827
+ errors.append(message)
2828
+ acceptance_failures.append(
2829
+ {
2830
+ "source_type": "acceptance_criterion_missing_seam",
2831
+ "story_id": story_id,
2832
+ "story_title": story_contract.get("title"),
2833
+ "criterion_index": idx,
2834
+ "criterion": criterion,
2835
+ "workflow_ids": related_workflow_ids,
2836
+ "message": message,
2837
+ }
2838
+ )
2839
+
2840
+ coverage_entries = list(pending_idea_acceptance_coverage.get("criteria") or [])
2841
+ coverage_by_index = {
2842
+ int(entry.get("criterion_index") or 0): dict(entry)
2843
+ for entry in coverage_entries
2844
+ if int(entry.get("criterion_index") or 0) > 0
2845
+ }
2846
+ for idx, criterion in enumerate(idea_acceptance_criteria, start=1):
2847
+ entry = coverage_by_index.get(idx)
2848
+ if entry is None:
2849
+ message = f"idea acceptance criterion uncovered [#{idx}]: {criterion}"
2850
+ errors.append(message)
2851
+ idea_acceptance_failures.append(
2852
+ {
2853
+ "source_type": "idea_acceptance_criterion_uncovered",
2854
+ "criterion_index": idx,
2855
+ "criterion": criterion,
2856
+ "workflow_ids": [],
2857
+ "story_ids": [],
2858
+ "message": message,
2859
+ }
2860
+ )
2861
+ continue
2862
+ if str(entry.get("criterion") or "").strip() != criterion:
2863
+ message = f"idea acceptance criterion drift [#{idx}]: expected '{criterion}'"
2864
+ errors.append(message)
2865
+ idea_acceptance_failures.append(
2866
+ {
2867
+ "source_type": "idea_acceptance_criterion_drift",
2868
+ "criterion_index": idx,
2869
+ "criterion": criterion,
2870
+ "workflow_ids": list(entry.get("workflow_ids") or []),
2871
+ "story_ids": list(entry.get("story_ids") or []),
2872
+ "message": message,
2873
+ }
2874
+ )
2875
+ verdict = str(entry.get("verdict") or "").strip().lower()
2876
+ story_ids = [sid for sid in (str(item).strip() for item in (entry.get("story_ids") or [])) if sid]
2877
+ workflow_ids = [wid for wid in (str(item).strip() for item in (entry.get("workflow_ids") or [])) if wid]
2878
+ evidence_refs = [ref for ref in (str(item).strip() for item in (entry.get("evidence_refs") or [])) if ref]
2879
+ mapped_story_ids = [sid for sid in story_ids if sid in story_index]
2880
+ mapped_workflow_ids = [wid for wid in workflow_ids if wid in workflow_map]
2881
+ seam_workflow_ids = [
2882
+ wid for wid in mapped_workflow_ids
2883
+ if (workflow_map.get(wid) or {}).get("interaction_points") or (workflow_map.get(wid) or {}).get("needed_interaction_points")
2884
+ ]
2885
+ under_proven_reasons: list[str] = []
2886
+ if verdict != "covered":
2887
+ under_proven_reasons.append(f"verdict={verdict or 'missing'}")
2888
+ if not mapped_story_ids:
2889
+ under_proven_reasons.append("missing story mapping")
2890
+ if not mapped_workflow_ids:
2891
+ under_proven_reasons.append("missing workflow mapping")
2892
+ if mapped_workflow_ids and not seam_workflow_ids:
2893
+ under_proven_reasons.append("workflow mapping has no interaction seam")
2894
+ if not evidence_refs:
2895
+ under_proven_reasons.append("missing evidence refs")
2896
+ if not str(entry.get("proof_summary") or "").strip():
2897
+ under_proven_reasons.append("missing proof summary")
2898
+ if under_proven_reasons:
2899
+ message = f"idea acceptance criterion under-proven [#{idx}]: {criterion} :: {', '.join(under_proven_reasons)}"
2900
+ errors.append(message)
2901
+ idea_acceptance_failures.append(
2902
+ {
2903
+ "source_type": "idea_acceptance_criterion_under_proven",
2904
+ "criterion_index": idx,
2905
+ "criterion": criterion,
2906
+ "workflow_ids": mapped_workflow_ids,
2907
+ "story_ids": mapped_story_ids,
2908
+ "message": message,
2909
+ "reasons": under_proven_reasons,
2910
+ }
2911
+ )
2912
+
2913
+ lineage = pending_idea_acceptance_coverage.get("execution_lineage") if isinstance(pending_idea_acceptance_coverage, dict) else None
2914
+ if idea_acceptance_criteria:
2915
+ if not _idea_acceptance_lineage_is_model_backed(lineage):
2916
+ errors.append("idea acceptance builder lineage missing model-backed proof")
2917
+ idea_acceptance_failures.append(
2918
+ {
2919
+ "source_type": "idea_acceptance_builder_not_model_backed",
2920
+ "criterion_index": None,
2921
+ "criterion": "",
2922
+ "workflow_ids": [],
2923
+ "story_ids": [],
2924
+ "message": "idea acceptance builder lineage missing model-backed proof",
2925
+ }
2926
+ )
2927
+
2928
+ if not attached_users.issubset(expected_users) and expected_users:
2929
+ errors.append("invalid actors admitted into workflows")
2930
+
2931
+ deterministic_errors = list(errors)
2932
+ errors.extend(
2933
+ f"agentic review blocking: {finding.get('workflow_id') or 'global'} :: {finding.get('summary') or 'unspecified issue'}"
2934
+ for finding in blocking_agentic_findings
2935
+ )
2936
+ repair_sources = _validate_workflow_repair_sources(
2937
+ deterministic_errors=deterministic_errors,
2938
+ blocking_agentic_findings=blocking_agentic_findings,
2939
+ acceptance_failures=acceptance_failures,
2940
+ idea_acceptance_failures=idea_acceptance_failures,
2941
+ )
2942
+ failing_workflow_ids = _determine_failing_workflow_ids(
2943
+ workflows=pending,
2944
+ errors=deterministic_errors,
2945
+ blocking_agentic_findings=blocking_agentic_findings,
2946
+ repair_sources=repair_sources,
2947
+ )
2948
+ failing_idea_criterion_indices = {
2949
+ int(item.get("criterion_index") or 0)
2950
+ for item in idea_acceptance_failures
2951
+ if int(item.get("criterion_index") or 0) > 0
2952
+ }
2953
+ protected_sections, do_not_touch = _build_validate_repair_protected_context(
2954
+ workflows=pending,
2955
+ failing_workflow_ids=failing_workflow_ids,
2956
+ idea_acceptance_coverage=pending_idea_acceptance_coverage,
2957
+ failing_idea_criterion_indices=failing_idea_criterion_indices,
2958
+ )
2959
+ repair_clusters = _build_validate_repair_clusters(
2960
+ workflows=pending,
2961
+ repair_sources=repair_sources,
2962
+ blocking_agentic_findings=blocking_agentic_findings,
2963
+ idea_acceptance_coverage=pending_idea_acceptance_coverage,
2964
+ failing_workflow_ids=failing_workflow_ids,
2965
+ failing_idea_criterion_indices=failing_idea_criterion_indices,
2966
+ )
2967
+
2968
+ iteration_record = {
2969
+ "iteration": iteration,
2970
+ "deterministic_errors": deterministic_errors,
2971
+ "blocking_agentic_findings": blocking_agentic_findings,
2972
+ "acceptance_failures": acceptance_failures,
2973
+ "idea_acceptance_failures": idea_acceptance_failures,
2974
+ "repair_sources": repair_sources,
2975
+ "failing_workflow_ids": sorted(failing_workflow_ids),
2976
+ "failing_idea_criterion_indices": sorted(failing_idea_criterion_indices),
2977
+ "protected_sections": protected_sections,
2978
+ "do_not_touch": do_not_touch,
2979
+ "repair_clusters": [
2980
+ {
2981
+ "cluster_id": cluster["cluster_id"],
2982
+ "cluster_type": cluster["cluster_type"],
2983
+ "seam_family": cluster["seam_family"],
2984
+ "scope": cluster["scope"],
2985
+ "workflow_ids": list(cluster["workflow_ids"]),
2986
+ "side_effect_ids": list(cluster["side_effect_ids"]),
2987
+ "criterion_indices": list(cluster["criterion_indices"]),
2988
+ "source_types": list(cluster["source_types"]),
2989
+ }
2990
+ for cluster in repair_clusters
2991
+ ],
2992
+ "errors": list(errors),
2993
+ }
2994
+ convergence_state = _validation_convergence_state(
2995
+ idea_acceptance_coverage=pending_idea_acceptance_coverage,
2996
+ deterministic_errors=deterministic_errors,
2997
+ blocking_agentic_findings=blocking_agentic_findings,
2998
+ acceptance_failures=acceptance_failures,
2999
+ idea_acceptance_failures=idea_acceptance_failures,
3000
+ )
3001
+ iteration_record["convergence_state"] = convergence_state
3002
+ validations.append(iteration_record)
3003
+ cluster_iteration_record = {
3004
+ "iteration": iteration,
3005
+ "cluster_execution_mode": "internal_sequential",
3006
+ "clusters": [],
3007
+ }
3008
+ final_repair_clusters_payload = {
3009
+ "idea_id": idea_id or side_effects_artifact.get("idea_id"),
3010
+ "mode": "internal_sequential_clusters",
3011
+ "iterations": cluster_history + [cluster_iteration_record],
3012
+ "latest_iteration": iteration,
3013
+ "unresolved_cluster_ids": [cluster["cluster_id"] for cluster in repair_clusters],
3014
+ }
3015
+ _persist_validate_repair_clusters(
3016
+ repo_root=repo_root,
3017
+ idea_id=idea_id or str(side_effects_artifact.get("idea_id") or "") or None,
3018
+ pipeline_dir=pipeline_dir,
3019
+ payload=final_repair_clusters_payload,
3020
+ )
3021
+
3022
+ final_errors = errors
3023
+ final_idea_acceptance_failures = idea_acceptance_failures
3024
+ if not errors:
3025
+ iteration_record["stop_reason"] = "passed"
3026
+ cluster_history.append(cluster_iteration_record)
3027
+ final_repair_clusters_payload = {
3028
+ "idea_id": idea_id or side_effects_artifact.get("idea_id"),
3029
+ "mode": "internal_sequential_clusters",
3030
+ "iterations": cluster_history,
3031
+ "latest_iteration": iteration,
3032
+ "unresolved_cluster_ids": [],
3033
+ }
3034
+ _persist_validate_repair_clusters(
3035
+ repo_root=repo_root,
3036
+ idea_id=idea_id or str(side_effects_artifact.get("idea_id") or "") or None,
3037
+ pipeline_dir=pipeline_dir,
3038
+ payload=final_repair_clusters_payload,
3039
+ )
3040
+ break
3041
+
3042
+ if prior_convergence_state == convergence_state:
3043
+ iteration_record["stop_reason"] = "converged_without_delta"
3044
+ cluster_history.append(cluster_iteration_record)
3045
+ final_repair_clusters_payload = {
3046
+ "idea_id": idea_id or side_effects_artifact.get("idea_id"),
3047
+ "mode": "internal_sequential_clusters",
3048
+ "iterations": cluster_history,
3049
+ "latest_iteration": iteration,
3050
+ "unresolved_cluster_ids": [cluster["cluster_id"] for cluster in repair_clusters],
3051
+ }
3052
+ _persist_validate_repair_clusters(
3053
+ repo_root=repo_root,
3054
+ idea_id=idea_id or str(side_effects_artifact.get("idea_id") or "") or None,
3055
+ pipeline_dir=pipeline_dir,
3056
+ payload=final_repair_clusters_payload,
3057
+ )
3058
+ break
3059
+
3060
+ prior_convergence_state = convergence_state
3061
+
3062
+ if iteration < max_iterations and repo_root is not None and idea_id is not None:
3063
+ blocking_for_repair = [
3064
+ f for f in list(agentic_review.get("findings") or [])
3065
+ if bool(f.get("blocking")) or str(f.get("severity") or "").lower() in {"error", "blocking", "critical"}
3066
+ ]
3067
+ for cluster_index, cluster in enumerate(repair_clusters, start=1):
3068
+ cluster_workflow_ids = {str(item).strip() for item in (cluster.get("workflow_ids") or []) if str(item).strip()}
3069
+ cluster_criterion_indices = {int(item) for item in (cluster.get("criterion_indices") or []) if int(item) > 0}
3070
+ scoped_workflows = [
3071
+ dict(workflow)
3072
+ for workflow in pending
3073
+ if str(workflow.get("workflow_id") or "") in cluster_workflow_ids
3074
+ ]
3075
+ scoped_coverage = _filter_idea_acceptance_coverage(
3076
+ idea_acceptance_coverage=pending_idea_acceptance_coverage,
3077
+ criterion_indices=cluster_criterion_indices,
3078
+ )
3079
+ cluster_protected_sections, cluster_do_not_touch = _build_validate_repair_protected_context(
3080
+ workflows=pending,
3081
+ failing_workflow_ids=cluster_workflow_ids,
3082
+ idea_acceptance_coverage=pending_idea_acceptance_coverage,
3083
+ failing_idea_criterion_indices=cluster_criterion_indices,
3084
+ )
3085
+ focused_code_evidence = _load_code_evidence_for_workflows(
3086
+ workflows=[workflow for workflow in pending if str(workflow.get("workflow_id") or "") in cluster_workflow_ids],
3087
+ code_evidence=code_evidence,
3088
+ repo_root=repo_root,
3089
+ ) if cluster_workflow_ids else []
3090
+ cluster_context = {
3091
+ "idea_id": idea_id,
3092
+ "implemented_idea": implemented_idea or {},
3093
+ "implemented_stories": implemented_stories,
3094
+ "enriched_workflows": scoped_workflows,
3095
+ "idea_acceptance_coverage": scoped_coverage,
3096
+ "repair_cluster": {
3097
+ "cluster_id": cluster["cluster_id"],
3098
+ "cluster_type": cluster["cluster_type"],
3099
+ "cluster_index": cluster_index,
3100
+ "cluster_total": len(repair_clusters),
3101
+ "seam_family": cluster["seam_family"],
3102
+ "scope": cluster["scope"],
3103
+ "workflow_ids": sorted(cluster_workflow_ids),
3104
+ "side_effect_ids": list(cluster.get("side_effect_ids") or []),
3105
+ "criterion_indices": sorted(cluster_criterion_indices),
3106
+ "repair_sources": list(cluster.get("repair_sources") or []),
3107
+ "blocking_findings": list(cluster.get("blocking_findings") or []),
3108
+ "source_types": list(cluster.get("source_types") or []),
3109
+ },
3110
+ "failing_workflows": scoped_workflows,
3111
+ "failing_idea_acceptance_criteria": [
3112
+ failure for failure in idea_acceptance_failures
3113
+ if int(failure.get("criterion_index") or 0) in cluster_criterion_indices
3114
+ ],
3115
+ "remaining_failing_workflow_ids": sorted(cluster_workflow_ids),
3116
+ "remaining_failing_idea_criterion_indices": sorted(cluster_criterion_indices),
3117
+ "deterministic_errors": [
3118
+ error for error in deterministic_errors
3119
+ if any(workflow_id in error for workflow_id in cluster_workflow_ids)
3120
+ or any(f"[#{criterion_index}]" in error for criterion_index in cluster_criterion_indices)
3121
+ or (not cluster_workflow_ids and not cluster_criterion_indices)
3122
+ ] or deterministic_errors,
3123
+ "blocking_findings": [
3124
+ finding for finding in blocking_for_repair
3125
+ if not cluster_workflow_ids or str(finding.get("workflow_id") or "").strip() in cluster_workflow_ids
3126
+ ] or list(cluster.get("blocking_findings") or []),
3127
+ "repair_sources": list(cluster.get("repair_sources") or []),
3128
+ "protected_sections": cluster_protected_sections,
3129
+ "do_not_touch": cluster_do_not_touch,
3130
+ "focused_code_evidence": focused_code_evidence,
3131
+ "source_docs": list(source_docs or []),
3132
+ "prior_iteration_convergence_state": prior_convergence_state,
3133
+ "required_repair_delta_ledger": {
3134
+ "must_state": [
3135
+ "what verdicts changed",
3136
+ "what evidence changed",
3137
+ "what seam was newly resolved",
3138
+ ],
3139
+ "stable_criteria_rule": "Do not revisit already-stable criteria unless you can cite a contradiction in the current artifact.",
3140
+ },
3141
+ "repair_instruction": "Repair only this internal VEG repair_cluster. Work only on remaining failing workflows and remaining failing idea-acceptance criteria. Preserve protected_sections and obey do_not_touch. Do not rewrite unrelated workflows, do not re-summarize the full artifact, and do not revisit already-stable criteria without citing a contradiction in the current artifact.",
3142
+ }
3143
+ repair_artifact, repair_envelope = integration_agentic.run_integration_agent_step(
3144
+ repo_root=repo_root,
3145
+ stage_name="validate_repair",
3146
+ output_model=VegIdeaAcceptanceBuilderArtifact,
3147
+ context_payload=cluster_context,
3148
+ guidance=[],
3149
+ timeout_seconds=_integration_agent_timeout_seconds(),
3150
+ )
3151
+ if pipeline_dir is not None:
3152
+ integration_agentic.persist_agent_run(
3153
+ pipeline_root=pipeline_dir,
3154
+ node_id=f"{repair_node_id_prefix}_repair_iter{iteration}_cluster{cluster_index}",
3155
+ envelope=repair_envelope,
3156
+ )
3157
+ pending = _merge_repaired_workflows(
3158
+ base_workflows=pending,
3159
+ repaired_workflows=[item.model_dump() for item in repair_artifact.enriched_workflows],
3160
+ allowed_workflow_ids=cluster_workflow_ids,
3161
+ side_effects_artifact=side_effects_artifact,
3162
+ )
3163
+ pending_idea_acceptance_coverage = _merge_repaired_idea_acceptance_coverage(
3164
+ base_coverage=pending_idea_acceptance_coverage,
3165
+ repaired_coverage=repair_artifact.idea_acceptance_coverage.model_dump(),
3166
+ allowed_criterion_indices=cluster_criterion_indices,
3167
+ canonical_criteria=idea_acceptance_criteria,
3168
+ )
3169
+ latest_cluster_review = repair_artifact.model_dump()
3170
+ cluster_iteration_record["clusters"].append(
3171
+ {
3172
+ "cluster_id": cluster["cluster_id"],
3173
+ "cluster_type": cluster["cluster_type"],
3174
+ "cluster_index": cluster_index,
3175
+ "seam_family": cluster["seam_family"],
3176
+ "scope": cluster["scope"],
3177
+ "workflow_ids": sorted(cluster_workflow_ids),
3178
+ "criterion_indices": sorted(cluster_criterion_indices),
3179
+ "source_types": list(cluster.get("source_types") or []),
3180
+ "repair_sources": list(cluster.get("repair_sources") or []),
3181
+ "protected_sections": cluster_protected_sections,
3182
+ "do_not_touch": cluster_do_not_touch,
3183
+ "agentic_review_summary": latest_cluster_review.get("summary"),
3184
+ "model_backed": _idea_acceptance_lineage_is_model_backed((repair_artifact.idea_acceptance_coverage.model_dump() or {}).get("execution_lineage")),
3185
+ "repair_delta_ledger": (
3186
+ repair_artifact.repair_delta_ledger.model_dump()
3187
+ if repair_artifact.repair_delta_ledger is not None
3188
+ else None
3189
+ ),
3190
+ }
3191
+ )
3192
+ agentic_review = latest_cluster_review
3193
+ agentic_findings = list(agentic_review.get("findings") or [])
3194
+ blocking_agentic_findings = [
3195
+ finding
3196
+ for finding in agentic_findings
3197
+ if bool(finding.get("blocking")) or str(finding.get("severity") or "").lower() in {"error", "blocking", "critical"}
3198
+ ]
3199
+ final_repair_clusters_payload = {
3200
+ "idea_id": idea_id or side_effects_artifact.get("idea_id"),
3201
+ "mode": "internal_sequential_clusters",
3202
+ "iterations": cluster_history + [cluster_iteration_record],
3203
+ "latest_iteration": iteration,
3204
+ "unresolved_cluster_ids": [item["cluster_id"] for item in repair_clusters[cluster_index:]],
3205
+ }
3206
+ _persist_validate_repair_clusters(
3207
+ repo_root=repo_root,
3208
+ idea_id=idea_id or str(side_effects_artifact.get("idea_id") or "") or None,
3209
+ pipeline_dir=pipeline_dir,
3210
+ payload=final_repair_clusters_payload,
3211
+ )
3212
+
3213
+ cluster_history.append(cluster_iteration_record)
3214
+ final_repair_clusters_payload = {
3215
+ "idea_id": idea_id or side_effects_artifact.get("idea_id"),
3216
+ "mode": "internal_sequential_clusters",
3217
+ "iterations": cluster_history,
3218
+ "latest_iteration": iteration,
3219
+ "unresolved_cluster_ids": [],
3220
+ }
3221
+ _persist_validate_repair_clusters(
3222
+ repo_root=repo_root,
3223
+ idea_id=idea_id or str(side_effects_artifact.get("idea_id") or "") or None,
3224
+ pipeline_dir=pipeline_dir,
3225
+ payload=final_repair_clusters_payload,
3226
+ )
3227
+
3228
+ return {
3229
+ "idea_id": side_effects_artifact.get("idea_id"),
3230
+ "passed": not final_errors,
3231
+ "iterations_used": iterations_used,
3232
+ "max_iterations": max_iterations,
3233
+ "stop_reason": str((validations[-1] or {}).get("stop_reason") or ("passed" if not final_errors else "max_iterations_exhausted")) if validations else "not_started",
3234
+ "checks": {
3235
+ "all_implicated_users_covered": not any("all implicated users covered" in error for error in final_errors),
3236
+ "all_side_effects_covered": not any("missing workflows for side effects" in error for error in final_errors),
3237
+ "story_alignment": not any("story alignment" in error for error in final_errors),
3238
+ "code_alignment": not any("code alignment" in error for error in final_errors),
3239
+ "interaction_points_complete": not any("interaction points missing" in error for error in final_errors),
3240
+ "acceptance_criteria_covered": not any("acceptance criteria uncovered" in error for error in final_errors),
3241
+ "acceptance_criteria_have_seams": not any("acceptance criterion missing seam" in error for error in final_errors),
3242
+ "idea_acceptance_criteria_covered": not any("idea acceptance criterion uncovered" in error for error in final_errors),
3243
+ "idea_acceptance_criteria_proven": not any("idea acceptance criterion under-proven" in error or "idea acceptance criterion drift" in error for error in final_errors),
3244
+ "idea_acceptance_builder_model_backed": not any("idea acceptance builder lineage missing model-backed proof" in error for error in final_errors),
3245
+ "no_invalid_actors_admitted": not any("invalid actors admitted" in error for error in final_errors),
3246
+ "agentic_review_clear": not blocking_agentic_findings,
3247
+ },
3248
+ "agentic_review": {
3249
+ "summary": agentic_review.get("summary"),
3250
+ "findings": agentic_findings,
3251
+ "blocking_findings": blocking_agentic_findings,
3252
+ "enriched_workflow_count": len(agentic_review.get("enriched_workflows") or pending),
3253
+ },
3254
+ "idea_acceptance_coverage": pending_idea_acceptance_coverage,
3255
+ "acceptance_criteria": {
3256
+ "idea_count": len(idea_acceptance_criteria),
3257
+ "story_count": sum(len(item.get("acceptance_criteria") or []) for item in story_acceptance.values()),
3258
+ "story_ids_with_acceptance": sorted(story_acceptance.keys()),
3259
+ "failing_idea_criterion_indices": sorted({int(item.get("criterion_index") or 0) for item in final_idea_acceptance_failures if int(item.get("criterion_index") or 0) > 0}),
3260
+ },
3261
+ "validation_history": validations,
3262
+ "errors": final_errors,
3263
+ "next_stages": [stage.node_id for stage in default_integration_stages()[3:]],
3264
+ "_final_workflows": pending,
3265
+ "_full_agentic_review": agentic_review,
3266
+ "_repair_clusters": final_repair_clusters_payload,
3267
+ }, iterations_used
3268
+
3269
+ def run_integration_dag(
3270
+ *,
3271
+ repo_root: Path,
3272
+ idea_id: str,
3273
+ implemented_idea: dict[str, Any],
3274
+ implemented_stories: list[dict[str, Any]],
3275
+ code_evidence: list[dict[str, Any]] | None = None,
3276
+ source_docs: list[dict[str, Any]] | None = None,
3277
+ project_id: str | None = None,
3278
+ dag_run_id: str | None = None,
3279
+ ) -> IntegrationDagResult:
3280
+ code_evidence = list(code_evidence or [])
3281
+ source_docs = list(source_docs or [])
3282
+ resume_freshness = _compute_integration_freshness(
3283
+ repo_root=repo_root,
3284
+ idea_id=idea_id,
3285
+ payload_body={
3286
+ "idea_id": idea_id,
3287
+ "implemented_idea": implemented_idea,
3288
+ "implemented_stories": implemented_stories,
3289
+ "code_evidence": code_evidence,
3290
+ "source_docs": source_docs,
3291
+ },
3292
+ )
3293
+ runs_parent = _integration_runs_dir(repo_root, idea_id)
3294
+ runs_parent.mkdir(parents=True, exist_ok=True)
3295
+ pipeline_dir = runs_parent / f"run_{uuid.uuid4().hex[:12]}"
3296
+ pipeline_dir.mkdir(parents=True, exist_ok=True)
3297
+
3298
+ # Detect most useful prior run for checkpoint/resume
3299
+ prior_run_dir = _select_prior_run_dir(
3300
+ runs_parent,
3301
+ pipeline_dir,
3302
+ fallback_runs_parent=_legacy_integration_runs_dir(repo_root, idea_id),
3303
+ expected_fingerprint=str(resume_freshness.get("fingerprint") or "") or None,
3304
+ )
3305
+
3306
+ wf = IntegrationWorkflow()
3307
+ ctx = wf.run({
3308
+ "repo_root": str(repo_root),
3309
+ "idea_id": idea_id,
3310
+ "implemented_idea": implemented_idea,
3311
+ "implemented_stories": implemented_stories,
3312
+ "code_evidence": code_evidence,
3313
+ "source_docs": source_docs,
3314
+ "pipeline_dir": str(pipeline_dir),
3315
+ "prior_run_dir": str(prior_run_dir) if prior_run_dir else None,
3316
+ "resume_freshness": resume_freshness,
3317
+ "project_id": project_id,
3318
+ "dag_run_id": dag_run_id,
3319
+ })
3320
+
3321
+ artifacts = dict(ctx.metadata.get("artifacts") or {})
3322
+ validation_report = dict(ctx.metadata.get("validation_report") or {})
3323
+ iterations_used = int(ctx.metadata.get("iterations_used") or 0)
3324
+ if ctx.metadata.get("structural_only"):
3325
+ return IntegrationDagResult(
3326
+ exit_code=0,
3327
+ message="integration dag complete; structural-only idea (no integration seams)\n",
3328
+ pipeline_dir=pipeline_dir,
3329
+ artifacts=artifacts,
3330
+ iterations_used=iterations_used,
3331
+ )
3332
+ if not ctx.should_stop:
3333
+ return IntegrationDagResult(
3334
+ exit_code=0,
3335
+ message="integration dag complete; downstream red/redreview/green/greenenrich/commit artifacts written\n",
3336
+ pipeline_dir=pipeline_dir,
3337
+ artifacts=artifacts,
3338
+ iterations_used=iterations_used,
3339
+ )
3340
+ return IntegrationDagResult(
3341
+ exit_code=1,
3342
+ message="integration dag failed\n",
3343
+ pipeline_dir=pipeline_dir,
3344
+ artifacts=artifacts,
3345
+ iterations_used=iterations_used,
3346
+ )
3347
+
3348
+
3349
+ def backfill_integration_current(*, repo_root: Path, idea_id: str) -> dict[str, Any]:
3350
+ current_dir = _integration_current_dir(repo_root, idea_id)
3351
+ current_dir.mkdir(parents=True, exist_ok=True)
3352
+ runs_dir = _integration_runs_dir(repo_root, idea_id)
3353
+ legacy_runs_dir = _legacy_integration_runs_dir(repo_root, idea_id)
3354
+ payload = json.loads(prepare_integration_payload(repo_root=repo_root, idea_id=idea_id).read_text(encoding="utf-8"))
3355
+ resume_freshness = _compute_integration_freshness(
3356
+ repo_root=repo_root,
3357
+ idea_id=idea_id,
3358
+ payload_body=_payload_body_without_freshness(payload),
3359
+ )
3360
+ selected = _select_prior_run_dir(
3361
+ runs_dir,
3362
+ current_dir / "_ignore",
3363
+ fallback_runs_parent=legacy_runs_dir,
3364
+ expected_fingerprint=str(resume_freshness.get("fingerprint") or "") or None,
3365
+ )
3366
+ copied: list[str] = []
3367
+ if selected is None:
3368
+ return {
3369
+ "idea_id": idea_id,
3370
+ "selected_run": None,
3371
+ "current_dir": str(current_dir),
3372
+ "copied": copied,
3373
+ }
3374
+ for path in sorted(selected.iterdir()):
3375
+ if not path.is_file() or path.name.endswith("_agent_run.json"):
3376
+ continue
3377
+ target = current_dir / path.name
3378
+ try:
3379
+ data = json.loads(path.read_text(encoding="utf-8"))
3380
+ except Exception:
3381
+ continue
3382
+ _write_json(target, data)
3383
+ copied.append(path.name)
3384
+ _write_resume_metadata(current_dir, resume_freshness)
3385
+ return {
3386
+ "idea_id": idea_id,
3387
+ "selected_run": str(selected),
3388
+ "current_dir": str(current_dir),
3389
+ "copied": copied,
3390
+ }
3391
+
3392
+
3393
+ def _describe_postgrest_exception(exc: Exception) -> str:
3394
+ if isinstance(exc, HTTPError):
3395
+ try:
3396
+ body = exc.read().decode("utf-8", errors="replace").strip()
3397
+ except Exception:
3398
+ body = ""
3399
+ if body:
3400
+ return f"HTTP {exc.code}: {body}"
3401
+ return f"HTTP {exc.code}: {exc.reason}"
3402
+ return str(exc) or exc.__class__.__name__
3403
+
3404
+
3405
+ def _sync_integration_to_supabase(
3406
+ *,
3407
+ idea_id: str,
3408
+ project_id: str,
3409
+ run_id: str,
3410
+ repo_root: Path | None = None,
3411
+ result: IntegrationDagResult | None = None,
3412
+ status: str = "running",
3413
+ ) -> None:
3414
+ config = _resolve_supabase_rest_config()
3415
+ if config is None:
3416
+ return
3417
+ if not project_id or project_id.startswith("unregistered:"):
3418
+ return
3419
+ url, key = config
3420
+ resolved_project_id = str(project_id or "").strip()
3421
+ if not _is_uuid_like(resolved_project_id):
3422
+ if repo_root is None:
3423
+ project_entry = resolve_project_entry(project_id)
3424
+ repo_root_value = str((project_entry or {}).get("repo_root") or "").strip()
3425
+ if repo_root_value:
3426
+ repo_root = Path(repo_root_value).expanduser()
3427
+ if repo_root is None:
3428
+ logger.warning(
3429
+ "integration-supabase-sync: skipping sync for idea_id=%s run_id=%s because local project_id=%s is not canonical and repo_root is unavailable",
3430
+ idea_id,
3431
+ run_id,
3432
+ project_id,
3433
+ )
3434
+ return
3435
+ project_entry = find_project_for_repo_root(repo_root)
3436
+ remote_url = str((project_entry or {}).get("remote_url") or "").strip() or None
3437
+ project_name = str((project_entry or {}).get("name") or "").strip() or None
3438
+ owner, repo = _infer_owner_repo(remote_url, repo_root, project_name)
3439
+ try:
3440
+ resolved_project_id = _lookup_supabase_project_uuid(
3441
+ url=url,
3442
+ key=key,
3443
+ repo_root=repo_root,
3444
+ remote_url=remote_url,
3445
+ owner=owner,
3446
+ repo=repo,
3447
+ ) or ""
3448
+ except Exception as exc:
3449
+ logger.warning(
3450
+ "integration-supabase-sync: skipping sync for idea_id=%s run_id=%s because canonical project UUID lookup failed for local project_id=%s: %s",
3451
+ idea_id,
3452
+ run_id,
3453
+ project_id,
3454
+ exc,
3455
+ )
3456
+ return
3457
+ if not _is_uuid_like(resolved_project_id):
3458
+ logger.warning(
3459
+ "integration-supabase-sync: skipping sync for idea_id=%s run_id=%s because canonical project UUID could not be resolved from local project_id=%s",
3460
+ idea_id,
3461
+ run_id,
3462
+ project_id,
3463
+ )
3464
+ return
3465
+ from datetime import UTC, datetime
3466
+
3467
+ now = datetime.now(UTC).isoformat()
3468
+
3469
+ def _load_artifact(key_name: str) -> dict[str, Any] | None:
3470
+ if result is None:
3471
+ return None
3472
+ path_str = result.artifacts.get(key_name)
3473
+ if not path_str:
3474
+ return None
3475
+ p = Path(path_str)
3476
+ if not p.exists():
3477
+ return None
3478
+ try:
3479
+ return json.loads(p.read_text(encoding="utf-8"))
3480
+ except Exception:
3481
+ return None
3482
+
3483
+ side_effects = _load_artifact("side_effects")
3484
+ implicated_users = _load_artifact("implicated_users")
3485
+ workflow_inventory = _load_artifact("workflow_inventory")
3486
+ validation_report = _load_artifact("validation_gate")
3487
+ red_package = _load_artifact("red_package")
3488
+ red_review = _load_artifact("red_review")
3489
+ green_package = _load_artifact("green_package")
3490
+ green_enrich = _load_artifact("green_enrich")
3491
+ commit_package = _load_artifact("commit_package")
3492
+
3493
+ side_effect_count = len((side_effects or {}).get("side_effects") or []) if side_effects else 0
3494
+ workflow_count = len((workflow_inventory or {}).get("workflow_ids") or []) if workflow_inventory else 0
3495
+
3496
+ effective_status = status
3497
+ if validation_report and validation_report.get("structural_only"):
3498
+ effective_status = "structural_only"
3499
+
3500
+ row = {
3501
+ "idea_id": idea_id,
3502
+ "project_id": resolved_project_id,
3503
+ "run_id": run_id,
3504
+ "pipeline_dir": str(result.pipeline_dir) if result is not None else None,
3505
+ "status": effective_status,
3506
+ "exit_code": result.exit_code if result is not None else None,
3507
+ "iterations_used": result.iterations_used if result is not None else None,
3508
+ "workflow_count": workflow_count,
3509
+ "side_effect_count": side_effect_count,
3510
+ "side_effects": side_effects,
3511
+ "implicated_users": implicated_users,
3512
+ "workflow_inventory": workflow_inventory,
3513
+ "validation_report": validation_report,
3514
+ "red_package": red_package,
3515
+ "red_review": red_review,
3516
+ "green_package": green_package,
3517
+ "green_enrich": green_enrich,
3518
+ "commit_package": commit_package,
3519
+ "failure_message": result.message if (result is not None and result.exit_code != 0) else None,
3520
+ "updated_at": now,
3521
+ }
3522
+ try:
3523
+ _postgrest_request(
3524
+ method="POST",
3525
+ url=f"{url}/rest/v1/devflow_idea_integrations?on_conflict=idea_id",
3526
+ key=key,
3527
+ body=[row],
3528
+ prefer="resolution=merge-duplicates",
3529
+ )
3530
+ except Exception as exc:
3531
+ logger.warning(
3532
+ "integration-supabase-sync: failed for idea_id=%s run_id=%s status=%s project_id=%s row_keys=%s error=%s",
3533
+ idea_id,
3534
+ run_id,
3535
+ effective_status,
3536
+ resolved_project_id,
3537
+ sorted(row.keys()),
3538
+ _describe_postgrest_exception(exc),
3539
+ )